{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "command-menu",
  "title": "Command Menu",
  "description": "Search and run grouped actions with full keyboard navigation.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/command-menu.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { Search } from \"lucide-react\";\nimport {\n  type ComponentProps,\n  createContext,\n  type KeyboardEvent,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const COMMAND_MENU_VARIANTS = [\"panel\", \"floating\", \"compact\"] as const;\nexport type CommandMenuVariant = (typeof COMMAND_MENU_VARIANTS)[number];\nexport type CommandMenuProps = Omit<ComponentProps<\"div\">, \"defaultValue\"> & {\n  variant?: CommandMenuVariant;\n  /** The search query. */\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  /** Called when Escape is pressed with an empty query, for closing a surrounding dialog. */\n  onDismiss?: () => void;\n  /** Label for the result listbox. */\n  label?: string;\n};\ntype MenuContext = {\n  id: string;\n  query: string;\n  /** Option rects by id, captured just before the query changes, for the filter FLIP. */\n  flip: React.RefObject<Map<string, number> | null>;\n  /** True once the first paint is done; rows that appear later fade in. */\n  ready: boolean;\n  setQuery: (value: string) => void;\n  activeId: string | null;\n  setActiveId: (id: string | null) => void;\n  empty: boolean;\n  label: string;\n  variant: CommandMenuVariant;\n  listRef: React.RefObject<HTMLDivElement | null>;\n  onDismiss?: () => void;\n};\nconst Context = createContext<MenuContext | null>(null);\nfunction useMenu(part: string) {\n  const context = useContext(Context);\n  if (!context) throw new Error(`${part} must be used within CommandMenu`);\n  return context;\n}\nconst GroupContext = createContext<string | null>(null);\nconst useIsomorphicLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\nfunction matches(query: string, text: string) {\n  const haystack = text.toLowerCase();\n  return query\n    .toLowerCase()\n    .split(/\\s+/)\n    .filter(Boolean)\n    .every((term) => haystack.includes(term));\n}\nconst EASE_POP = \"cubic-bezier(0.16, 1, 0.3, 1)\";\nconst EASE_QUINT = \"cubic-bezier(0.23, 1, 0.32, 1)\";\nconst HIGHLIGHT_NAME = \"uai-command-match\";\nconst prefersReducedMotion = () =>\n  typeof window !== \"undefined\" &&\n  typeof window.matchMedia === \"function\" &&\n  window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n/** Set after the page's first paint: a menu mounted later was opened by something. */\nlet pageSettled = false;\n\n/** Ranges of every query term inside the list's option text, for the CSS Custom Highlight API. */\nfunction matchRanges(list: HTMLElement, query: string) {\n  const terms = query.toLowerCase().split(/\\s+/).filter(Boolean);\n  const ranges: Range[] = [];\n  if (!terms.length) return ranges;\n  for (const option of list.querySelectorAll<HTMLElement>('[role=\"option\"]')) {\n    const walker = document.createTreeWalker(option, NodeFilter.SHOW_TEXT, {\n      acceptNode: (node) =>\n        node.parentElement?.closest('kbd, [aria-hidden=\"true\"]')\n          ? NodeFilter.FILTER_REJECT\n          : NodeFilter.FILTER_ACCEPT,\n    });\n    for (let node = walker.nextNode(); node; node = walker.nextNode()) {\n      const text = node.textContent?.toLowerCase() ?? \"\";\n      for (const term of terms) {\n        for (let at = text.indexOf(term); at !== -1; at = text.indexOf(term, at + term.length)) {\n          const range = document.createRange();\n          range.setStart(node, at);\n          range.setEnd(node, at + term.length);\n          ranges.push(range);\n        }\n      }\n    }\n  }\n  return ranges;\n}\n\nfunction options(list: HTMLElement | null) {\n  return Array.from(\n    list?.querySelectorAll<HTMLElement>('[role=\"option\"]:not([aria-disabled=\"true\"])') ?? [],\n  );\n}\n\nconst commandMenuVariants = cva(\n  \"flex min-w-0 origin-top flex-col overflow-hidden bg-popover text-[13px]/[18px] text-popover-foreground\",\n  {\n    variants: {\n      variant: {\n        panel: \"rounded-[14px] border p-0\",\n        floating:\n          \"rounded-[14px] border-0 p-0 shadow-[0_0_0_1px_var(--border-strong),0_10px_28px_oklch(0_0_0/0.42)]\",\n        compact: \"rounded-xl border p-0\",\n      },\n    },\n  },\n);\n\nexport function CommandMenu({\n  variant = \"panel\",\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  onDismiss,\n  label = \"Commands\",\n  children,\n  className,\n  ...props\n}: CommandMenuProps) {\n  const id = useId();\n  const listRef = useRef<HTMLDivElement>(null);\n  const rootRef = useRef<HTMLDivElement>(null);\n  const [internal, setInternal] = useState(defaultValue);\n  const [activeId, setActiveId] = useState<string | null>(null);\n  const [empty, setEmpty] = useState(false);\n  const [ready, setReady] = useState(false);\n  const flip = useRef<Map<string, number> | null>(null);\n  const query = value ?? internal;\n  // Keep the active option valid after filtering and when the result set changes.\n  useIsomorphicLayoutEffect(() => {\n    const visible = options(listRef.current);\n    const nextEmpty = !listRef.current?.querySelector('[role=\"option\"]');\n    if (nextEmpty !== empty) setEmpty(nextEmpty);\n    if (!visible.some((option) => option.id === activeId)) setActiveId(visible[0]?.id ?? null);\n  });\n  const lastQuery = useRef(query);\n  useIsomorphicLayoutEffect(() => {\n    if (lastQuery.current === query) return;\n    lastQuery.current = query;\n    setActiveId(options(listRef.current)[0]?.id ?? null);\n  }, [query]);\n  const floating = variant === \"floating\";\n  // Open: pop in from the input. A panel that is part of the page's first paint stays put;\n  // one mounted later (by a trigger) or a floating palette always pops.\n  useEffect(() => {\n    setReady(true);\n    const root = rootRef.current;\n    const opened = floating || pageSettled;\n    if (!pageSettled) requestAnimationFrame(() => (pageSettled = true));\n    if (!opened || !root?.animate) return;\n    const reduced = prefersReducedMotion();\n    root.animate(\n      reduced\n        ? [{ opacity: 0 }, { opacity: 1 }]\n        : [\n            { opacity: 0, scale: \"0.96\" },\n            { opacity: 1, scale: \"1\" },\n          ],\n      { duration: reduced ? 120 : 180, easing: reduced ? \"ease-out\" : EASE_POP },\n    );\n  }, [floating]);\n  // Matched characters turn foreground through the CSS Custom Highlight API, so item\n  // children stay untouched and widths never change while typing.\n  useEffect(() => {\n    const list = listRef.current;\n    const registry = typeof CSS !== \"undefined\" ? CSS.highlights : undefined;\n    if (!list || !registry || typeof Highlight === \"undefined\") return;\n    registry.set(HIGHLIGHT_NAME, new Highlight(...matchRanges(list, query)));\n    return () => {\n      registry.delete(HIGHLIGHT_NAME);\n    };\n  });\n  return (\n    <Context.Provider\n      value={{\n        id,\n        query,\n        flip,\n        ready,\n        setQuery: (next) => {\n          const positions = new Map<string, number>();\n          for (const option of listRef.current?.querySelectorAll<HTMLElement>('[role=\"option\"]') ??\n            [])\n            positions.set(option.id, option.offsetTop);\n          flip.current = positions;\n          if (value === undefined) setInternal(next);\n          onValueChange?.(next);\n        },\n        activeId,\n        setActiveId,\n        empty,\n        label,\n        variant,\n        listRef,\n        onDismiss,\n      }}\n    >\n      <div\n        data-slot=\"command-menu\"\n        data-variant={variant}\n        className={cn(commandMenuVariants({ variant }), className)}\n        {...props}\n        ref={rootRef}\n      >\n        {children}\n      </div>\n    </Context.Provider>\n  );\n}\n\nexport function CommandMenuInput({\n  className,\n  onChange,\n  onKeyDown,\n  placeholder = \"Search commands…\",\n  ...props\n}: Omit<ComponentProps<\"input\">, \"value\" | \"defaultValue\">) {\n  const context = useMenu(\"CommandMenuInput\");\n  const compact = context.variant === \"compact\";\n  const move = (event: KeyboardEvent<HTMLInputElement>) => {\n    const list = options(context.listRef.current);\n    const index = list.findIndex((option) => option.id === context.activeId);\n    let target: HTMLElement | undefined;\n    if (event.key === \"ArrowDown\") target = list[(index + 1) % list.length];\n    else if (event.key === \"ArrowUp\") target = list[(index - 1 + list.length) % list.length];\n    else if (event.key === \"Home\") target = list[0];\n    else if (event.key === \"End\") target = list[list.length - 1];\n    else if (event.key === \"Enter\") {\n      if (event.nativeEvent.isComposing) return;\n      event.preventDefault();\n      list[index]?.click();\n      return;\n    } else if (event.key === \"Escape\") {\n      event.preventDefault();\n      if (context.query) context.setQuery(\"\");\n      else context.onDismiss?.();\n      return;\n    } else return;\n    event.preventDefault();\n    if (!target) return;\n    // Key repeat moves the highlight faster so it never trails the key.\n    context.listRef.current?.toggleAttribute(\"data-repeat\", event.repeat);\n    context.setActiveId(target.id);\n    target.scrollIntoView?.({ block: \"nearest\" });\n  };\n  return (\n    <div\n      data-slot=\"command-menu-input-row\"\n      className={cn(\n        \"flex flex-none items-center border-b text-muted-foreground\",\n        compact ? \"h-10 gap-2 px-3\" : \"h-12 gap-2.5 px-3.5\",\n      )}\n    >\n      <Search size={16} strokeWidth={1.75} aria-hidden=\"true\" className=\"flex-none\" />\n      <input\n        type=\"text\"\n        role=\"combobox\"\n        aria-label={context.label}\n        autoComplete=\"off\"\n        spellCheck={false}\n        placeholder={placeholder}\n        data-slot=\"command-menu-input\"\n        className={cn(\n          \"min-w-0 flex-1 border-0 bg-transparent text-foreground outline-none placeholder:text-muted-foreground\",\n          compact ? \"h-6 text-[12.5px]/[18px]\" : \"h-7 text-[13.5px]/[18px]\",\n          className,\n        )}\n        {...props}\n        value={context.query}\n        aria-expanded={!context.empty}\n        aria-controls={`${context.id}-list`}\n        aria-autocomplete=\"list\"\n        aria-activedescendant={context.activeId ?? undefined}\n        onChange={(event) => {\n          onChange?.(event);\n          if (!event.defaultPrevented) context.setQuery(event.target.value);\n        }}\n        onKeyDown={(event) => {\n          onKeyDown?.(event);\n          if (!event.defaultPrevented) move(event);\n        }}\n        onKeyUp={(event) => {\n          props.onKeyUp?.(event);\n          context.listRef.current?.removeAttribute(\"data-repeat\");\n        }}\n      />\n      <span\n        aria-hidden=\"true\"\n        className=\"flex-none rounded-md bg-foreground/6 px-1.25 font-mono text-[11px]/[18px] font-medium text-muted-foreground\"\n      >\n        esc\n      </span>\n    </div>\n  );\n}\n\nexport function CommandMenuList({ className, children, style, ...props }: ComponentProps<\"div\">) {\n  const context = useMenu(\"CommandMenuList\");\n  const contentRef = useRef<HTMLDivElement>(null);\n  const highlightRef = useRef<HTMLSpanElement>(null);\n  const [height, setHeight] = useState<number | null>(null);\n  const [measured, setMeasured] = useState(false);\n  // The panel follows its results: measure the content and transition the list height\n  // (approved exception E5 in quality/premium-exceptions.json).\n  useIsomorphicLayoutEffect(() => {\n    const content = contentRef.current;\n    if (!content || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(() => setHeight(content.offsetHeight));\n    observer.observe(content);\n    setHeight(content.offsetHeight);\n    const frame = requestAnimationFrame(() => setMeasured(true));\n    return () => {\n      observer.disconnect();\n      cancelAnimationFrame(frame);\n    };\n  }, []);\n  // One shared highlight slides to the active row.\n  useIsomorphicLayoutEffect(() => {\n    const highlight = highlightRef.current;\n    const row = context.activeId ? document.getElementById(context.activeId) : null;\n    if (!highlight) return;\n    if (!row || !context.listRef.current?.contains(row)) {\n      highlight.style.opacity = \"0\";\n      return;\n    }\n    const wasHidden = highlight.style.opacity !== \"1\";\n    if (wasHidden) highlight.style.transitionProperty = \"none\";\n    highlight.style.translate = `0 ${row.offsetTop}px`;\n    highlight.style.height = `${row.offsetHeight}px`;\n    highlight.style.opacity = \"1\";\n    if (wasHidden) {\n      void highlight.offsetHeight;\n      highlight.style.transitionProperty = \"\";\n    }\n  });\n  // Filter: surviving rows slide from where they were; new rows fade in without moving.\n  useIsomorphicLayoutEffect(() => {\n    const before = context.flip.current;\n    context.flip.current = null;\n    const list = context.listRef.current;\n    if (!before || !list || prefersReducedMotion()) return;\n    for (const option of list.querySelectorAll<HTMLElement>('[role=\"option\"]')) {\n      if (typeof option.animate !== \"function\") continue;\n      const from = before.get(option.id);\n      if (from === undefined) {\n        option.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 120, easing: \"ease-out\" });\n        continue;\n      }\n      const dy = from - option.offsetTop;\n      if (Math.abs(dy) > 0.5)\n        option.animate([{ translate: `0 ${dy}px` }, { translate: \"0 0\" }], {\n          duration: 200,\n          easing: EASE_QUINT,\n        });\n    }\n  }, [context.query]);\n  const compact = context.variant === \"compact\";\n  return (\n    <div\n      role=\"listbox\"\n      aria-label={context.label}\n      data-slot=\"command-menu-list\"\n      data-measured={measured || undefined}\n      className={cn(\n        \"relative max-h-80 overflow-y-auto overscroll-contain scroll-py-1\",\n        \"data-measured:transition-[height] data-measured:duration-160 data-measured:ease-out-quint motion-reduce:transition-none\",\n        \"[&_*::highlight(uai-command-match)]:text-foreground\",\n        className,\n      )}\n      style={height === null ? style : { height: Math.min(height, 320), ...style }}\n      {...props}\n      ref={context.listRef}\n      id={`${context.id}-list`}\n    >\n      <span\n        ref={highlightRef}\n        aria-hidden=\"true\"\n        data-slot=\"command-menu-highlight\"\n        className={cn(\n          \"pointer-events-none absolute inset-x-1 top-0 bg-accent opacity-0\",\n          compact ? \"rounded-[7px]\" : \"rounded-lg\",\n          \"transition-[translate,opacity,background-color,scale] duration-160 ease-out-quint in-data-repeat:duration-100 motion-reduce:transition-[opacity,background-color]\",\n          \"data-running:scale-[0.99] data-running:bg-[color-mix(in_oklab,var(--accent)_80%,var(--foreground))]\",\n        )}\n      />\n      <div ref={contentRef} data-slot=\"command-menu-list-content\" className=\"relative p-1\">\n        {children}\n      </div>\n    </div>\n  );\n}\n\nexport function CommandMenuGroup({ className, children, ...props }: ComponentProps<\"div\">) {\n  useMenu(\"CommandMenuGroup\");\n  const labelId = useId();\n  const ref = useRef<HTMLDivElement>(null);\n  const [hidden, setHidden] = useState(false);\n  useIsomorphicLayoutEffect(() => {\n    const next = !ref.current?.querySelector('[role=\"option\"]');\n    if (next !== hidden) setHidden(next);\n  });\n  return (\n    <GroupContext.Provider value={labelId}>\n      {/* biome-ignore lint/a11y/useSemanticElements: APG listbox groups use role=\"group\"; a fieldset is not allowed in a listbox. */}\n      <div\n        role=\"group\"\n        aria-labelledby={labelId}\n        data-slot=\"command-menu-group\"\n        className={cn(hidden ? \"hidden\" : \"grid\", \"gap-px pb-0.5\", className)}\n        {...props}\n        ref={ref}\n        hidden={hidden}\n      >\n        {children}\n      </div>\n    </GroupContext.Provider>\n  );\n}\n\nexport function CommandMenuGroupLabel({ className, ...props }: ComponentProps<\"div\">) {\n  const labelId = useContext(GroupContext);\n  if (!labelId) throw new Error(\"CommandMenuGroupLabel must be used within CommandMenuGroup\");\n  return (\n    <div\n      data-slot=\"command-menu-group-label\"\n      className={cn(\n        \"px-2.5 pt-2 pb-1.5 text-[11.5px]/4 font-normal text-subtle-foreground\",\n        className,\n      )}\n      {...props}\n      id={labelId}\n    />\n  );\n}\n\nexport type CommandMenuItemProps = Omit<ComponentProps<\"div\">, \"onSelect\"> & {\n  /** Text matched against the query. */\n  value: string;\n  /** Extra search terms, such as synonyms. */\n  keywords?: readonly string[];\n  disabled?: boolean;\n  onSelect?: () => void;\n};\n\nexport function CommandMenuItem({\n  value,\n  keywords = [],\n  disabled = false,\n  onSelect,\n  onClick,\n  onPointerMove,\n  className,\n  ...props\n}: CommandMenuItemProps) {\n  const context = useMenu(\"CommandMenuItem\");\n  const id = useId();\n  if (!matches(context.query, [value, ...keywords].join(\" \"))) return null;\n  const active = context.activeId === id;\n  const compact = context.variant === \"compact\";\n  // Run: flash the shared highlight for 120ms (scale .99 and a deeper tint).\n  const flash = () => {\n    const highlight = context.listRef.current?.querySelector<HTMLElement>(\n      '[data-slot=\"command-menu-highlight\"]',\n    );\n    if (!highlight) return;\n    highlight.setAttribute(\"data-running\", \"\");\n    window.setTimeout(() => highlight.removeAttribute(\"data-running\"), 120);\n  };\n  return (\n    // biome-ignore lint/a11y/useFocusableInteractive: options are navigated with aria-activedescendant from the combobox input.\n    // biome-ignore lint/a11y/useKeyWithClickEvents: keyboard selection is handled by the combobox input.\n    <div\n      role=\"option\"\n      data-slot=\"command-menu-item\"\n      className={cn(\n        \"relative flex items-center font-medium select-none [&>svg]:size-4 [&>svg]:shrink-0\",\n        \"transition-colors duration-120 ease-out motion-reduce:transition-none [&>svg]:transition-colors [&>svg]:duration-100 [&>svg]:ease-out\",\n        compact\n          ? \"min-h-7 gap-2 rounded-[7px] px-2 text-[12.5px]/[18px]\"\n          : \"min-h-9 gap-2.5 rounded-lg px-2.5 text-[13px]/[18px]\",\n        disabled\n          ? \"cursor-not-allowed text-subtle-foreground [&>svg]:text-subtle-foreground [&>svg]:opacity-60\"\n          : \"cursor-pointer text-muted-foreground data-active:text-foreground [&>svg]:text-muted-foreground data-active:[&>svg]:text-foreground\",\n        className,\n      )}\n      {...props}\n      id={id}\n      aria-selected={active}\n      aria-disabled={disabled || undefined}\n      data-active={active || undefined}\n      onPointerMove={(event) => {\n        onPointerMove?.(event);\n        if (!disabled && !active) context.setActiveId(id);\n      }}\n      onClick={(event) => {\n        onClick?.(event);\n        if (disabled || event.defaultPrevented) return;\n        if (!active) context.setActiveId(id);\n        if (!prefersReducedMotion()) flash();\n        onSelect?.();\n      }}\n    />\n  );\n}\n\nexport function CommandMenuShortcut({ className, ...props }: ComponentProps<\"kbd\">) {\n  return (\n    <kbd\n      data-slot=\"command-menu-shortcut\"\n      className={cn(\n        \"ml-auto min-w-5 rounded-md bg-foreground/6 px-1.25 text-center font-mono text-[11px]/[18px] font-medium text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function CommandMenuEmpty({ className, children, ...props }: ComponentProps<\"div\">) {\n  const context = useMenu(\"CommandMenuEmpty\");\n  const ref = useRef<HTMLDivElement>(null);\n  useEffect(() => {\n    if (!context.empty || !context.ready || prefersReducedMotion()) return;\n    ref.current?.animate?.(\n      [\n        { opacity: 0, translate: \"0 4px\" },\n        { opacity: 1, translate: \"0 0\" },\n      ],\n      { duration: 200, easing: EASE_QUINT },\n    );\n  }, [context.empty, context.ready]);\n  return (\n    <div\n      ref={ref}\n      role=\"status\"\n      data-slot=\"command-menu-empty\"\n      className={cn(\n        context.empty ? \"grid\" : \"hidden\",\n        \"justify-items-center gap-1 px-4 pt-6 pb-7 text-center\",\n        className,\n      )}\n      {...props}\n    >\n      {context.empty ? (\n        <>\n          <span\n            aria-hidden=\"true\"\n            className=\"mb-2 grid size-8 place-items-center rounded-full bg-muted text-muted-foreground\"\n          >\n            <Search size={16} strokeWidth={1.75} />\n          </span>\n          <span className=\"text-[13px]/[18px] font-medium text-foreground\">\n            {context.query ? `No results for “${context.query}”` : \"No results\"}\n          </span>\n          {children ? (\n            <span className=\"text-[12.5px]/[18px] text-muted-foreground\">{children}</span>\n          ) : null}\n        </>\n      ) : null}\n    </div>\n  );\n}\n\n/** Optional key legend under the list. Put a context action in it with `ml-auto`. */\nexport function CommandMenuFooter({ className, children, ...props }: ComponentProps<\"div\">) {\n  const context = useMenu(\"CommandMenuFooter\");\n  return (\n    <div\n      data-slot=\"command-menu-footer\"\n      className={cn(\n        \"flex h-9 flex-none items-center gap-3 border-t px-3 text-[11.5px]/4 text-subtle-foreground\",\n        context.variant === \"compact\" && \"h-8 px-2.5\",\n        className,\n      )}\n      {...props}\n    >\n      {children ?? (\n        <>\n          <span>\n            <kbd className=\"font-mono\">↑↓</kbd> Navigate\n          </span>\n          <span>\n            <kbd className=\"font-mono\">↵</kbd> Open\n          </span>\n          <span>\n            <kbd className=\"font-mono\">esc</kbd> Close\n          </span>\n        </>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/command-menu.tsx"
    }
  ],
  "categories": [
    "navigation"
  ],
  "type": "registry:ui"
}