{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"command-menu","type":"registry:ui","title":"CommandMenu","description":"A modal search palette over a flat vocabulary of items — grouped results, diacritic-insensitive filtering, match highlighting, and full keyboard navigation with focus that never leaves the search field.","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["navigation"],"registryDependencies":["https://fancy-ui.rama.app/r/internals.json","https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/internals-motion.json","https://fancy-ui.rama.app/r/sound.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/command-menu/CommandMenu.tsx","content":"\"use client\";\n\nimport {\n\tFragment,\n\tforwardRef,\n\tuseCallback,\n\tuseEffect,\n\tuseInsertionEffect,\n\tuseRef,\n\tuseState,\n\tuseSyncExternalStore,\n} from \"react\";\nimport type { ChangeEvent, KeyboardEvent, ReactNode } from \"react\";\n\nimport { cn } from \"../../utils\";\nimport { Portal } from \"../../internals/Portal\";\nimport { useDismissable } from \"../../internals/dismissable\";\nimport { useFocusTrap } from \"../../internals/focus-trap\";\nimport { useScrollLock } from \"../../internals/scroll-lock\";\nimport { useConstant } from \"../../internals/dom/ssr\";\nimport { useComposedRefs } from \"../../internals/dom/use-composed-refs\";\nimport { useElementRef } from \"../../internals/dom/use-element-ref\";\nimport { useEventCallback } from \"../../internals/dom/use-event-callback\";\nimport { useLiveRef } from \"../../internals/dom/use-live-ref\";\nimport { anchored } from \"../../internals/motion/anchored\";\nimport { DURATIONS } from \"../../internals/motion/tokens\";\nimport { usePresence } from \"../../internals/motion/presence\";\nimport { createListbox } from \"../../internals/listbox\";\nimport { useFancyId } from \"../../internals/use-id\";\nimport { useSoundCue } from \"../../sound/use-sound\";\nimport { defaultFilter, getMatchRange } from \"./match\";\nimport type { CommandItem } from \"./types\";\nimport \"./command-menu.css\";\n\nexport type { CommandItem };\n\nexport interface CommandMenuProps {\n\t/** Whether the menu is open. Controlled when given; the component keeps its own copy either way. */\n\topen?: boolean;\n\t/** Fires whenever `open` changes, from Escape, an outside click, or committing an item. */\n\tonOpenChange?: (open: boolean) => void;\n\t/** The full, unfiltered vocabulary. */\n\titems: CommandItem[];\n\t/** The current search text. Controlled when given. Reset to `\"\"` every time the menu reopens. */\n\tquery?: string;\n\t/** Fires whenever `query` changes. */\n\tonQueryChange?: (query: string) => void;\n\t/** Called with the committed item — Enter on the active row, or a click. Fires after the item's own `onSelect`. */\n\tonSelect?: (item: CommandItem) => void;\n\t/** Placeholder for the search field. */\n\tplaceholder?: string;\n\t/** Shown in place of the list when nothing matches. */\n\temptyMessage?: string;\n\t/** Accessible name for the dialog (and its search field — see the README). */\n\tlabel?: string;\n\t/** Matches an item against the current query. Default: case- and diacritic-insensitive substring match on `label` plus `keywords`. */\n\tfilter?: (item: CommandItem, query: string) => boolean;\n\t/**\n\t * The panel's `id`. A trigger the caller renders outside the component\n\t * points its `aria-controls` at it while the panel is open.\n\t */\n\tid?: string;\n\t/** Additional CSS classes for the panel. */\n\tclassName?: string;\n\t/** Rendered before each row's label, given that row's item. Treated as decorative — see the README. */\n\ticon?: (item: CommandItem) => ReactNode;\n\t/** Rendered in place of the list when nothing matches, instead of `emptyMessage`. */\n\tempty?: ReactNode;\n\t/**\n\t * Plays the matching interface cue through the sound controller. Off by\n\t * default; only audible once the user has enabled sound.\n\t */\n\tsound?: boolean;\n}\n\n// How long the query has to sit still before the live region reports a\n// fresh count — announcing on every keystroke would turn fast typing\n// into a screen reader narrating a number after every character. See\n// the README for the exact contract this settles on.\nconst ANNOUNCE_DEBOUNCE_MS = 300;\n\n/**\n * Reorders the already-filtered items into the order they actually\n * render in: every ungrouped item first, then each group's items, in\n * the order that group name was first seen in `items` — never\n * alphabetized, never reordered relative to how the caller listed\n * things within a bucket. This is a *display* order, not `items`'s own\n * order, and it matters beyond rendering: it is also the flat index\n * space `optionId`, `aria-activedescendant`, and the listbox core all\n * share below, so \"first\" (on open, on Home) and \"next\" (ArrowDown)\n * mean the row that is visually first/next, not `items[0]`/`items[i+1]`\n * — those two can disagree the moment an ungrouped item sits after a\n * grouped one in `items`' own declaration order.\n */\nfunction computeDisplayItems(filtered: CommandItem[]): CommandItem[] {\n\tconst ungrouped: CommandItem[] = [];\n\tconst order: string[] = [];\n\tconst byName = new Map<string, CommandItem[]>();\n\tfor (const item of filtered) {\n\t\tif (!item.group) {\n\t\t\tungrouped.push(item);\n\t\t\tcontinue;\n\t\t}\n\t\tlet bucket = byName.get(item.group);\n\t\tif (!bucket) {\n\t\t\tbucket = [];\n\t\t\tbyName.set(item.group, bucket);\n\t\t\torder.push(item.group);\n\t\t}\n\t\tbucket.push(item);\n\t}\n\treturn [...ungrouped, ...order.flatMap((name) => byName.get(name)!)];\n}\n\ninterface GroupRow {\n\titem: CommandItem;\n\tindex: number;\n}\ninterface ItemGroup {\n\tname: string | null;\n\trows: GroupRow[];\n}\n\n/**\n * `displayItems` is already ungrouped-first-then-grouped, so a single\n * left-to-right pass finds each heading's boundary just by watching\n * `item.group` change — no separate bucketing pass needed here, and\n * each row's `index` is simply its position in `displayItems`, the\n * same index space `optionId`/the listbox core use. A group only\n * exists here if at least one of its items survived filtering, so an\n * empty heading never renders.\n */\nfunction computeGroups(display: CommandItem[]): ItemGroup[] {\n\tconst groups: ItemGroup[] = [];\n\tlet current: ItemGroup | null = null;\n\tdisplay.forEach((item, index) => {\n\t\tconst name = item.group ?? null;\n\t\tif (!current || current.name !== name) {\n\t\t\tcurrent = { name, rows: [] };\n\t\t\tgroups.push(current);\n\t\t}\n\t\tcurrent.rows.push({ item, index });\n\t});\n\treturn groups;\n}\n\n// -1 is the value the listbox core starts at, so the server render and the\n// hydration render agree that nothing is active.\nconst getServerSnapshot = () => -1;\n\nexport const CommandMenu = forwardRef<HTMLDivElement, CommandMenuProps>(function CommandMenu(\n\t{\n\t\topen: openProp,\n\t\tonOpenChange,\n\t\titems,\n\t\tquery: queryProp,\n\t\tonQueryChange,\n\t\tonSelect,\n\t\tplaceholder = \"Search...\",\n\t\temptyMessage = \"No results\",\n\t\tlabel = \"Command menu\",\n\t\tfilter,\n\t\tid,\n\t\tclassName,\n\t\ticon,\n\t\tempty,\n\t\tsound = false,\n\t},\n\tforwardedRef\n) {\n\t// The React shape of the source's `open = $bindable(false)` /\n\t// `query = $bindable(\"\")`: an internal copy seeded from the prop,\n\t// re-synced during render whenever the CALLER changes the prop, and free\n\t// to move on its own in between. That is what makes every documented call\n\t// shape work off one implementation — a caller driving the value from its\n\t// own state, a caller who passes only the change callback, and a caller\n\t// who passes neither.\n\t//\n\t// Re-synced in the render path, not an effect: an effect would paint one\n\t// frame of the stale value first, and the pattern React documents for\n\t// \"adjust state when a prop changes\" is exactly this.\n\tconst [open, setOpenState] = useState(openProp ?? false);\n\tconst [lastOpenProp, setLastOpenProp] = useState(openProp);\n\tif (lastOpenProp !== openProp) {\n\t\tsetLastOpenProp(openProp);\n\t\tsetOpenState(openProp ?? false);\n\t}\n\n\tconst [query, setQueryState] = useState(queryProp ?? \"\");\n\tconst [lastQueryProp, setLastQueryProp] = useState(queryProp);\n\tif (lastQueryProp !== queryProp) {\n\t\tsetLastQueryProp(queryProp);\n\t\tsetQueryState(queryProp ?? \"\");\n\t}\n\n\t// `useFancyId()`, not `uid()` (which throws outside the browser): row and\n\t// list ids are needed at SSR render time, and this is the seed every other\n\t// control in the library uses for that (convention C-6).\n\tconst uid = useFancyId();\n\tconst listId = `${uid}-list`;\n\tfunction optionId(index: number): string {\n\t\treturn `${uid}-option-${index}`;\n\t}\n\n\t// Convention C-1: the NODE, not a ref. Both elements are created by\n\t// `presence.mounted`, so a `useRef` + `[]`-deps effect would still be\n\t// holding `null` when the consuming hook's effect fires.\n\tconst [panel, setPanelNode] = useElementRef<HTMLDivElement>();\n\tconst [listNode, setListNode] = useElementRef<HTMLDivElement>();\n\n\tfunction matches(item: CommandItem, q: string): boolean {\n\t\treturn filter ? filter(item, q) : defaultFilter(item, q);\n\t}\n\n\tfunction computeFilteredItems(q: string): CommandItem[] {\n\t\treturn items.filter((item) => matches(item, q));\n\t}\n\n\tconst filteredItems = computeFilteredItems(query);\n\tconst displayItems = computeDisplayItems(filteredItems);\n\tconst groups = computeGroups(displayItems);\n\n\t// The React stand-in for the source's plain `computeFilteredItems(query)`\n\t// call inside `handleInput`. `query` is state here, so the re-render that\n\t// carries a keystroke has not happened yet when `handleInput` goes on to\n\t// call `listbox.moveToEdge(\"first\")` in the same synchronous pass — the\n\t// listbox's `count`/`enabled` getters would still be describing the\n\t// PRE-keystroke list, which is exactly the stale read the source's comment\n\t// warns against. `setQuery` writes this ref synchronously, so the getters\n\t// below always see the query that is being handled right now.\n\t//\n\t// Written in an insertion effect for the committed value (never during\n\t// render, per `useLiveRef`'s own reasoning): a concurrent render React\n\t// throws away must not be able to publish a query that never committed.\n\tconst queryRef = useRef(query);\n\tuseInsertionEffect(() => {\n\t\tqueryRef.current = query;\n\t}, [query]);\n\tconst itemsRef = useLiveRef(items);\n\tconst filterRef = useLiveRef(filter);\n\n\tconst handleActiveChange = useEventCallback((index: number) => {\n\t\t// `Element.prototype.scrollIntoView` does not exist in jsdom —\n\t\t// optional-chaining the method itself (not just the element) makes\n\t\t// this a silent no-op there instead of a thrown TypeError, while a\n\t\t// real browser still scrolls the row into view exactly as intended.\n\t\tconst row = listNode?.querySelector<HTMLElement>(`#${CSS.escape(optionId(index))}`);\n\t\trow?.scrollIntoView?.({ block: \"nearest\" });\n\t});\n\n\t// The listbox core is wired by hand rather than through `useListbox`, and\n\t// the reason is the one above: `useListbox` takes `count` as a plain\n\t// number read from the last COMMITTED render, which is one keystroke\n\t// behind inside `handleInput`. The factory's own getter shape is what the\n\t// source passes, so passing it live getters over refs is the literal port.\n\tconst listbox = useConstant(() => {\n\t\tconst live = (): CommandItem[] => {\n\t\t\tconst q = queryRef.current;\n\t\t\tconst f = filterRef.current;\n\t\t\treturn computeDisplayItems(\n\t\t\t\titemsRef.current.filter((item) => (f ? f(item, q) : defaultFilter(item, q)))\n\t\t\t);\n\t\t};\n\t\treturn createListbox({\n\t\t\tcount: () => live().length,\n\t\t\tenabled: (index) => !live()[index]?.disabled,\n\t\t\tloop: true,\n\t\t\tonActiveChange: handleActiveChange,\n\t\t});\n\t});\n\n\t// The source's `onDestroy(() => listbox.destroy())`.\n\tuseEffect(() => () => listbox.destroy(), [listbox]);\n\n\t// The one index in this component that has to drive rendering:\n\t// `aria-activedescendant` on the input, `aria-selected` on every row.\n\tconst getSnapshot = useCallback(() => listbox.activeIndex, [listbox]);\n\tconst activeIndex = useSyncExternalStore(listbox.subscribe, getSnapshot, getServerSnapshot);\n\n\t// Returns the two functions the source hands out through `onActivate`:\n\t// the eager return, and the re-arm.\n\tconst trap = useFocusTrap(panel);\n\n\tconst presence = usePresence(open, {\n\t\t// The two halves of the focus handshake, at the two moments the source\n\t\t// puts them: `onintrostart` → rearm, `onoutrostart` → returnFocusNow.\n\t\t//\n\t\t// `returnFocusNow` at the dismiss instant is the whole point: waiting\n\t\t// for the trap's own `destroy()` would leave a keyboard user on\n\t\t// `<body>` for the whole length of the fade, because the panel is\n\t\t// marked `inert` the instant the exit starts.\n\t\t//\n\t\t// `rearm` is the other half. A menu reopened DURING its fade reverses\n\t\t// the exit instead of remounting, so the trap is never re-created:\n\t\t// without this the panel would come back `aria-modal` and interactive\n\t\t// with focus left on whatever was focused before it opened, Tab\n\t\t// walking the page rather than the panel, and the eager return already\n\t\t// spent for the life of the instance.\n\t\tonEnterStart: () => trap.rearm(),\n\t\tonExitStart: () => trap.returnFocusNow(),\n\t});\n\n\t// Release timing, which is the only reason this is not keyed on `open`:\n\t// `presence.mounted` stays true for the whole exit, so the page stays\n\t// locked until the backdrop has actually finished fading instead of\n\t// unlocking the instant `open` flips and leaving the page scrollable under\n\t// a scrim still on screen — the source's reason for making the lock an\n\t// action (whose `destroy()` is outro-delayed) rather than an `$effect`.\n\t//\n\t// Not a bare `useScrollLock()`: this component is mounted for as long as a\n\t// `CommandMenu` is anywhere in the tree, open or closed, so a bare call\n\t// would lock the page forever. Emphatically not `useScrollLock(open)`\n\t// either — that releases at exit START.\n\tuseScrollLock(presence.mounted);\n\n\t// `active: open` — a plain boolean where the source needed `() => open`.\n\t// The layer stays ON the stack for its whole exit and stops being TOP of\n\t// it the instant `open` flips, so a second Escape during the fade falls\n\t// through to whatever is underneath.\n\tuseDismissable(panel, { onDismiss: () => setOpen(false), active: open, keepFocus: true });\n\n\t// Convention C-2: composed ABOVE any conditional. Calling this inside the\n\t// JSX branch below would be a conditional hook and would throw the first\n\t// time `mounted` flips.\n\t//\n\t// ONE bidirectional transition per surface, never a split in/out pair: a\n\t// reopen mid-exit hands the in-flight run over as the counterpart, so the\n\t// entrance resumes from where it is instead of snapping to invisible\n\t// first. `entering` is the direction signal the transition cannot work out\n\t// on its own. This is the MODAL rung (`base` in, `exit` out), not the\n\t// anchored rung the dropdown and context menus are on: a centred,\n\t// scrim-backed, focus-trapped surface belongs with Dialog, not with a menu\n\t// hanging off a button.\n\tconst panelRef = useComposedRefs(\n\t\tsetPanelNode,\n\t\tforwardedRef,\n\t\tpresence.register(\"panel\", anchored, (entering) => ({\n\t\t\tentering,\n\t\t\tduration: DURATIONS.base,\n\t\t\texitDuration: DURATIONS.exit,\n\t\t}))\n\t);\n\n\t// The scrim fades on opacity alone (`scale: false`) — a full-viewport\n\t// fixed element has no business acquiring a compositing layer for a\n\t// transform it does not use. It shares the panel's clock exactly, so the\n\t// two leave together and the \"destroy the subtree when the LAST transition\n\t// finishes\" rule is a tie rather than a straggler.\n\tconst scrimRef = presence.register(\"scrim\", anchored, (entering) => ({\n\t\tentering,\n\t\tscale: false,\n\t\tduration: DURATIONS.base,\n\t\texitDuration: DURATIONS.exit,\n\t}));\n\n\tconst emitQueryChange = useEventCallback(onQueryChange);\n\n\t// Identity-stable, and a no-op while `sound` is falsy — the enabled check\n\t// lives inside the hook, so the call sites below read as the source's do.\n\tconst playCue = useSoundCue(sound);\n\n\t// Whatever the very first paint's `query` was is worth keeping — a caller\n\t// that mounts the menu already open with a prefilled query is seeding it\n\t// on purpose. Only actual *reopenings* (closed, now open again) are what a\n\t// stale query is about, so this flips true the first time this instance\n\t// ever sees `open === true` and stays true forever after, exempting\n\t// exactly that one first transition.\n\tconst hasOpenedBefore = useRef(false);\n\n\t// The `open` value this effect last acted on, `null` before it has ever\n\t// run. `hasOpenedBefore` alone is a latch that survives a StrictMode\n\t// remount, so a dev-mode double invoke would run the body twice for ONE\n\t// open and the second pass would take the reopen branch — wiping a query\n\t// the caller seeded on the very first mount and pushing `onQueryChange(\"\")`\n\t// back at it. Keying the work on the open EDGE instead makes a repeated\n\t// invoke at an unchanged `open` a no-op, which is what makes the dev-only\n\t// remount indistinguishable from a single mount. Deliberately not a\n\t// cleanup that rewinds `hasOpenedBefore`: that would fire on every real\n\t// close and turn every reopen back into a first mount.\n\tconst lastHandledOpen = useRef<boolean | null>(null);\n\n\t// The React counterpart of the source's `untrack` block: this effect\n\t// re-fires on `open` alone — not on a keystroke, not on an `items` swap.\n\t// Every other value it touches is reached through an identity-stable ref\n\t// or handle, so the dependency array below is both honest and minimal.\n\tuseEffect(() => {\n\t\tif (lastHandledOpen.current === open) return;\n\t\tlastHandledOpen.current = open;\n\t\tif (!open) return;\n\t\tif (hasOpenedBefore.current) {\n\t\t\tqueryRef.current = \"\";\n\t\t\tsetQueryState(\"\");\n\t\t\temitQueryChange(\"\");\n\t\t}\n\t\thasOpenedBefore.current = true;\n\t\tlistbox.setActive(-1);\n\t\tlistbox.moveToEdge(\"first\");\n\t}, [open, listbox, emitQueryChange]);\n\n\t// Debounced result-count announcement. Re-running this effect whenever the\n\t// result count (or `open`) changes clears whatever timer the previous run\n\t// scheduled — React runs the cleanup before the next run's body, exactly\n\t// once per change — so a burst of keystrokes keeps pushing the\n\t// announcement out rather than stacking up several, and the count that\n\t// finally lands is always the latest one. `null`, not `0`, before anything\n\t// has settled — the live region must stay silent while a debounce is\n\t// pending, not flash a misleading \"0 results\" for the first\n\t// `ANNOUNCE_DEBOUNCE_MS` even when the list is not actually empty.\n\tconst [announcedCount, setAnnouncedCount] = useState<number | null>(null);\n\tconst resultCount = filteredItems.length;\n\tuseEffect(() => {\n\t\tif (!open) return;\n\t\t// Zero is announced immediately, not debounced like every other\n\t\t// count. The empty state is already visible on screen the instant\n\t\t// the count hits zero (the list already shows `emptyMessage`) —\n\t\t// waiting out the debounce here would leave the live region reporting\n\t\t// a stale \"N results\" while the list a screen reader user cannot see\n\t\t// already reads empty, which actively misleads rather than merely\n\t\t// lagging. A nonzero count carries no equivalent urgency: it is never\n\t\t// a contradiction of what's on screen, only a preview of it, so it\n\t\t// keeps the debounce that avoids narrating every keystroke.\n\t\tif (resultCount === 0) {\n\t\t\tsetAnnouncedCount(0);\n\t\t\treturn;\n\t\t}\n\t\tconst timer = setTimeout(() => {\n\t\t\tsetAnnouncedCount(resultCount);\n\t\t}, ANNOUNCE_DEBOUNCE_MS);\n\t\treturn () => clearTimeout(timer);\n\t}, [resultCount, open]);\n\n\tconst resultsMessage =\n\t\tannouncedCount === null ? \"\" : announcedCount === 1 ? \"1 result\" : `${announcedCount} results`;\n\n\tfunction setOpen(next: boolean, options: { silent?: boolean } = {}): void {\n\t\tif (open === next) return;\n\t\tsetOpenState(next);\n\t\tonOpenChange?.(next);\n\t\t// No `open` cue here on purpose — this menu is opened programmatically\n\t\t// by the consumer (⌘K and the like), never by an interaction this\n\t\t// component itself handles, so there is no gesture here to attach one\n\t\t// to. Only a dismissal (Escape, an outside click) plays `close`; a\n\t\t// commit-driven close passes `{ silent: true }` from `commitItem` below\n\t\t// so a committed row's own `select` cue is the only one that plays —\n\t\t// the same commit/dismiss split `Select`'s panel close uses.\n\t\tif (!next && !options.silent) playCue(\"close\");\n\t}\n\n\tfunction commitItem(item: CommandItem): void {\n\t\tif (item.disabled) return;\n\t\tplayCue(\"select\");\n\t\titem.onSelect?.();\n\t\tonSelect?.(item);\n\t\tsetOpen(false, { silent: true });\n\t}\n\n\tfunction setQuery(next: string): void {\n\t\tif (query === next) return;\n\t\tqueryRef.current = next;\n\t\tsetQueryState(next);\n\t\tonQueryChange?.(next);\n\t}\n\n\tfunction handleInput(event: ChangeEvent<HTMLInputElement>): void {\n\t\tsetQuery(event.currentTarget.value);\n\t\t// Re-activates the first (visually topmost) surviving match on every\n\t\t// keystroke — the listbox's getters read `queryRef`, which the line\n\t\t// above just wrote, not the state this render was built from.\n\t\tlistbox.moveToEdge(\"first\");\n\t}\n\n\tfunction handleKeydown(event: KeyboardEvent<HTMLInputElement>): void {\n\t\tswitch (event.key) {\n\t\t\tcase \"ArrowDown\":\n\t\t\t\tevent.preventDefault();\n\t\t\t\tlistbox.move(1);\n\t\t\t\tbreak;\n\t\t\tcase \"ArrowUp\":\n\t\t\t\tevent.preventDefault();\n\t\t\t\tlistbox.move(-1);\n\t\t\t\tbreak;\n\t\t\tcase \"Home\":\n\t\t\t\tevent.preventDefault();\n\t\t\t\tlistbox.moveToEdge(\"first\");\n\t\t\t\tbreak;\n\t\t\tcase \"End\":\n\t\t\t\tevent.preventDefault();\n\t\t\t\tlistbox.moveToEdge(\"last\");\n\t\t\t\tbreak;\n\t\t\tcase \"Enter\": {\n\t\t\t\t// Reads `displayItems`, not `filteredItems`:\n\t\t\t\t// `listbox.activeIndex` is an index into the display order\n\t\t\t\t// (see `computeDisplayItems`), not into `items`' own\n\t\t\t\t// filtered-but-unreordered order.\n\t\t\t\tconst active = displayItems[listbox.activeIndex];\n\t\t\t\tif (active) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tcommitItem(active);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t// No Escape case here on purpose — the panel's own dismiss layer\n\t\t\t// already closes on Escape via its document-level listener; a\n\t\t\t// second listener here would duplicate it.\n\t\t}\n\t}\n\n\tconst classes = cn(\n\t\t\"ft-command-menu border-border bg-popover text-popover-foreground fixed top-[12vh] left-1/2 z-50 flex max-h-[70vh] w-[calc(100%-2rem)] max-w-xl -translate-x-1/2 flex-col overflow-hidden rounded-xl border shadow-2xl\",\n\t\t\"focus-visible:outline-none\",\n\t\tclassName\n\t);\n\n\t// The `Portal` stays mounted and its CHILDREN are what `presence.mounted`\n\t// gates. `usePortalTarget` resolves its container in a layout effect, so a\n\t// `Portal` that mounts in the same commit as the surface renders nothing\n\t// on that pass — the registered nodes would not exist yet when\n\t// `usePresence`'s own layout effect looks for legs to start, and the\n\t// entrance would be silently skipped. Hoisting the `Portal` above the gate\n\t// resolves the container once, at this component's own mount.\n\t//\n\t// No portal-before-focus-trap ordering ceremony either: `createPortal`\n\t// commits children into the container before any effect runs and refs\n\t// populate before layout effects, so the node the trap focuses is always\n\t// connected. The silent-no-op `.focus()` hazard the source's comment block\n\t// warns about cannot recur here.\n\treturn (\n\t\t<Portal>\n\t\t\t{presence.mounted ? (\n\t\t\t\t<>\n\t\t\t\t\t<div\n\t\t\t\t\t\tref={scrimRef}\n\t\t\t\t\t\tclassName=\"ft-command-menu-scrim fixed inset-0 z-50 bg-black/60\"\n\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t/>\n\t\t\t\t\t{/*\n\t\t\t\t\t\t`data-state` is an ordinary React attribute carrying\n\t\t\t\t\t\t`surfaceState`'s TWO values — never `\"opening\"`\n\t\t\t\t\t\t(convention C-5). `inert` is not written by hand either:\n\t\t\t\t\t\t`usePresence` sets it on every registered node for the whole\n\t\t\t\t\t\texit, which is exactly what a closing modal wants — and it is\n\t\t\t\t\t\twhat keeps the live region below from announcing a stale count\n\t\t\t\t\t\twhile the panel fades.\n\t\t\t\t\t*/}\n\t\t\t\t\t<div\n\t\t\t\t\t\tref={panelRef}\n\t\t\t\t\t\tid={id}\n\t\t\t\t\t\trole=\"dialog\"\n\t\t\t\t\t\taria-modal=\"true\"\n\t\t\t\t\t\taria-label={label}\n\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\tclassName={classes}\n\t\t\t\t\t\tdata-state={presence.surfaceState}\n\t\t\t\t\t>\n\t\t\t\t\t\t<div className=\"border-border flex h-[42px] shrink-0 items-center gap-[10px] border-b px-[14px]\">\n\t\t\t\t\t\t\t<span aria-hidden=\"true\" className=\"text-muted-foreground text-[13px]\">\n\t\t\t\t\t\t\t\t⌕\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\trole=\"combobox\"\n\t\t\t\t\t\t\t\taria-label={label}\n\t\t\t\t\t\t\t\taria-haspopup=\"listbox\"\n\t\t\t\t\t\t\t\taria-expanded=\"true\"\n\t\t\t\t\t\t\t\taria-controls={listId}\n\t\t\t\t\t\t\t\taria-autocomplete=\"list\"\n\t\t\t\t\t\t\t\taria-activedescendant={activeIndex >= 0 ? optionId(activeIndex) : undefined}\n\t\t\t\t\t\t\t\tplaceholder={placeholder}\n\t\t\t\t\t\t\t\tvalue={query}\n\t\t\t\t\t\t\t\tclassName=\"text-foreground placeholder:text-muted-foreground min-w-0 flex-1 bg-transparent text-[13px] outline-none\"\n\t\t\t\t\t\t\t\tonChange={handleInput}\n\t\t\t\t\t\t\t\tonKeyDown={handleKeydown}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<kbd\n\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t\tclassName=\"text-muted-foreground border-border bg-accent ml-auto shrink-0 rounded-[4px] border px-[6px] py-[2px] font-mono text-[10px]\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tesc\n\t\t\t\t\t\t\t</kbd>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tref={setListNode}\n\t\t\t\t\t\t\tid={listId}\n\t\t\t\t\t\t\trole=\"listbox\"\n\t\t\t\t\t\t\taria-label={label}\n\t\t\t\t\t\t\tclassName=\"flex min-h-0 flex-col gap-[2px] overflow-y-auto p-[8px]\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{filteredItems.length === 0\n\t\t\t\t\t\t\t\t? (empty ?? (\n\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\trole=\"presentation\"\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"ft-command-menu-empty text-muted-foreground rounded-[8px] px-[12px] py-[8px] text-[13px]\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{emptyMessage}\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t))\n\t\t\t\t\t\t\t\t: groups.map((group) => (\n\t\t\t\t\t\t\t\t\t\t<Fragment key={group.name ?? \"__ft_ungrouped__\"}>\n\t\t\t\t\t\t\t\t\t\t\t{group.name ? (\n\t\t\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\t\t\trole=\"presentation\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"ft-command-menu-heading text-muted-foreground/60 px-[12px] py-[4px] text-[10px] font-medium tracking-[.08em] uppercase\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{group.name}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t{group.rows.map(({ item, index }) => {\n\t\t\t\t\t\t\t\t\t\t\t\tconst range = getMatchRange(item.label, query);\n\t\t\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t\t\t/*\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t`onMouseDown` defends against a real browser's\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tfocus-follows-mousedown default action stealing\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tfocus onto this row before its own `onClick`\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tcommits — without it, that focus shift blurs the\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tinput first, which nothing here reacts to\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdirectly, but the whole point of this pattern is\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tthat focus never leaves the input at all. jsdom\n\t\t\t\t\t\t\t\t\t\t\t\t\t\timplements no such default action to suppress, so\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tno test in this folder can watch this guard\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tprevent that outcome — only that the call\n\t\t\t\t\t\t\t\t\t\t\t\t\t\thappens. Do not delete this as dead code on the\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tstrength of a green suite.\n\t\t\t\t\t\t\t\t\t\t\t\t\t*/\n\t\t\t\t\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tkey={item.id}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tid={optionId(index)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\trole=\"option\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdisabled={item.disabled}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\taria-selected={activeIndex === index}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\taria-disabled={item.disabled ? \"true\" : undefined}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"ft-command-menu-row flex w-full items-center justify-between gap-3 rounded-[8px] px-[12px] py-[8px] text-left text-[13px]\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tactiveIndex === index && !item.disabled\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? \"bg-accent text-accent-foreground\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: \"text-foreground\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\titem.disabled && \"pointer-events-none opacity-50\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonMouseDown={(event) => event.preventDefault()}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={() => commitItem(item)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"flex min-w-0 flex-1 items-center gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{icon ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span aria-hidden=\"true\" className=\"shrink-0\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{icon(item)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"min-w-0 truncate\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{range ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{item.label.slice(0, range.start)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<mark>{item.label.slice(range.start, range.end)}</mark>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{item.label.slice(range.end)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\titem.label\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{item.meta ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"shrink-0 text-[11px] opacity-60\">{item.meta}</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t\t\t</Fragment>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t{/* Always mounted while the panel is, whether or not the count has\n\t\t\t\t\t\t    settled — an element that only appears at the same moment its own\n\t\t\t\t\t\t    text does usually goes unread. Content is a debounced count,\n\t\t\t\t\t\t    never the item list itself. No `open ? … : \"\"` guard needed here:\n\t\t\t\t\t\t    this whole block only ever renders inside the presence gate, so\n\t\t\t\t\t\t    the region unmounting on close (not a text change) is what clears\n\t\t\t\t\t\t    it — see the \"clears immediately on close\" test. */}\n\t\t\t\t\t\t<div className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n\t\t\t\t\t\t\t{resultsMessage}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</>\n\t\t\t) : null}\n\t\t</Portal>\n\t);\n});\n\nCommandMenu.displayName = \"CommandMenu\";\n","type":"registry:ui","target":"@ui/fancy/components/command-menu/CommandMenu.tsx"},{"path":"react/src/components/command-menu/command-menu.css","content":"/*\n * The source's `<style>` block, anchored on the panel's own `.ft-command-menu`\n * root class — which the source already declares as the first token of the\n * panel's `cn()` call, so nothing had to be added here.\n *\n * The nav accent has no semantic Tailwind token, so it is a scoped\n * custom property with a `light-dark()` fallback, declared once here on\n * the component root and read below via `var(--ft-nav-accent)`.\n * `--ft-accent` itself is deliberately never redeclared — doing so would\n * shadow whatever value a consumer set higher up the tree, the exact bug\n * an earlier wave shipped in `Link`.\n */\n.ft-command-menu {\n\t--ft-nav-accent: var(\n\t\t--ft-accent,\n\t\tlight-dark(oklch(0.5432 0.2528 300.22), oklch(0.604 0.2606 301.75))\n\t);\n}\n\n.ft-command-menu mark {\n\tbackground: transparent;\n\tcolor: var(--ft-nav-accent);\n\tfont-weight: 600;\n}\n\n/*\n * No `@keyframes` and no `@media (prefers-reduced-motion)` block here any\n * more: both surfaces are driven by the shared JS transition on the\n * markup above, which collapses its own duration to 0 when the user has\n * asked for reduced motion — the runtime then skips `element.animate()`\n * entirely and the menu appears and disappears instantly, with the close\n * staying synchronous.\n *\n * The keyframes this replaced also carried a bug worth naming: the\n * panel's `from` restated `translateX(-50%)` as a `transform`, on a node\n * whose centring comes from Tailwind v4's separate `translate` property.\n * The two composed, so the panel drifted in from half its own width to\n * the left. `transform: scale(…)` alone composes after `translate` and\n * scales the panel about its own centre without touching the centring.\n */\n","type":"registry:ui","target":"@ui/fancy/components/command-menu/command-menu.css"},{"path":"react/src/components/command-menu/index.ts","content":"\"use client\";\n\nexport { CommandMenu } from \"./CommandMenu\";\nexport type { CommandMenuProps } from \"./CommandMenu\";\nexport type { CommandItem } from \"./types\";\n","type":"registry:ui","target":"@ui/fancy/components/command-menu/index.ts"},{"path":"react/src/components/command-menu/match.ts","content":"\"use client\";\n\n// Pure text-matching helpers: the default filter (case- and\n// diacritic-insensitive substring match over `label` plus `keywords`) and\n// `getMatchRange`, which locates the matched span for highlighting.\n//\n// The two are independent by construction, the same relationship\n// `combobox/match.ts` draws between its own filter and `getMatchRange`:\n// highlighting always looks for a literal, folded substring of `label`\n// itself, regardless of what logic actually selected the item. A consumer\n// `filter` can match on `keywords`, fuzzy logic, anything — when none of\n// that produces a literal substring of `label`, there is nothing honest to\n// highlight, so the row renders its plain, unhighlighted label instead of a\n// span that would misrepresent why it matched.\n\nimport type { CommandItem } from \"./types\";\n\n// Built from an escaped-codepoint string rather than a regex literal on\n// purpose — a raw combining mark sitting directly in this file's source\n// would attach itself to whatever precedes it on screen in most editors and\n// terminals, rendering unreadable.\nconst COMBINING_MARKS = new RegExp(\"[\\\\u0300-\\\\u036f]\", \"g\");\n\n/** Case- and diacritic-folded form of a string, for matching only — never rendered. */\nfunction fold(text: string): string {\n\treturn text.normalize(\"NFD\").replace(COMBINING_MARKS, \"\").toLowerCase();\n}\n\n/** Case- and diacritic-insensitive substring match over `label` plus `keywords`. An empty query matches everything. */\nexport function defaultFilter(item: CommandItem, query: string): boolean {\n\tif (!query) return true;\n\tconst needle = fold(query);\n\tif (fold(item.label).includes(needle)) return true;\n\treturn item.keywords?.some((keyword) => fold(keyword).includes(needle)) ?? false;\n}\n\nexport interface MatchRange {\n\tstart: number;\n\tend: number;\n}\n\n/**\n * Builds the folded (case/diacritic-stripped) form of `text` alongside a map\n * from each **UTF-16 code unit** of that folded form back to the UTF-16\n * offset it came from in `text`. `map` must be indexed exactly the way\n * `String.prototype.indexOf` counts — one entry per code unit of `folded`,\n * not one per codepoint — because `getMatchRange` below searches with\n * `indexOf` and reads `map` at the index it returns; if the two used\n * different units, every index past a surrogate pair would be off by one.\n *\n * A precomposed accented letter (\"é\") decomposes under NFD into a base\n * letter plus one or more combining marks; the marks are stripped and\n * contribute no output units, so only the base letter survives — still\n * pointing at the original character's own offset. An astral character\n * (outside the Basic Multilingual Plane — an emoji, say) is different:\n * `fold()` leaves it untouched, but it is still a surrogate pair, two UTF-16\n * code units long, in both `text` and `folded`. Pushing one `map` entry for\n * it (one per *codepoint*, matching the `for...of` iteration below, which\n * always yields one codepoint per pass) would silently under-count by one\n * for every such character — `map.length` would fall behind `folded.length`,\n * and every match found after it would read the wrong entry. The inner loop\n * pushes `outChar.length` entries instead, one per code unit `outChar`\n * actually contributes to `folded`, all pointing at the same source offset.\n *\n * This is what lets a match found in the folded string be translated back\n * to a span of the real, accented label: folding finds the right\n * *characters*, but it can shift *positions* (an NFD decomposition is\n * longer than its precomposed source, before the marks are stripped back\n * off), so a raw folded-string index is not a safe index into the original\n * label on its own — this map is the correction.\n */\nfunction foldWithMap(text: string): { folded: string; map: number[] } {\n\tlet folded = \"\";\n\tconst map: number[] = [];\n\tlet offset = 0;\n\t// `for...of` walks by Unicode codepoint, not UTF-16 code unit, so a\n\t// surrogate pair (an astral character) is one iteration, not two —\n\t// `char.length` below is then 1 or 2 accordingly, which is what keeps\n\t// `offset` a valid UTF-16 index into `text` for slicing later.\n\tfor (const char of text) {\n\t\tconst strippedChar = fold(char);\n\t\tfor (const outChar of strippedChar) {\n\t\t\tfolded += outChar;\n\t\t\tfor (let i = 0; i < outChar.length; i++) {\n\t\t\t\tmap.push(offset);\n\t\t\t}\n\t\t}\n\t\toffset += char.length;\n\t}\n\treturn { folded, map };\n}\n\n/**\n * Finds the first case- and diacritic-insensitive occurrence of `query`\n * inside `label`, returning the span **in `label`'s own original\n * characters** — not in the folded form used to search.\n *\n * Returns `null` when `query` is empty or not literally present in the\n * folded label — including when a custom `filter` matched the item for a\n * reason unrelated to a literal substring of `label` (a keyword, a fuzzy\n * match). Callers render the label plain in that case rather than\n * fabricating a highlighted span the match had nothing to do with.\n */\nexport function getMatchRange(label: string, query: string): MatchRange | null {\n\tif (!query) return null;\n\tconst foldedQuery = fold(query);\n\tif (!foldedQuery) return null;\n\n\tconst { folded, map } = foldWithMap(label);\n\tconst index = folded.indexOf(foldedQuery);\n\tif (index === -1) return null;\n\n\tconst start = map[index];\n\t// `map[index]` types as `number`, not `number | undefined` — this\n\t// project's tsconfig has `strict` but not `noUncheckedIndexedAccess`, so\n\t// the type system does not actually back that up here. Only\n\t// `foldWithMap`'s own invariant does (one map entry per UTF-16 code unit,\n\t// matching `folded` exactly), and a future edit to that function could\n\t// break it silently. Guarded anyway: a range with an `undefined` bound is\n\t// worse than no highlight — `CommandMenu.tsx`'s\n\t// `label.slice(range.start, range.end)` would see `slice(undefined, …)`,\n\t// which JS silently coerces to `slice(0, …)` instead of throwing, and the\n\t// row would render its entire label twice, once plain and once inside\n\t// `<mark>`.\n\tif (start === undefined) return null;\n\t// The offset one folded-character past the match's own last character —\n\t// `map[that index]` is where the *next* real character starts, which is\n\t// exactly the exclusive end this range needs. Past the end of `map`\n\t// (the match runs to the very end of the folded string) there is no\n\t// next entry to read, so the end is `label.length` instead.\n\t// Port note: this package's tsconfig DOES enable `noUncheckedIndexedAccess`\n\t// (the Svelte side's does not, which is what the comment above records), so\n\t// the read is spelled out as a local and the `undefined` case folds into\n\t// the same `label.length` branch the bounds check already selects. Same\n\t// values out, in every case — `nextStart` cannot be `undefined` while\n\t// `endFoldedIndex < map.length` holds.\n\tconst endFoldedIndex = index + foldedQuery.length;\n\tconst nextStart = map[endFoldedIndex];\n\tconst end = endFoldedIndex < map.length && nextStart !== undefined ? nextStart : label.length;\n\treturn { start, end };\n}\n","type":"registry:ui","target":"@ui/fancy/components/command-menu/match.ts"},{"path":"react/src/components/command-menu/types.ts","content":"\"use client\";\n\n/**\n * One selectable row. `id` is what a row is keyed by; `label` is what a\n * person reads and types against. `group` and `meta` are purely\n * presentational — CommandMenu never reorders or sorts `items` itself, it\n * only filters and buckets them by `group` for display.\n */\nexport interface CommandItem {\n\tid: string;\n\tlabel: string;\n\t/** Optional right-aligned secondary text — the category column in the mockup. */\n\tmeta?: string;\n\t/** Optional group heading this item sits under. Items with no group render first, ungrouped. */\n\tgroup?: string;\n\t/** Extra terms the default filter matches against, alongside `label`. Never shown. */\n\tkeywords?: string[];\n\tdisabled?: boolean;\n\t/** Called when this item is committed — Enter on the active row, or a click. Runs before the component's own `onSelect`. */\n\tonSelect?: () => void;\n}\n","type":"registry:ui","target":"@ui/fancy/components/command-menu/types.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/command-menu?framework=react"}}