{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "command-search",
  "title": "Command Search",
  "description": "The nx-ui Command Search block.",
  "dependencies": [
    "@untitledui/icons@^0.0.23",
    "cmdk@^1.1.1"
  ],
  "registryDependencies": [
    "@nx-ui/badge",
    "@nx-ui/command",
    "@nx-ui/kbd",
    "@nx-ui/skeleton",
    "@nx-ui/utils"
  ],
  "files": [
    {
      "path": "components/blocks/command-search.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { useCommandState } from \"cmdk\"\nimport {\n  AlertCircle,\n  Clock,\n  CornerDownLeft,\n  RefreshCcw01,\n  SearchLg,\n} from \"@untitledui/icons\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/components/ui/badge\"\nimport {\n  Command,\n  CommandDialog,\n  CommandEmpty,\n  CommandGroup,\n  CommandItem,\n  CommandInput,\n  CommandList,\n} from \"@/components/ui/command\"\nimport { Kbd } from \"@/components/ui/kbd\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\n\n/**\n * CommandSearch — a framework-neutral ⌘K command palette.\n *\n * Nothing here imports `next/*` or assumes a router: navigation goes through\n * `onNavigate`, which defaults to `window.location.assign(href)`, and an item\n * may instead carry its own `onSelect`. The block is safe to render on a\n * server (Next, a Cloudflare Worker, Vite SSR): no `window`, `localStorage` or\n * `navigator` is touched during render — only inside effects and handlers.\n *\n * Three result sources compose:\n *   - `staticGroups` — always-available pages and actions, filtered in the\n *     browser,\n *   - `search` — an async callback for remote hits, debounced, aborted when it\n *     goes stale, and never rendered out of order,\n *   - `recents` — opt-in `localStorage` persistence of previously chosen items,\n *     gated by a consumer-supplied `filter` so personal data need never be\n *     written to disk.\n */\n\n/** One selectable row. `id` must be unique across every group. */\nexport type CommandSearchItem = {\n  id: string\n  title: string\n  subtitle?: string\n  /** Navigated to via `onNavigate` when the row has no `onSelect`. */\n  href?: string\n  badge?: string\n  icon?: React.ReactNode\n  /** Extra match terms; also handed to cmdk when it does the filtering. */\n  keywords?: string[]\n  /** Runs instead of navigation when present. */\n  onSelect?: () => void\n}\n\n/** A labelled set of rows. `key` must be unique across the rendered list. */\nexport type CommandSearchGroup = {\n  key: string\n  heading: string\n  items: CommandSearchItem[]\n  /** Shows a \"more matches exist\" hint under the group. */\n  truncated?: boolean\n}\n\n/** Called when a row with an `href` (and no `onSelect`) is chosen. */\nexport type CommandSearchNavigate = (\n  href: string,\n  item: CommandSearchItem\n) => void\n\n/** Remote search. Reject or return late and the response is discarded. */\nexport type CommandSearchFetcher = (\n  query: string,\n  options: { signal: AbortSignal }\n) => Promise<CommandSearchGroup[]>\n\n/** Opt-in recents persistence. Only items `filter` accepts are ever written. */\nexport type CommandSearchRecentsOptions = {\n  /** `localStorage` key. Namespace it per product and per signed-in actor. */\n  storageKey: string\n  /** How many recents to keep. Default 5. */\n  max?: number\n  /**\n   * Gate for what may be persisted. Return false for anything that must not\n   * reach `localStorage` (people, cases, anything personal). Default: keep\n   * nothing — recents are opt-in per item, not opt-out.\n   */\n  filter?: (item: CommandSearchItem) => boolean\n}\n\n/**\n * Hotkey descriptor: `\"mod+k\"` (⌘ on Apple platforms, Ctrl elsewhere),\n * `\"ctrl+k\"`, `\"shift+/\"`, a bare `\"/\"`, or `false` to disable the shortcut.\n */\nexport type CommandSearchHotkey = string | false\n\nexport type CommandSearchProps = {\n  /** Controlled open state. Omit for an uncontrolled palette. */\n  open?: boolean\n  onOpenChange?: (open: boolean) => void\n  /** Initial open state when uncontrolled. Default false. */\n  defaultOpen?: boolean\n  /** Global shortcut that toggles the palette. Default `\"mod+k\"`. */\n  hotkey?: CommandSearchHotkey\n  placeholder?: string\n  /** Accessible dialog name. Default \"Search\". */\n  title?: string\n  /** Accessible dialog description, announced on open. */\n  description?: string\n  /** Always-available pages and actions, filtered in the browser. */\n  staticGroups?: CommandSearchGroup[]\n  /** Remote results. Debounced, aborted and ordered by the block. */\n  search?: CommandSearchFetcher\n  /**\n   * The context `search` answers in — an organisation id, an account id, a\n   * data-version stamp. Change it and the palette invalidates: anything in\n   * flight is discarded, the rendered remote results are cleared, and the\n   * current query is run again against the new context. Leave it out only when\n   * one mounted palette can never span two contexts.\n   *\n   * Changing the `search` function's identity invalidates the in-flight\n   * request too, but does not clear what is already on screen — so an inline\n   * arrow recreated by a parent re-render costs a re-issued request, not a\n   * flicker. Prefer a `useCallback`ed fetcher plus an explicit `searchKey`.\n   */\n  searchKey?: string | number\n  /** Characters required before `search` runs. Default 2. */\n  minQueryLength?: number\n  /** Idle time before `search` runs, in ms. Default 150. */\n  debounceMs?: number\n  recents?: CommandSearchRecentsOptions\n  /** Heading for the recents group. Default \"Recent\". */\n  recentsHeading?: string\n  emptyLabel?: React.ReactNode\n  errorLabel?: React.ReactNode\n  loadingLabel?: string\n  /** Rendered under the list — hints, counts, a \"powered by\" line. */\n  footer?: React.ReactNode\n  /** Navigation. Default `window.location.assign(href)`. */\n  onNavigate?: CommandSearchNavigate\n  className?: string\n}\n\nconst DEFAULT_HOTKEY = \"mod+k\"\nconst DEFAULT_MIN_QUERY_LENGTH = 2\nconst DEFAULT_DEBOUNCE_MS = 150\nconst DEFAULT_RECENTS_MAX = 5\nconst RECENTS_GROUP_KEY = \"__recents__\"\n\n// ---------------------------------------------------------------------------\n// Pure helpers — exported so they can be unit-tested without a DOM.\n// ---------------------------------------------------------------------------\n\n/** A parsed hotkey. `mod` means ⌘ on Apple platforms and Ctrl everywhere else. */\nexport type ParsedCommandSearchHotkey = {\n  key: string\n  mod: boolean\n  meta: boolean\n  ctrl: boolean\n  alt: boolean\n  shift: boolean\n}\n\n/** Parse `\"mod+k\"` / `\"ctrl+shift+p\"` / `\"/\"`. Returns null for `false` or junk. */\nexport function parseCommandSearchHotkey(\n  hotkey: CommandSearchHotkey | undefined\n): ParsedCommandSearchHotkey | null {\n  if (hotkey === false) return null\n  const source = (hotkey ?? DEFAULT_HOTKEY).trim()\n  if (!source) return null\n\n  const parts = source\n    .split(\"+\")\n    .map((part) => part.trim().toLowerCase())\n    .filter(Boolean)\n  if (parts.length === 0) return null\n\n  const parsed: ParsedCommandSearchHotkey = {\n    key: \"\",\n    mod: false,\n    meta: false,\n    ctrl: false,\n    alt: false,\n    shift: false,\n  }\n\n  for (const part of parts) {\n    if (part === \"mod\" || part === \"cmdorctrl\") parsed.mod = true\n    else if (part === \"meta\" || part === \"cmd\" || part === \"command\") parsed.meta = true\n    else if (part === \"ctrl\" || part === \"control\") parsed.ctrl = true\n    else if (part === \"alt\" || part === \"option\") parsed.alt = true\n    else if (part === \"shift\") parsed.shift = true\n    else parsed.key = part\n  }\n\n  return parsed.key ? parsed : null\n}\n\n/** True when the hotkey needs a modifier held down. */\nexport function commandSearchHotkeyHasModifier(\n  parsed: ParsedCommandSearchHotkey\n): boolean {\n  return parsed.mod || parsed.meta || parsed.ctrl || parsed.alt\n}\n\n/** The subset of a `KeyboardEvent` the matcher needs — keeps tests DOM-free. */\nexport type CommandSearchKeyEvent = {\n  key: string\n  metaKey: boolean\n  ctrlKey: boolean\n  altKey: boolean\n  shiftKey: boolean\n}\n\n/** Does this keystroke fire the hotkey? `apple` picks ⌘ vs Ctrl for `mod`. */\nexport function matchesCommandSearchHotkey(\n  event: CommandSearchKeyEvent,\n  parsed: ParsedCommandSearchHotkey | null,\n  apple: boolean\n): boolean {\n  if (!parsed) return false\n  if (event.key.toLowerCase() !== parsed.key) return false\n\n  const wantMeta = parsed.meta || (parsed.mod && apple)\n  const wantCtrl = parsed.ctrl || (parsed.mod && !apple)\n\n  if (event.metaKey !== wantMeta) return false\n  if (event.ctrlKey !== wantCtrl) return false\n  if (event.altKey !== parsed.alt) return false\n  // A bare `/` arrives with Shift on some layouts, so shift is only enforced\n  // when the descriptor asks for it.\n  if (parsed.shift && !event.shiftKey) return false\n\n  return true\n}\n\n/** The bit of an event target the typing check needs. */\nexport type CommandSearchEventTarget = {\n  tagName?: string\n  isContentEditable?: boolean\n  getAttribute?: (name: string) => string | null\n}\n\n/**\n * True when the keystroke landed in a text-entry surface. An unmodified\n * hotkey (`/`) must not hijack someone mid-sentence.\n */\nexport function isTypingTarget(target: unknown): boolean {\n  if (!target || typeof target !== \"object\") return false\n  const element = target as CommandSearchEventTarget\n  if (element.isContentEditable) return true\n\n  const tag = (element.tagName ?? \"\").toUpperCase()\n  if (tag === \"INPUT\" || tag === \"TEXTAREA\" || tag === \"SELECT\") return true\n\n  const role = element.getAttribute?.(\"role\")\n  return role === \"textbox\" || role === \"searchbox\" || role === \"combobox\"\n}\n\n/** The text a local filter matches against. */\nexport function commandSearchHaystack(\n  item: CommandSearchItem,\n  heading?: string\n): string {\n  return [item.title, item.subtitle, heading, ...(item.keywords ?? [])]\n    .filter(Boolean)\n    .join(\" \")\n    .toLocaleLowerCase()\n}\n\n/**\n * Local, case-insensitive, all-terms-must-match filter over static groups.\n * Empty groups are dropped; an empty query returns the groups untouched.\n */\nexport function filterCommandSearchGroups(\n  groups: readonly CommandSearchGroup[],\n  query: string\n): CommandSearchGroup[] {\n  const terms = query.trim().toLocaleLowerCase().split(/\\s+/).filter(Boolean)\n  if (terms.length === 0) return groups.map((group) => ({ ...group }))\n\n  return groups\n    .map((group) => ({\n      ...group,\n      items: group.items.filter((item) => {\n        const haystack = commandSearchHaystack(item, group.heading)\n        return terms.every((term) => haystack.includes(term))\n      }),\n    }))\n    .filter((group) => group.items.length > 0)\n}\n\n/** Total rows across groups — the number announced to screen readers. */\nexport function countCommandSearchItems(\n  groups: readonly CommandSearchGroup[]\n): number {\n  return groups.reduce((total, group) => total + group.items.length, 0)\n}\n\n/** Ignore a response once a newer request has started or this one was aborted. */\nexport function isCurrentCommandSearchRequest(\n  requestId: number,\n  latestRequestId: number,\n  signal: Pick<AbortSignal, \"aborted\">\n): boolean {\n  return !signal.aborted && requestId === latestRequestId\n}\n\n/** Where a remote search has got to. */\nexport type CommandSearchRemoteStatus = \"idle\" | \"loading\" | \"ready\" | \"error\"\n\n/**\n * Do the rendered remote results answer a query the user has already moved on\n * from?\n *\n * Remote groups are rendered with cmdk's filter switched off, so they would\n * otherwise stay fully clickable — and reachable by Enter — while the palette\n * loads what was typed next. A row offering a record that does not match the\n * visible query is worse than an empty list: it navigates somewhere the person\n * did not ask for.\n *\n * This compares the two queries and nothing else. Deriving it from a `loading`\n * status instead would leave a live window one render wide: a keystroke renders\n * the new input before the search effect runs, so results marked `ready` would\n * still be selectable for that frame even though they answer the old query.\n *\n * Re-issuing the *same* query (a retry, or a fetcher whose identity changed)\n * is therefore not superseded: those rows still answer what is on screen, so\n * they stay live and the list does not flicker.\n */\nexport function commandSearchResultsSuperseded(\n  resultsQuery: string,\n  pendingQuery: string\n): boolean {\n  return resultsQuery !== pendingQuery\n}\n\n/** The search context a rendered set of remote results belongs to. */\nexport type CommandSearchContext = {\n  search?: CommandSearchFetcher\n  searchKey?: string | number\n}\n\n/** What a change of search context requires of the palette. */\nexport type CommandSearchInvalidation = {\n  /** Retire the in-flight request so a late response cannot be applied. */\n  invalidate: boolean\n  /** Drop the remote results already on screen — they answer the old context. */\n  clearResults: boolean\n  /** Run the current query again against the new context. */\n  rerun: boolean\n}\n\nconst NO_INVALIDATION: CommandSearchInvalidation = {\n  invalidate: false,\n  clearResults: false,\n  rerun: false,\n}\n\n/**\n * Decide what a change of search context costs.\n *\n * A changed `searchKey` is a context switch — a different organisation, a\n * different account — so the results on screen are wrong, not merely stale:\n * retire the in-flight request, clear what is rendered, and search again.\n *\n * A changed `search` identity is weaker evidence: for a consumer passing an\n * inline arrow it happens on every parent re-render, so results are kept and\n * only the in-flight request is retired and re-issued, guaranteeing that no\n * response from the previous fetcher is ever applied.\n */\nexport function commandSearchInvalidation(\n  previous: CommandSearchContext,\n  next: CommandSearchContext\n): CommandSearchInvalidation {\n  if (previous.searchKey !== next.searchKey) {\n    return { invalidate: true, clearResults: true, rerun: true }\n  }\n  if (previous.search !== next.search) {\n    return { invalidate: true, clearResults: false, rerun: true }\n  }\n  return NO_INVALIDATION\n}\n\n/** ⌘K on Apple platforms, Ctrl K elsewhere. Returns null when disabled. */\nexport function commandSearchShortcutLabel(\n  hotkey: CommandSearchHotkey | undefined,\n  apple: boolean\n): string | null {\n  const parsed = parseCommandSearchHotkey(hotkey)\n  if (!parsed) return null\n\n  const parts: string[] = []\n  if (parsed.mod) parts.push(apple ? \"⌘\" : \"Ctrl\")\n  if (parsed.meta) parts.push(apple ? \"⌘\" : \"Win\")\n  if (parsed.ctrl && !(parsed.mod && !apple)) parts.push(\"Ctrl\")\n  if (parsed.alt) parts.push(apple ? \"⌥\" : \"Alt\")\n  if (parsed.shift) parts.push(apple ? \"⇧\" : \"Shift\")\n\n  const key = parsed.key.length === 1 ? parsed.key.toUpperCase() : parsed.key\n  const separator = apple ? \"\" : \" \"\n  return [...parts, key].join(separator)\n}\n\n/**\n * The `aria-keyshortcuts` value for a hotkey — both bindings when it is\n * platform-relative, so assistive tech announces the shortcut without the\n * component having to know which platform it is on during render.\n */\nexport function commandSearchAriaKeyShortcuts(\n  hotkey: CommandSearchHotkey | undefined\n): string | undefined {\n  const parsed = parseCommandSearchHotkey(hotkey)\n  if (!parsed) return undefined\n\n  const key = parsed.key.length === 1 ? parsed.key.toUpperCase() : parsed.key\n  const suffix = [parsed.alt ? \"Alt\" : null, parsed.shift ? \"Shift\" : null, key]\n    .filter(Boolean)\n    .join(\"+\")\n\n  const bindings: string[] = []\n  if (parsed.mod) bindings.push(`Meta+${suffix}`, `Control+${suffix}`)\n  if (parsed.meta && !parsed.mod) bindings.push(`Meta+${suffix}`)\n  if (parsed.ctrl && !parsed.mod) bindings.push(`Control+${suffix}`)\n  if (bindings.length === 0) bindings.push(suffix)\n\n  return [...new Set(bindings)].join(\" \")\n}\n\n/** The serialisable shape a recent is stored as — never an icon or a callback. */\nexport type StoredCommandSearchItem = Pick<\n  CommandSearchItem,\n  \"id\" | \"title\" | \"subtitle\" | \"href\" | \"badge\" | \"keywords\"\n>\n\n/** The `localStorage`-shaped surface the recents helpers need. */\nexport type CommandSearchStorage = Pick<Storage, \"getItem\" | \"setItem\">\n\nfunction toStoredItem(item: CommandSearchItem): StoredCommandSearchItem {\n  return {\n    id: item.id,\n    title: item.title,\n    subtitle: item.subtitle,\n    href: item.href,\n    badge: item.badge,\n    keywords: item.keywords,\n  }\n}\n\n/** Read persisted recents. Any malformed or unreadable store yields []. */\nexport function readCommandSearchRecents(\n  storage: CommandSearchStorage | null | undefined,\n  storageKey: string\n): StoredCommandSearchItem[] {\n  if (!storage) return []\n  try {\n    const raw = storage.getItem(storageKey)\n    if (!raw) return []\n    const parsed: unknown = JSON.parse(raw)\n    if (!Array.isArray(parsed)) return []\n    return parsed.filter(\n      (entry): entry is StoredCommandSearchItem =>\n        typeof entry === \"object\" &&\n        entry !== null &&\n        typeof (entry as StoredCommandSearchItem).id === \"string\" &&\n        typeof (entry as StoredCommandSearchItem).title === \"string\"\n    )\n  } catch {\n    return []\n  }\n}\n\n/** Move `item` to the front, de-duplicating by id and capping at `max`. */\nexport function mergeCommandSearchRecents(\n  existing: readonly StoredCommandSearchItem[],\n  item: CommandSearchItem,\n  max: number\n): StoredCommandSearchItem[] {\n  const next = [\n    toStoredItem(item),\n    ...existing.filter((entry) => entry.id !== item.id),\n  ]\n  return next.slice(0, Math.max(0, max))\n}\n\n/**\n * Can this item still do something when it comes back out of storage?\n *\n * Serialisation drops `onSelect`, so an action whose only behaviour was a\n * callback would be replayed as a row that silently does nothing. A recent is\n * replayable when it carries an `href` (navigation survives the round trip) or\n * when its id still matches a current static item, which is re-linked to the\n * live object on the way out — callback and icon intact.\n */\nexport function isReplayableCommandSearchItem(\n  item: Pick<CommandSearchItem, \"id\" | \"href\">,\n  replayableIds?: ReadonlySet<string>\n): boolean {\n  return Boolean(item.href) || Boolean(replayableIds?.has(item.id))\n}\n\n/**\n * Persist a chosen item — but only when `options.filter` accepts it. Without a\n * filter nothing is stored: keeping personal data out of `localStorage` is the\n * consumer's call to make explicitly. Items that could not be replayed (no\n * `href`, no matching static item in `replayableIds`) are never written, since\n * a stored row that does nothing is worse than no row. Returns the list now on\n * disk.\n */\nexport function writeCommandSearchRecent(\n  storage: CommandSearchStorage | null | undefined,\n  options: CommandSearchRecentsOptions,\n  item: CommandSearchItem,\n  replayableIds?: ReadonlySet<string>\n): StoredCommandSearchItem[] {\n  const existing = readCommandSearchRecents(storage, options.storageKey)\n  if (!storage) return existing\n  if (!options.filter?.(item)) return existing\n  if (!isReplayableCommandSearchItem(item, replayableIds)) return existing\n\n  const next = mergeCommandSearchRecents(\n    existing,\n    item,\n    options.max ?? DEFAULT_RECENTS_MAX\n  )\n  try {\n    storage.setItem(options.storageKey, JSON.stringify(next))\n  } catch {\n    // Private mode, a full quota or a blocked origin — recents are a nicety.\n    return existing\n  }\n  return next\n}\n\n/**\n * Turn stored recents back into rows.\n *\n * An entry whose id still matches a static item is replaced by that live\n * object, so its `onSelect`, icon and badge come back. An entry with an `href`\n * and no live match is navigable as it stands. Anything else is dropped rather\n * than rendered as a row that does nothing — which is also how a recent\n * persisted before an item was removed from `staticGroups` quietly retires.\n *\n * `fallbackIcon` is applied to rows that have no icon of their own, and only\n * here: it belongs to the recents group, not to every group that happens to be\n * rendered alongside it.\n */\nexport function buildCommandSearchRecentsGroup(\n  stored: readonly StoredCommandSearchItem[],\n  liveItemsById: ReadonlyMap<string, CommandSearchItem>,\n  heading: string,\n  fallbackIcon?: React.ReactNode,\n  groupKey = RECENTS_GROUP_KEY\n): CommandSearchGroup | null {\n  const items: CommandSearchItem[] = []\n\n  for (const entry of stored) {\n    const live = liveItemsById.get(entry.id)\n    if (!live && !entry.href) continue\n    const item: CommandSearchItem = live ?? entry\n    items.push(item.icon ? item : { ...item, icon: fallbackIcon })\n  }\n\n  return items.length > 0 ? { key: groupKey, heading, items } : null\n}\n\n// ---------------------------------------------------------------------------\n// Small hooks\n// ---------------------------------------------------------------------------\n\n/** Controlled-or-uncontrolled open state. */\nfunction useOpenState(\n  controlled: boolean | undefined,\n  defaultOpen: boolean,\n  onOpenChange?: (open: boolean) => void\n): [boolean, (next: boolean | ((previous: boolean) => boolean)) => void] {\n  const [uncontrolled, setUncontrolled] = React.useState(defaultOpen)\n  const isControlled = controlled !== undefined\n  const open = isControlled ? controlled : uncontrolled\n\n  const openRef = React.useRef(open)\n  openRef.current = open\n\n  const setOpen = React.useCallback(\n    (next: boolean | ((previous: boolean) => boolean)) => {\n      const value = typeof next === \"function\" ? next(openRef.current) : next\n      if (!isControlled) setUncontrolled(value)\n      onOpenChange?.(value)\n    },\n    [isControlled, onOpenChange]\n  )\n\n  return [open, setOpen]\n}\n\n/**\n * Apple-platform detection, resolved after mount only. Server render and the\n * first client render agree (both `false`), so hydration never mismatches.\n */\nexport function useApplePlatform(): { apple: boolean; mounted: boolean } {\n  const [state, setState] = React.useState({ apple: false, mounted: false })\n\n  React.useEffect(() => {\n    const platform =\n      // `userAgentData` is not in every lib.dom yet.\n      (navigator as { userAgentData?: { platform?: string } }).userAgentData\n        ?.platform ??\n      navigator.platform ??\n      \"\"\n    const apple = /mac|iphone|ipad|ipod/i.test(\n      `${platform} ${navigator.userAgent ?? \"\"}`\n    )\n    setState({ apple, mounted: true })\n  }, [])\n\n  return state\n}\n\nfunction safeLocalStorage(): CommandSearchStorage | null {\n  try {\n    if (typeof window === \"undefined\") return null\n    return window.localStorage\n  } catch {\n    return null\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Trigger\n// ---------------------------------------------------------------------------\n\nexport type CommandSearchTriggerProps = Omit<\n  React.ComponentProps<\"button\">,\n  \"children\"\n> & {\n  /** Visible label. Default \"Search…\". */\n  label?: React.ReactNode\n  /** Shortcut hint to render. Pass `false` to hide it. Default `\"mod+k\"`. */\n  hotkey?: CommandSearchHotkey\n  /** Hide the label below `sm`, keeping just the icon. Default true. */\n  collapseOnMobile?: boolean\n}\n\n/**\n * CommandSearchTrigger — the input-shaped button an app shell puts in its top\n * bar. The shortcut hint renders the platform's own modifier, but only after\n * mount: the server and the first client render both draw the non-Apple label\n * invisibly, so the width is reserved without a hydration mismatch.\n */\nfunction CommandSearchTrigger({\n  label = \"Search…\",\n  hotkey = DEFAULT_HOTKEY,\n  collapseOnMobile = true,\n  className,\n  type = \"button\",\n  ...props\n}: CommandSearchTriggerProps) {\n  const { apple, mounted } = useApplePlatform()\n  const shortcut = commandSearchShortcutLabel(hotkey, apple)\n  const keyShortcuts = commandSearchAriaKeyShortcuts(hotkey)\n\n  return (\n    <button\n      type={type}\n      data-slot=\"command-search-trigger\"\n      aria-keyshortcuts={keyShortcuts}\n      className={cn(\n        \"group flex w-full cursor-pointer items-center gap-2 rounded-lg bg-background py-2 pr-1.5 pl-3 text-left shadow-xs ring-1 ring-border ring-inset outline-none transition hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/25\",\n        className\n      )}\n      {...props}\n    >\n      <SearchLg\n        className=\"size-4 shrink-0 text-fg-quaternary\"\n        aria-hidden=\"true\"\n      />\n      <span\n        className={cn(\n          \"flex-1 truncate text-sm text-muted-foreground\",\n          collapseOnMobile && \"max-sm:hidden\"\n        )}\n      >\n        {label}\n      </span>\n      {shortcut ? (\n        <Kbd\n          aria-hidden=\"true\"\n          className={cn(\n            \"select-none\",\n            collapseOnMobile && \"max-sm:hidden\",\n            !mounted && \"invisible\"\n          )}\n        >\n          {shortcut}\n        </Kbd>\n      ) : null}\n    </button>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// Palette\n// ---------------------------------------------------------------------------\n\ntype RemoteState = {\n  /** The query the palette is currently trying to answer. */\n  query: string\n  /**\n   * The query `groups` actually answer. It lags `query` from the keystroke\n   * that supersedes them until their replacement lands, which is exactly the\n   * window in which those rows must not be selectable — see\n   * `commandSearchResultsSuperseded`.\n   */\n  resultsQuery: string\n  groups: CommandSearchGroup[]\n  status: CommandSearchRemoteStatus\n}\n\nconst IDLE_REMOTE: RemoteState = {\n  query: \"\",\n  resultsQuery: \"\",\n  groups: [],\n  status: \"idle\",\n}\n\nfunction itemValue(group: CommandSearchGroup, item: CommandSearchItem): string {\n  // cmdk matches on this string when it is doing the filtering, and uses it as\n  // the row's identity either way — so it carries the searchable text plus the\n  // id that makes it unique.\n  return [item.title, item.subtitle, group.heading, item.id]\n    .filter(Boolean)\n    .join(\" \")\n}\n\nfunction CommandSearchRow({\n  group,\n  item,\n  disabled,\n  onChoose,\n}: {\n  group: CommandSearchGroup\n  item: CommandSearchItem\n  /** Superseded rows: dimmed, unclickable, and skipped by keyboard selection. */\n  disabled?: boolean\n  onChoose: (item: CommandSearchItem) => void\n}) {\n  return (\n    <CommandItem\n      value={itemValue(group, item)}\n      keywords={item.keywords}\n      disabled={disabled}\n      aria-disabled={disabled || undefined}\n      onSelect={disabled ? undefined : () => onChoose(item)}\n    >\n      <span className=\"flex size-4 shrink-0 items-center justify-center text-fg-quaternary [&_svg]:size-4\">\n        {item.icon ?? <SearchLg aria-hidden=\"true\" />}\n      </span>\n      <span className=\"flex min-w-0 flex-1 flex-col\">\n        <span className=\"truncate\">{item.title}</span>\n        {item.subtitle ? (\n          <span className=\"truncate text-xs text-muted-foreground\">\n            {item.subtitle}\n          </span>\n        ) : null}\n      </span>\n      {item.badge ? (\n        <Badge variant=\"secondary\" className=\"ml-auto font-normal\">\n          {item.badge}\n        </Badge>\n      ) : null}\n    </CommandItem>\n  )\n}\n\nfunction CommandSearchGroups({\n  groups,\n  disabled,\n  onChoose,\n}: {\n  groups: readonly CommandSearchGroup[]\n  /** Set while these groups answer a query the user has already moved on from. */\n  disabled?: boolean\n  onChoose: (item: CommandSearchItem) => void\n}) {\n  return (\n    <>\n      {groups.map((group) => (\n        <CommandGroup key={group.key} heading={group.heading}>\n          {group.items.map((item) => (\n            <CommandSearchRow\n              key={`${group.key}:${item.id}`}\n              group={group}\n              item={item}\n              disabled={disabled}\n              onChoose={onChoose}\n            />\n          ))}\n          {group.truncated ? (\n            <p className=\"px-2 py-1.5 text-xs text-muted-foreground\">\n              More matches exist — keep typing to narrow them.\n            </p>\n          ) : null}\n        </CommandGroup>\n      ))}\n    </>\n  )\n}\n\nfunction CommandSearchSkeleton({ label }: { label: string }) {\n  return (\n    <div className=\"flex flex-col gap-1 p-1\" aria-hidden=\"true\" title={label}>\n      {[\"w-3/5\", \"w-4/5\", \"w-2/5\"].map((width) => (\n        <div key={width} className=\"flex items-center gap-2 px-2 py-2\">\n          <Skeleton className=\"size-4 shrink-0 rounded\" />\n          <Skeleton className={cn(\"h-3\", width)} />\n        </div>\n      ))}\n    </div>\n  )\n}\n\n/**\n * The polite live region. It sits inside `<Command>` so it can read cmdk's own\n * filtered count — accurate whether cmdk or this block did the filtering.\n */\nfunction CommandSearchStatus({\n  query,\n  loading,\n  error,\n  loadingLabel,\n  errorText,\n}: {\n  query: string\n  loading: boolean\n  error: boolean\n  loadingLabel: string\n  errorText: string\n}) {\n  const count = useCommandState((state) => state.filtered.count) ?? 0\n  const message = error\n    ? errorText\n    : loading\n      ? loadingLabel\n      : query.length === 0\n        ? \"\"\n        : `${count} ${count === 1 ? \"result\" : \"results\"}`\n\n  return (\n    <div role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\">\n      {message}\n    </div>\n  )\n}\n\nfunction CommandSearchError({\n  label,\n  onRetry,\n}: {\n  label: React.ReactNode\n  onRetry: () => void\n}) {\n  return (\n    <div\n      role=\"alert\"\n      className=\"flex flex-col items-center gap-2 px-4 py-7 text-center\"\n    >\n      <AlertCircle className=\"size-5 text-fg-error-primary\" aria-hidden=\"true\" />\n      <p className=\"text-sm text-muted-foreground\">{label}</p>\n      <button\n        type=\"button\"\n        onClick={onRetry}\n        className=\"inline-flex cursor-pointer items-center gap-1.5 rounded-md px-2 py-1 text-sm font-semibold text-text-brand-secondary outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/25\"\n      >\n        <RefreshCcw01 className=\"size-3.5\" aria-hidden=\"true\" />\n        Try again\n      </button>\n    </div>\n  )\n}\n\nfunction CommandSearch({\n  open: controlledOpen,\n  onOpenChange,\n  defaultOpen = false,\n  hotkey = DEFAULT_HOTKEY,\n  placeholder = \"Search…\",\n  title = \"Search\",\n  description = \"Search pages, records and actions.\",\n  staticGroups,\n  search,\n  searchKey,\n  minQueryLength = DEFAULT_MIN_QUERY_LENGTH,\n  debounceMs = DEFAULT_DEBOUNCE_MS,\n  recents,\n  recentsHeading = \"Recent\",\n  emptyLabel = \"No matches found.\",\n  errorLabel = \"Search is unavailable right now.\",\n  loadingLabel = \"Searching…\",\n  footer,\n  onNavigate,\n  className,\n}: CommandSearchProps) {\n  const [open, setOpen] = useOpenState(controlledOpen, defaultOpen, onOpenChange)\n  const [query, setQuery] = React.useState(\"\")\n  const [remote, setRemote] = React.useState<RemoteState>(IDLE_REMOTE)\n  const [retryToken, setRetryToken] = React.useState(0)\n  // Bumped when the search context changes, to re-run the search effect\n  // without putting the fetcher itself in its dependency list.\n  const [contextToken, setContextToken] = React.useState(0)\n  const [storedRecents, setStoredRecents] = React.useState<\n    StoredCommandSearchItem[]\n  >([])\n\n  const { apple } = useApplePlatform()\n  const restoreFocusRef = React.useRef<HTMLElement | null>(null)\n  const latestRequestRef = React.useRef(0)\n\n  // The fetcher is held in a ref and updated in an effect, not during render,\n  // so the effect below can see the identity it is replacing.\n  const searchRef = React.useRef(search)\n  const contextRef = React.useRef<CommandSearchContext>({ search, searchKey })\n  const navigateRef = React.useRef(onNavigate)\n  navigateRef.current = onNavigate\n  const recentsRef = React.useRef(recents)\n  recentsRef.current = recents\n\n  const resolvedStaticGroups = React.useMemo(\n    () => staticGroups ?? [],\n    [staticGroups]\n  )\n  const trimmedQuery = query.trim()\n  const remoteEnabled = Boolean(search)\n  const remoteActive = remoteEnabled && trimmedQuery.length >= minQueryLength\n\n  // --- search context invalidation ----------------------------------------\n  // Declared before the search effect so the ref it reads is already current\n  // by the time that effect runs in the same commit.\n  React.useEffect(() => {\n    const next = { search, searchKey }\n    const { invalidate, clearResults, rerun } = commandSearchInvalidation(\n      contextRef.current,\n      next\n    )\n    contextRef.current = next\n    searchRef.current = search\n    if (!invalidate) return\n\n    // Retire anything in flight: a response from the old context can no\n    // longer be applied even if its fetcher ignores the abort signal.\n    latestRequestRef.current += 1\n    if (clearResults) setRemote(IDLE_REMOTE)\n    if (rerun) setContextToken((token) => token + 1)\n  }, [search, searchKey])\n\n  // --- global hotkey ------------------------------------------------------\n  const parsedHotkey = React.useMemo(\n    () => parseCommandSearchHotkey(hotkey),\n    [hotkey]\n  )\n\n  React.useEffect(() => {\n    if (!parsedHotkey) return\n    const needsModifier = !commandSearchHotkeyHasModifier(parsedHotkey)\n\n    function onKeyDown(event: KeyboardEvent) {\n      if (!matchesCommandSearchHotkey(event, parsedHotkey, apple)) return\n      // An unmodified shortcut must not steal keystrokes from a text field.\n      if (needsModifier && isTypingTarget(event.target)) return\n      event.preventDefault()\n      setOpen((previous) => !previous)\n    }\n\n    document.addEventListener(\"keydown\", onKeyDown)\n    return () => document.removeEventListener(\"keydown\", onKeyDown)\n  }, [apple, parsedHotkey, setOpen])\n\n  // --- focus handling -----------------------------------------------------\n  const wasOpenRef = React.useRef(open)\n  React.useEffect(() => {\n    if (open && !wasOpenRef.current) {\n      const active = document.activeElement\n      restoreFocusRef.current =\n        active instanceof HTMLElement && active !== document.body ? active : null\n    }\n    if (!open && wasOpenRef.current) {\n      const target = restoreFocusRef.current\n      restoreFocusRef.current = null\n      // After the dialog has unmounted and released the focus trap.\n      const frame = window.requestAnimationFrame(() => {\n        if (target?.isConnected) target.focus()\n      })\n      wasOpenRef.current = open\n      return () => window.cancelAnimationFrame(frame)\n    }\n    wasOpenRef.current = open\n  }, [open])\n\n  // --- recents ------------------------------------------------------------\n  React.useEffect(() => {\n    if (!open || !recents) return\n    setStoredRecents(readCommandSearchRecents(safeLocalStorage(), recents.storageKey))\n  }, [open, recents])\n\n  // --- reset on close -----------------------------------------------------\n  React.useEffect(() => {\n    if (open) return\n    setQuery(\"\")\n    setRemote(IDLE_REMOTE)\n    latestRequestRef.current += 1\n  }, [open])\n\n  // --- remote search ------------------------------------------------------\n  React.useEffect(() => {\n    if (!open || !remoteEnabled) return\n    if (trimmedQuery.length < minQueryLength) {\n      latestRequestRef.current += 1\n      setRemote(IDLE_REMOTE)\n      return\n    }\n\n    const controller = new AbortController()\n    const requestId = ++latestRequestRef.current\n\n    // Keep whatever is on screen visible while the next page loads, but leave\n    // `resultsQuery` pointing at the query those rows actually answer — that\n    // is what makes them non-interactive once this query differs from it.\n    setRemote((previous) => ({\n      query: trimmedQuery,\n      resultsQuery: previous.resultsQuery,\n      groups: previous.groups,\n      status: \"loading\",\n    }))\n\n    const timer = window.setTimeout(() => {\n      const fetcher = searchRef.current\n      if (!fetcher) return\n      void fetcher(trimmedQuery, { signal: controller.signal })\n        .then((groups) => {\n          if (\n            !isCurrentCommandSearchRequest(\n              requestId,\n              latestRequestRef.current,\n              controller.signal\n            )\n          ) {\n            return\n          }\n          setRemote({\n            query: trimmedQuery,\n            resultsQuery: trimmedQuery,\n            groups: Array.isArray(groups) ? groups : [],\n            status: \"ready\",\n          })\n        })\n        .catch(() => {\n          if (\n            !isCurrentCommandSearchRequest(\n              requestId,\n              latestRequestRef.current,\n              controller.signal\n            )\n          ) {\n            return\n          }\n          setRemote({\n            query: trimmedQuery,\n            resultsQuery: \"\",\n            groups: [],\n            status: \"error\",\n          })\n        })\n    }, debounceMs)\n\n    return () => {\n      window.clearTimeout(timer)\n      controller.abort()\n    }\n  }, [\n    contextToken,\n    debounceMs,\n    minQueryLength,\n    open,\n    remoteEnabled,\n    retryToken,\n    trimmedQuery,\n  ])\n\n  // --- selection ----------------------------------------------------------\n  // Every id currently in `staticGroups`. A recent is only worth persisting\n  // when it can be replayed: either it navigates, or it is one of these and\n  // can be re-linked to the live item on the way back out.\n  const liveItemsById = React.useMemo(() => {\n    const map = new Map<string, CommandSearchItem>()\n    for (const group of resolvedStaticGroups) {\n      for (const item of group.items) map.set(item.id, item)\n    }\n    return map\n  }, [resolvedStaticGroups])\n\n  const liveIdsRef = React.useRef<ReadonlySet<string>>(new Set())\n  liveIdsRef.current = React.useMemo(\n    () => new Set(liveItemsById.keys()),\n    [liveItemsById]\n  )\n\n  const chooseItem = React.useCallback((item: CommandSearchItem) => {\n    const recentsOptions = recentsRef.current\n    if (recentsOptions) {\n      setStoredRecents(\n        writeCommandSearchRecent(\n          safeLocalStorage(),\n          recentsOptions,\n          item,\n          liveIdsRef.current\n        )\n      )\n    }\n\n    setOpen(false)\n\n    if (item.onSelect) {\n      item.onSelect()\n      return\n    }\n    if (!item.href) return\n\n    const navigate = navigateRef.current\n    if (navigate) navigate(item.href, item)\n    else window.location.assign(item.href)\n  }, [setOpen])\n\n  // --- what to render -----------------------------------------------------\n  // cmdk filters for us only when there is no live remote query; once remote\n  // results are in play its built-in filter would hide server hits that do not\n  // literally contain the typed string, so we filter the static groups here\n  // instead and hand cmdk an unfiltered list.\n  const shouldFilter = !remoteActive\n\n  const localGroups = React.useMemo(\n    () =>\n      shouldFilter\n        ? resolvedStaticGroups\n        : filterCommandSearchGroups(resolvedStaticGroups, trimmedQuery),\n    [resolvedStaticGroups, shouldFilter, trimmedQuery]\n  )\n\n  const recentsGroup = React.useMemo<CommandSearchGroup | null>(() => {\n    if (!recents || trimmedQuery.length > 0 || storedRecents.length === 0) {\n      return null\n    }\n    // The clock belongs to this group alone — the other groups' iconless rows\n    // keep the generic search glyph.\n    return buildCommandSearchRecentsGroup(\n      storedRecents,\n      liveItemsById,\n      recentsHeading,\n      <Clock aria-hidden=\"true\" />\n    )\n  }, [liveItemsById, recents, recentsHeading, storedRecents, trimmedQuery])\n\n  const remoteGroups = remoteActive ? remote.groups : []\n  const showLoading = remoteActive && remote.status === \"loading\"\n  const showError = remoteActive && remote.status === \"error\"\n\n  // Remote rows that answer an earlier query stay on screen for continuity,\n  // but must not be selectable — by pointer or by Enter — or the palette would\n  // navigate to a record the visible query never asked for.\n  const remoteSuperseded =\n    remoteActive &&\n    commandSearchResultsSuperseded(remote.resultsQuery, trimmedQuery)\n\n  const localRendered: CommandSearchGroup[] = [\n    ...(recentsGroup ? [recentsGroup] : []),\n    ...localGroups,\n  ]\n\n  const showSkeleton = showLoading && remoteGroups.length === 0\n\n  return (\n    <CommandDialog\n      open={open}\n      onOpenChange={setOpen}\n      title={title}\n      description={description}\n      className={className}\n    >\n      <Command shouldFilter={shouldFilter} label={title}>\n        <CommandInput\n          placeholder={placeholder}\n          value={query}\n          onValueChange={setQuery}\n        />\n        <CommandList>\n          {/* cmdk shows this only when nothing matched — true in both modes,\n              since with shouldFilter=false its count is the rendered rows. */}\n          {!showError && !showLoading ? (\n            <CommandEmpty>{emptyLabel}</CommandEmpty>\n          ) : null}\n\n          {showError ? (\n            <CommandSearchError\n              label={errorLabel}\n              onRetry={() => setRetryToken((token) => token + 1)}\n            />\n          ) : null}\n\n          {/* Static groups and recents always answer the visible query — they\n              are filtered locally — so they stay live throughout. */}\n          <CommandSearchGroups groups={localRendered} onChoose={chooseItem} />\n          <CommandSearchGroups\n            groups={remoteGroups}\n            disabled={remoteSuperseded}\n            onChoose={chooseItem}\n          />\n\n          {showSkeleton ? <CommandSearchSkeleton label={loadingLabel} /> : null}\n        </CommandList>\n\n        <CommandSearchStatus\n          query={trimmedQuery}\n          loading={showLoading}\n          error={showError}\n          loadingLabel={loadingLabel}\n          errorText={typeof errorLabel === \"string\" ? errorLabel : \"Search failed.\"}\n        />\n      </Command>\n\n      {footer ?? (\n        <div className=\"flex items-center justify-between border-t border-border px-3 py-2 text-xs text-muted-foreground\">\n          <span className=\"flex items-center gap-1.5\">\n            <Kbd>\n              <CornerDownLeft className=\"size-3\" aria-hidden=\"true\" />\n            </Kbd>\n            to select\n          </span>\n          <span className=\"flex items-center gap-1.5\">\n            <Kbd>Esc</Kbd>\n            to close\n          </span>\n        </div>\n      )}\n    </CommandDialog>\n  )\n}\n\nexport { CommandSearch, CommandSearchTrigger }\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:block"
}