{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "command-menu",
  "title": "Command menu",
  "author": "Tori Bryan",
  "description": "A Cmd+K palette with nested pages, recent commands, ranked search and a preview pane.",
  "dependencies": [
    "@base-ui/react",
    "class-variance-authority",
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "https://fibo.toribryan.com/r/badge.json",
    "https://fibo.toribryan.com/r/button.json",
    "https://fibo.toribryan.com/r/empty-state.json",
    "https://fibo.toribryan.com/r/input-group.json",
    "https://fibo.toribryan.com/r/kbd.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/command-menu.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Autocomplete as AutocompletePrimitive } from \"@base-ui/react/autocomplete\"\nimport { Dialog as DialogPrimitive } from \"@base-ui/react/dialog\"\nimport { cva } from \"class-variance-authority\"\nimport { motion, useReducedMotion, type Transition } from \"motion/react\"\nimport { ChevronLeftIcon, ChevronRightIcon, SearchIcon } from \"lucide-react\"\n\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  EmptyState,\n  EmptyStateTitle,\n} from \"@/components/ui/empty-state\"\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupInput,\n} from \"@/components/ui/input-group\"\nimport { Kbd, KbdGroup } from \"@/components/ui/kbd\"\nimport { cn } from \"@/lib/utils\"\n\ntype CommandMenuItem = {\n  /** Unique across the whole menu, nested pages included. */\n  value: string\n  label: string\n  icon?: React.ReactNode\n  /** Keys shown on the right, such as `[\"⌘\", \"N\"]`. Display only. */\n  shortcut?: string[]\n  /** Extra words a search matches, such as synonyms. */\n  keywords?: string[]\n  disabled?: boolean\n  /** Runs when the item is picked, before the menu closes. */\n  onSelect?: () => void\n  /** Turns the item into a page: picking it opens these items instead. */\n  items?: CommandMenuItem[]\n  /** Hint in the search box while this item's page is open. */\n  placeholder?: string\n  /** Shown in the side pane while the item is highlighted. */\n  preview?: React.ReactNode\n}\n\ntype CommandMenuGroup = {\n  label: string\n  items: CommandMenuItem[]\n}\n\ntype CommandMenuLabels = {\n  /** Heads the recently run commands. */\n  recent: string\n  /** Heads search results. */\n  results: string\n  /** The back button on a page, named after where it goes. */\n  back: (to: string) => string\n  /** Where the first back button goes. */\n  home: string\n  /** Fills the preview pane when the highlighted item has none. */\n  noPreview: string\n  /** Announces how many results a search found. */\n  count: (count: number) => string\n}\n\nconst DEFAULT_LABELS: CommandMenuLabels = {\n  recent: \"Recent\",\n  results: \"Results\",\n  back: (to) => `Back to ${to}`,\n  home: \"all commands\",\n  noPreview: \"No preview\",\n  count: (count) => `${count} ${count === 1 ? \"result\" : \"results\"}`,\n}\n\ntype CommandMenuProps = {\n  /** The commands, in groups shown in order. */\n  groups: CommandMenuGroup[]\n  /** Called with every command run, after the item's own `onSelect`. */\n  onSelect?: (item: CommandMenuItem) => void\n  /** Whether the menu is open, when you control it. */\n  open?: boolean\n  /** Whether the menu starts open, when it keeps its own state. */\n  defaultOpen?: boolean\n  /** Called when the menu opens or closes. */\n  onOpenChange?: (open: boolean) => void\n  /** Values of recently run commands, newest first, when you control them. */\n  recent?: string[]\n  /** Recently run values to start with, when the menu keeps its own. */\n  defaultRecent?: string[]\n  /** Called with the new list each time a command runs. */\n  onRecentChange?: (recent: string[]) => void\n  /** How many recent commands to keep and show. `0` turns recents off. */\n  maxRecent?: number\n  /** A localStorage key that keeps recents across visits. */\n  storageKey?: string\n  /**\n   * The letter that toggles the menu with Cmd on macOS or Ctrl elsewhere.\n   * `null` turns the shortcut off.\n   */\n  hotkey?: string | null\n  /**\n   * An element that opens the menu, in place of the default search button.\n   * It must forward its ref and props, as fibo's Button does. `null` renders\n   * no trigger, for a menu opened only by the shortcut or from state.\n   */\n  trigger?: React.ReactElement | null\n  /** Hint in the search box on the first page. */\n  placeholder?: string\n  /** Shown when a search matches nothing. */\n  emptyText?: React.ReactNode\n  /** Names the dialog and the search box for assistive technology. */\n  label?: string\n  /** Wording the menu writes for itself, for translation. */\n  labels?: Partial<CommandMenuLabels>\n  /** Whether to show the row of keyboard hints under the list. */\n  hints?: boolean\n  /**\n   * `false` opens the dialog without trapping focus, locking the page's\n   * scroll or moving focus into it, for a menu shown inside a card or cover\n   * rather than over the page.\n   */\n  modal?: boolean\n  /** `inset` sets the list in a card inside a muted shell that holds the search and hints. */\n  variant?: \"default\" | \"inset\"\n  /** Where the dialog portals to. Defaults to the body. */\n  container?: DialogPrimitive.Portal.Props[\"container\"]\n  /** Classes for the trigger. */\n  className?: string\n  /** Classes for the dialog. */\n  popupClassName?: string\n}\n\n/** One searchable command with where it lives in the menu. */\ntype Entry = {\n  item: CommandMenuItem\n  group: string\n  /** Labels of the pages above it, outermost first. */\n  trail: string[]\n  order: number\n}\n\ntype Row = { key: string; entry: Entry; context?: string }\ntype Section = { label: string; items: Row[] }\n\nconst commandMenuVariants = cva(\n  \"fixed top-[15vh] left-1/2 z-50 flex h-[min(26rem,70vh)] w-[calc(100vw-2rem)] max-w-xl -translate-x-1/2 flex-col overflow-hidden border border-border text-popover-foreground shadow-lg outline-hidden transition-[max-width] duration-150 motion-reduce:animate-none motion-reduce:transition-none sm:data-preview:max-w-3xl data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95\",\n  {\n    variants: {\n      variant: {\n        default: \"rounded-xl bg-popover\",\n        // The shell is muted so the list's card reads as a layer above it,\n        // lighter in light mode and darker in dark.\n        inset: \"rounded-2xl bg-muted p-1\",\n      },\n    },\n    defaultVariants: { variant: \"default\" },\n  }\n)\n\nconst SLIDE: Transition = { duration: 0.2, ease: [0.22, 1, 0.36, 1] }\nconst INSTANT: Transition = { duration: 0 }\n\nfunction useControllable<T>(\n  value: T | undefined,\n  defaultValue: T,\n  onChange: ((value: T) => void) | undefined\n) {\n  const [own, setOwn] = React.useState(defaultValue)\n  const current = value ?? own\n  const set = (next: T) => {\n    if (value === undefined) setOwn(next)\n    onChange?.(next)\n  }\n  return [current, set, setOwn] as const\n}\n\nconst noSubscription = () => () => {}\n\n// The platform never changes, so there is nothing to subscribe to. The\n// server snapshot keeps the first render hydration-safe.\nfunction useIsMac() {\n  return React.useSyncExternalStore(\n    noSubscription,\n    () => /mac|iphone|ipad/i.test(navigator.platform || navigator.userAgent),\n    () => false\n  )\n}\n\nfunction readStored(key: string | undefined) {\n  if (!key || typeof window === \"undefined\") return undefined\n  try {\n    const stored = JSON.parse(window.localStorage.getItem(key) ?? \"null\")\n    return Array.isArray(stored) ? (stored as string[]) : undefined\n  } catch {\n    return undefined\n  }\n}\n\nfunction flatten(\n  items: CommandMenuItem[],\n  group: string,\n  trail: string[],\n  out: Entry[]\n) {\n  for (const item of items) {\n    out.push({ item, group, trail, order: out.length })\n    if (item.items) flatten(item.items, group, [...trail, item.label], out)\n  }\n  return out\n}\n\n/*\n * Ranks how well a command answers the query: the whole label, then the\n * start of it, then the start of any word, then anywhere in it, then its\n * keywords. Zero means no match.\n */\nfunction score(item: CommandMenuItem, term: string) {\n  const label = item.label.toLowerCase()\n  if (label === term) return 100\n  if (label.startsWith(term)) return 80\n  if (label.split(/[\\s\\-_/]+/).some((word) => word.startsWith(term))) return 60\n  if (label.includes(term)) return 40\n  const keywords = (item.keywords ?? []).map((k) => k.toLowerCase())\n  if (keywords.some((k) => k.startsWith(term))) return 30\n  if (keywords.some((k) => k.includes(term))) return 20\n  return 0\n}\n\nfunction Highlight({ text, query }: { text: string; query: string }) {\n  const at = query ? text.toLowerCase().indexOf(query.toLowerCase()) : -1\n  if (at < 0) return <>{text}</>\n  return (\n    <>\n      {text.slice(0, at)}\n      <mark className=\"bg-transparent font-semibold text-foreground\">\n        {text.slice(at, at + query.length)}\n      </mark>\n      {text.slice(at + query.length)}\n    </>\n  )\n}\n\n/**\n * A search dialog of grouped commands, opened with Cmd+K or Ctrl+K. Items\n * can open pages of their own, recently run commands come first, a search\n * ranks every command by how well it matches, and a side pane previews the\n * highlighted one.\n */\nfunction CommandMenu({\n  groups,\n  onSelect,\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  recent: recentProp,\n  defaultRecent = [],\n  onRecentChange,\n  maxRecent = 5,\n  storageKey,\n  hotkey = \"k\",\n  trigger,\n  placeholder = \"Type a command or search…\",\n  emptyText = \"No results found\",\n  label = \"Command menu\",\n  labels,\n  hints = true,\n  modal = true,\n  variant = \"default\",\n  container,\n  className,\n  popupClassName,\n}: CommandMenuProps) {\n  const text = { ...DEFAULT_LABELS, ...labels }\n  const [open, setOpen] = useControllable(openProp, defaultOpen, onOpenChange)\n  const [recent, setRecent, restoreRecent] = useControllable(\n    recentProp,\n    defaultRecent,\n    onRecentChange\n  )\n  // Read after mount: the server has no storage, so reading it while\n  // rendering would make the first client render differ from the server's.\n  const restore = React.useEffectEvent(() => {\n    const stored = readStored(storageKey)\n    if (stored) restoreRecent(stored)\n  })\n  React.useEffect(() => restore(), [storageKey])\n  const [query, setQuery] = React.useState(\"\")\n  const [path, setPath] = React.useState<CommandMenuItem[]>([])\n  // Null until the first page change, so opening the menu doesn't also\n  // slide the list in under the dialog's own fade.\n  const [direction, setDirection] = React.useState<1 | -1 | null>(null)\n  const [activeKey, setActiveKey] = React.useState<string | null>(null)\n  const reduceMotion = useReducedMotion()\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const mac = useIsMac()\n\n  const toggle = React.useEffectEvent(() => setOpen(!open))\n\n  React.useEffect(() => {\n    if (!hotkey) return\n    const onKeyDown = (event: KeyboardEvent) => {\n      // Something nearer the key already took it, such as Rich text editor's\n      // Mod+K for a link.\n      if (event.defaultPrevented) return\n      if (\n        event.key.toLowerCase() === hotkey.toLowerCase() &&\n        (event.metaKey || event.ctrlKey) &&\n        !event.altKey &&\n        !event.shiftKey\n      ) {\n        event.preventDefault()\n        toggle()\n      }\n    }\n    document.addEventListener(\"keydown\", onKeyDown)\n    return () => document.removeEventListener(\"keydown\", onKeyDown)\n  }, [hotkey])\n\n  const page = path.at(-1)\n  const term = query.trim().toLowerCase()\n  const pageKey = path.map((p) => p.value).join(\"/\") || \"root\"\n\n  const everything = React.useMemo(\n    () =>\n      groups.reduce<Entry[]>(\n        (out, group) => flatten(group.items, group.label, [], out),\n        []\n      ),\n    [groups]\n  )\n\n  const sections: Section[] = React.useMemo(() => {\n    const trailOf = (entry: Entry) =>\n      entry.trail.length ? entry.trail.join(\" › \") : undefined\n\n    if (term) {\n      // A search from the first page reaches into every page; inside a\n      // page it stays there.\n      const pool = page ? flatten(page.items ?? [], \"\", [], []) : everything\n      const boost = (value: string) => {\n        const at = recent.indexOf(value)\n        return at < 0 ? 0 : maxRecent - at\n      }\n      const rows = pool\n        .map((entry) => ({ entry, rank: score(entry.item, term) }))\n        .filter(({ rank }) => rank > 0)\n        .sort(\n          (a, b) =>\n            b.rank +\n              boost(b.entry.item.value) -\n              (a.rank + boost(a.entry.item.value)) ||\n            a.entry.order - b.entry.order\n        )\n        .map(({ entry }) => ({\n          key: `result:${entry.item.value}`,\n          entry,\n          context: trailOf(entry) ?? (page ? undefined : entry.group),\n        }))\n      return rows.length ? [{ label: text.results, items: rows }] : []\n    }\n\n    if (page)\n      return [\n        {\n          label: page.label,\n          items: (page.items ?? []).map((item, order) => ({\n            key: `page:${item.value}`,\n            entry: { item, group: \"\", trail: [], order },\n          })),\n        },\n      ]\n\n    const byValue = new Map(everything.map((e) => [e.item.value, e]))\n    const recentRows = maxRecent\n      ? recent\n          .slice(0, maxRecent)\n          .map((value) => byValue.get(value))\n          .filter((entry): entry is Entry => Boolean(entry))\n          .map((entry) => ({\n            key: `recent:${entry.item.value}`,\n            entry,\n            context: trailOf(entry),\n          }))\n      : []\n    return [\n      ...(recentRows.length ? [{ label: text.recent, items: recentRows }] : []),\n      ...groups.map((group) => ({\n        label: group.label,\n        items: group.items.map((item) => ({\n          key: `${group.label}:${item.value}`,\n          entry: byValue.get(item.value) ?? {\n            item,\n            group: group.label,\n            trail: [],\n            order: 0,\n          },\n        })),\n      })),\n    ]\n  }, [\n    term,\n    page,\n    everything,\n    groups,\n    recent,\n    maxRecent,\n    text.results,\n    text.recent,\n  ])\n\n  const rows = sections.flatMap((section) => section.items)\n  const active = rows.find((row) => row.key === activeKey) ?? rows[0]\n  const hasPreview = rows.some((row) => row.entry.item.preview !== undefined)\n\n  const enter = (item: CommandMenuItem) => {\n    setDirection(1)\n    setPath((current) => [...current, item])\n    setQuery(\"\")\n    setActiveKey(null)\n  }\n\n  const back = () => {\n    setDirection(-1)\n    setPath((current) => current.slice(0, -1))\n    setQuery(\"\")\n    setActiveKey(null)\n  }\n\n  const run = (item: CommandMenuItem) => {\n    if (item.disabled) return\n    if (item.items) {\n      enter(item)\n      // A non-modal menu never pulls focus: focusing scrolls the page to it.\n      if (modal) inputRef.current?.focus()\n      return\n    }\n    item.onSelect?.()\n    onSelect?.(item)\n    if (maxRecent) {\n      const next = [\n        item.value,\n        ...recent.filter((v) => v !== item.value),\n      ].slice(0, maxRecent)\n      setRecent(next)\n      if (storageKey) {\n        try {\n          window.localStorage.setItem(storageKey, JSON.stringify(next))\n        } catch {\n          // Storage can be full or blocked; recents still work this visit.\n        }\n      }\n    }\n    setOpen(false)\n  }\n\n  const shortcut = hotkey ? (\n    <KbdGroup>\n      <Kbd>{mac ? \"⌘\" : \"Ctrl\"}</Kbd>\n      <Kbd>{hotkey.toUpperCase()}</Kbd>\n    </KbdGroup>\n  ) : null\n\n  const slide = reduceMotion ? 0 : 16\n  const parent = path.at(-2)\n\n  return (\n    <DialogPrimitive.Root\n      open={open}\n      modal={modal}\n      onOpenChange={(next, details) => {\n        // Escape steps back before it closes: first it clears the search,\n        // then it leaves the open page.\n        if (!next && details.reason === \"escape-key\" && (query || page)) {\n          details.cancel()\n          if (query) setQuery(\"\")\n          else back()\n          return\n        }\n        setOpen(next)\n      }}\n      onOpenChangeComplete={(isOpen) => {\n        if (isOpen) return\n        setQuery(\"\")\n        setPath([])\n        setActiveKey(null)\n        setDirection(null)\n      }}\n    >\n      {trigger === null ? null : trigger ? (\n        <DialogPrimitive.Trigger\n          data-slot=\"command-menu-trigger\"\n          render={trigger}\n          className={className}\n        />\n      ) : (\n        <DialogPrimitive.Trigger\n          data-slot=\"command-menu-trigger\"\n          render={<Button variant=\"outline\" />}\n          className={cn(\n            \"w-60 justify-start text-muted-foreground has-data-[slot=kbd-group]:pr-1.5\",\n            className\n          )}\n        >\n          <SearchIcon data-icon=\"inline-start\" aria-hidden=\"true\" />\n          <span className=\"flex-1 text-left\">Search…</span>\n          {shortcut}\n        </DialogPrimitive.Trigger>\n      )}\n      <DialogPrimitive.Portal container={container}>\n        <DialogPrimitive.Backdrop\n          data-slot=\"command-menu-backdrop\"\n          className=\"fixed inset-0 z-50 bg-backdrop duration-150 motion-reduce:animate-none data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0\"\n        />\n        <DialogPrimitive.Popup\n          data-slot=\"command-menu\"\n          data-page={page ? page.value : undefined}\n          data-preview={hasPreview || undefined}\n          data-variant={variant}\n          initialFocus={modal ? inputRef : false}\n          className={cn(commandMenuVariants({ variant }), popupClassName)}\n        >\n          <DialogPrimitive.Title className=\"sr-only\">\n            {label}\n          </DialogPrimitive.Title>\n          <AutocompletePrimitive.Root\n            inline\n            open\n            items={sections}\n            filter={null}\n            value={query}\n            onValueChange={(next, details) => {\n              // Picking an item would copy its label into the search box;\n              // the menu runs it or opens its page instead.\n              if (details.reason === \"item-press\") return\n              setQuery(next)\n              setActiveKey(null)\n            }}\n            onItemHighlighted={(row: Row | undefined) => {\n              if (row) setActiveKey(row.key)\n            }}\n            itemToStringValue={(row: Row) => row.entry.item.label}\n            autoHighlight=\"always\"\n            keepHighlight\n          >\n            <InputGroup\n              variant=\"ghost\"\n              data-slot=\"command-menu-search\"\n              className={cn(\n                \"h-12 shrink-0\",\n                variant === \"default\" && \"border-b border-border\"\n              )}\n            >\n              <InputGroupAddon className=\"has-[>button]:pl-3\">\n                {page ? (\n                  <>\n                    <Button\n                      variant=\"ghost\"\n                      size=\"icon-xs\"\n                      data-slot=\"command-menu-back\"\n                      aria-label={text.back(parent ? parent.label : text.home)}\n                      onClick={() => {\n                        back()\n                        inputRef.current?.focus()\n                      }}\n                      className=\"text-muted-foreground\"\n                    >\n                      <ChevronLeftIcon aria-hidden=\"true\" className=\"size-4\" />\n                    </Button>\n                    <Badge\n                      variant=\"secondary\"\n                      data-slot=\"command-menu-breadcrumb\"\n                      className=\"shrink-0 font-medium\"\n                    >\n                      {page.label}\n                    </Badge>\n                  </>\n                ) : (\n                  <SearchIcon aria-hidden=\"true\" className=\"mx-1\" />\n                )}\n              </InputGroupAddon>\n              <InputGroupInput\n                render={<AutocompletePrimitive.Input />}\n                ref={inputRef}\n                aria-label={page ? `${label}, ${page.label}` : label}\n                placeholder={page?.placeholder ?? placeholder}\n                spellCheck={false}\n                onKeyDown={(event) => {\n                  if (event.key === \"Backspace\" && !query && page) {\n                    event.preventDefault()\n                    back()\n                  }\n                }}\n                className=\"text-sm\"\n              />\n            </InputGroup>\n            <div\n              data-slot=\"command-menu-body\"\n              className={cn(\n                \"flex min-h-0 flex-1\",\n                variant === \"inset\" &&\n                  \"overflow-hidden rounded-xl border border-border bg-popover shadow-xs\"\n              )}\n            >\n              <motion.div\n                key={pageKey}\n                initial={\n                  direction === null\n                    ? false\n                    : { opacity: 0, x: direction * slide }\n                }\n                animate={{ opacity: 1, x: 0 }}\n                transition={reduceMotion ? INSTANT : SLIDE}\n                className=\"flex min-h-0 min-w-0 flex-1 flex-col\"\n              >\n                <AutocompletePrimitive.Empty\n                  data-slot=\"command-menu-empty\"\n                  className=\"empty:hidden\"\n                >\n                  <EmptyState size=\"sm\" className=\"py-10\">\n                    <EmptyStateTitle>{emptyText}</EmptyStateTitle>\n                  </EmptyState>\n                </AutocompletePrimitive.Empty>\n                <AutocompletePrimitive.List\n                  data-slot=\"command-menu-list\"\n                  className=\"min-h-0 flex-1 scroll-py-2 overflow-y-auto p-2 outline-hidden empty:hidden\"\n                >\n                  {(section: Section) => (\n                    <AutocompletePrimitive.Group\n                      key={section.label}\n                      items={section.items}\n                      data-slot=\"command-menu-group\"\n                      className=\"not-first:mt-2\"\n                    >\n                      <AutocompletePrimitive.GroupLabel\n                        data-slot=\"command-menu-group-label\"\n                        className=\"px-2 pt-1 pb-1.5 text-xs text-muted-foreground\"\n                      >\n                        {section.label}\n                      </AutocompletePrimitive.GroupLabel>\n                      <AutocompletePrimitive.Collection>\n                        {(row: Row) => (\n                          <CommandMenuRow\n                            key={row.key}\n                            row={row}\n                            query={term}\n                            onRun={() => run(row.entry.item)}\n                          />\n                        )}\n                      </AutocompletePrimitive.Collection>\n                    </AutocompletePrimitive.Group>\n                  )}\n                </AutocompletePrimitive.List>\n              </motion.div>\n              {hasPreview ? (\n                <div\n                  data-slot=\"command-menu-preview\"\n                  aria-live=\"polite\"\n                  className=\"hidden w-72 shrink-0 overflow-y-auto border-l border-border p-4 text-sm sm:block\"\n                >\n                  {active?.entry.item.preview ?? (\n                    <p className=\"flex h-full items-center justify-center text-muted-foreground\">\n                      {text.noPreview}\n                    </p>\n                  )}\n                </div>\n              ) : null}\n            </div>\n          </AutocompletePrimitive.Root>\n          <span className=\"sr-only\" aria-live=\"polite\">\n            {term ? text.count(rows.length) : \"\"}\n          </span>\n          {hints ? (\n            <div\n              data-slot=\"command-menu-hints\"\n              aria-hidden=\"true\"\n              className={cn(\n                \"flex h-10 shrink-0 items-center gap-4 text-xs text-muted-foreground\",\n                variant === \"default\" ? \"border-t border-border px-4\" : \"px-3\"\n              )}\n            >\n              <span className=\"flex items-center gap-1.5\">\n                <Kbd>↵</Kbd>\n                {active?.entry.item.items ? \"Open\" : \"Run\"}\n              </span>\n              <span className=\"flex items-center gap-1.5\">\n                <KbdGroup>\n                  <Kbd>↑</Kbd>\n                  <Kbd>↓</Kbd>\n                </KbdGroup>\n                Move\n              </span>\n              <span className=\"flex items-center gap-1.5\">\n                <Kbd>Esc</Kbd>\n                {query || page ? \"Back\" : \"Close\"}\n              </span>\n            </div>\n          ) : null}\n        </DialogPrimitive.Popup>\n      </DialogPrimitive.Portal>\n    </DialogPrimitive.Root>\n  )\n}\n\n// The list moves its highlight into view with scrollIntoView, which also\n// scrolls the page to reach a menu that is partly off screen: a menu in a\n// card pulled the page to itself as its highlight moved. Each row scrolls\n// only the list instead.\nfunction scrollWithinList(node: HTMLElement | null) {\n  if (!node) return\n  node.scrollIntoView = () => {\n    const list = node.closest<HTMLElement>(\"[data-slot=command-menu-list]\")\n    if (!list) return\n    const bounds = list.getBoundingClientRect()\n    const target = node.getBoundingClientRect()\n    // A menu drawn inside a scaled element measures scaled on screen.\n    const scale = bounds.height / list.offsetHeight || 1\n    if (target.top < bounds.top) {\n      list.scrollTop -= (bounds.top - target.top) / scale\n    } else if (target.bottom > bounds.bottom) {\n      list.scrollTop += (target.bottom - bounds.bottom) / scale\n    }\n  }\n}\n\nfunction CommandMenuRow({\n  row,\n  query,\n  onRun,\n}: {\n  row: Row\n  query: string\n  onRun: () => void\n}) {\n  const { item } = row.entry\n  return (\n    <AutocompletePrimitive.Item\n      ref={scrollWithinList}\n      value={row}\n      disabled={item.disabled}\n      onClick={onRun}\n      data-slot=\"command-menu-item\"\n      className=\"flex h-9 cursor-default items-center gap-2 rounded-md px-2 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n    >\n      {item.icon ? (\n        <span aria-hidden=\"true\" className=\"flex text-muted-foreground\">\n          {item.icon}\n        </span>\n      ) : null}\n      <span className=\"min-w-0 flex-1 truncate\">\n        <Highlight text={item.label} query={query} />\n      </span>\n      {row.context ? (\n        // Muted text on the highlighted row's accent fill measures 4.35:1 in\n        // light mode, under AA, so it takes the row's foreground there.\n        <span\n          data-slot=\"command-menu-context\"\n          className=\"max-w-40 truncate text-xs text-muted-foreground in-data-highlighted:text-accent-foreground\"\n        >\n          {row.context}\n        </span>\n      ) : null}\n      {item.shortcut ? (\n        <KbdGroup data-slot=\"command-menu-shortcut\">\n          {item.shortcut.map((key) => (\n            <Kbd key={key}>{key}</Kbd>\n          ))}\n        </KbdGroup>\n      ) : null}\n      {item.items ? (\n        <>\n          <span className=\"sr-only\">, opens a page</span>\n          <ChevronRightIcon\n            aria-hidden=\"true\"\n            className=\"text-muted-foreground\"\n          />\n        </>\n      ) : null}\n    </AutocompletePrimitive.Item>\n  )\n}\n\nexport { CommandMenu, commandMenuVariants }\nexport type {\n  CommandMenuGroup,\n  CommandMenuItem,\n  CommandMenuLabels,\n  CommandMenuProps,\n}\n",
      "type": "registry:ui"
    }
  ],
  "cssVars": {
    "theme": {
      "color-backdrop": "var(--backdrop)"
    },
    "light": {
      "backdrop": "color-mix(in oklch, var(--color-neutral-950) 20%, transparent)"
    },
    "dark": {
      "backdrop": "color-mix(in oklch, var(--color-black) 60%, transparent)"
    }
  },
  "meta": {
    "tier": "special-components",
    "group": "Navigation",
    "docs": "https://fibo.toribryan.com/?path=/docs/special-components-command-menu--docs"
  },
  "docs": "Docs and live examples: https://fibo.toribryan.com/?path=/docs/special-components-command-menu--docs",
  "categories": [
    "special-components",
    "navigation"
  ],
  "type": "registry:ui"
}