{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "elements-command-palette",
  "type": "registry:component",
  "title": "Command palette",
  "description": "Everything the app can do, one keystroke away and grouped by where it acts.",
  "registryDependencies": [
    "https://r.assistant-ui.com/elements-surfaces.json"
  ],
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "type": "registry:component",
      "path": "components/assistant-ui/elements/command-palette.tsx",
      "sourcePath": "packages/ui/src/components/react/assistant-ui/elements/command-palette.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, type ComponentProps } from \"react\";\nimport { SearchIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { field, floating, mono } from \"./surfaces\";\n\nexport interface PaletteCommand {\n  id: string;\n  label: string;\n  group: string;\n  keys: readonly string[];\n}\n\nexport function CommandPalette({\n  commands,\n  query,\n  activeId,\n  onQueryChange,\n  onActiveChange,\n  onRun,\n  className,\n  ...props\n}: Omit<\n  ComponentProps<\"div\">,\n  | \"children\"\n  | \"commands\"\n  | \"query\"\n  | \"activeId\"\n  | \"onQueryChange\"\n  | \"onActiveChange\"\n  | \"onRun\"\n> & {\n  commands: readonly PaletteCommand[];\n  query: string;\n  activeId: string;\n  onQueryChange?: (query: string) => void;\n  onActiveChange?: (id: string) => void;\n  onRun?: (id: string) => void;\n}) {\n  const listId = useId();\n  const optionId = (id: string) => `${listId}-${id}`;\n  const matches = commands.filter((command) =>\n    command.label.toLowerCase().includes(query.toLowerCase()),\n  );\n  const groups = [...new Set(matches.map((command) => command.group))];\n  // display order, which is grouped and so differs from the filter order\n  const ordered = groups.flatMap((group) =>\n    matches.filter((command) => command.group === group),\n  );\n\n  const move = (delta: number) => {\n    if (ordered.length === 0) return;\n    const at = ordered.findIndex((command) => command.id === activeId);\n    // activeId can be filtered out by the query; start from the edge the key implies\n    const from = at === -1 ? (delta > 0 ? -1 : 0) : at;\n    const next = ordered[(from + delta + ordered.length) % ordered.length];\n    if (next) onActiveChange?.(next.id);\n  };\n\n  // aria-activedescendant moves the highlight without moving focus, and only\n  // focus scrolls; the list is short enough to walk the active row out of view.\n  useEffect(() => {\n    document\n      .getElementById(optionId(activeId))\n      ?.scrollIntoView({ block: \"nearest\" });\n  }, [activeId, listId]);\n\n  const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n    if (event.nativeEvent.isComposing) return;\n    if (event.key === \"ArrowDown\") {\n      event.preventDefault();\n      move(1);\n    } else if (event.key === \"ArrowUp\") {\n      event.preventDefault();\n      move(-1);\n    } else if (event.key === \"Enter\") {\n      event.preventDefault();\n      const active = ordered.find((command) => command.id === activeId);\n      if (active) onRun?.(active.id);\n    }\n  };\n\n  return (\n    <div\n      data-slot=\"command-palette\"\n      className={cn(\n        floating,\n        \"flex w-full max-w-sm flex-col overflow-hidden rounded-[20px]\",\n        className,\n      )}\n\n      {...props}\n    >\n      <div className=\"flex items-center gap-2.5 px-3.5 py-3\">\n        <SearchIcon className=\"text-foreground/30 size-3.5 shrink-0\" />\n        <input\n          value={query}\n          onChange={(event) => onQueryChange?.(event.target.value)}\n          onKeyDown={onKeyDown}\n          placeholder=\"Type a command\"\n          aria-label=\"Type a command\"\n          role=\"combobox\"\n          aria-expanded={ordered.length > 0}\n          aria-controls={listId}\n          aria-autocomplete=\"list\"\n          aria-activedescendant={\n            ordered.some((command) => command.id === activeId)\n              ? optionId(activeId)\n              : undefined\n          }\n          className=\"text-foreground/85 placeholder:text-foreground/30 min-w-0 flex-1 bg-transparent text-[13.5px] outline-none\"\n        />\n        <span\n          className={cn(\n            field,\n            mono,\n            \"text-foreground/35 rounded px-1.5 py-0.5\",\n          )}\n        >\n          esc\n        </span>\n      </div>\n\n      <div\n        id={listId}\n        role=\"listbox\"\n        aria-label=\"Commands\"\n        className=\"border-foreground/[0.07] flex max-h-72 flex-col overflow-y-auto border-t p-1.5\"\n      >\n        {groups.map((group) => (\n          <div\n            key={group}\n            role=\"group\"\n            aria-label={group}\n            className=\"flex flex-col\"\n          >\n            <span\n              aria-hidden\n              className={cn(mono, \"text-foreground/25 px-2 pt-2 pb-1\")}\n            >\n              {group}\n            </span>\n            {matches\n              .filter((command) => command.group === group)\n              .map((command) => (\n                <button\n                  key={command.id}\n                  id={optionId(command.id)}\n                  type=\"button\"\n                  role=\"option\"\n                  tabIndex={-1}\n                  aria-selected={command.id === activeId}\n                  onMouseDown={(event) => event.preventDefault()}\n                  onClick={() => onRun?.(command.id)}\n                  className={cn(\n                    \"flex items-center gap-2 rounded-xl px-2 py-1.5 text-start transition-colors\",\n                    command.id === activeId\n                      ? \"bg-foreground/[0.06]\"\n                      : \"hover:bg-foreground/[0.03]\",\n                  )}\n                >\n                  <span className=\"min-w-0 flex-1 truncate text-[13px]\">\n                    {command.label}\n                  </span>\n                  <span className=\"flex shrink-0 gap-1\">\n                    {command.keys.map((key) => (\n                      <span\n                        key={key}\n                        className={cn(\n                          field,\n                          mono,\n                          \"text-foreground/40 rounded px-1.5 py-0.5\",\n                        )}\n                      >\n                        {key}\n                      </span>\n                    ))}\n                  </span>\n                </button>\n              ))}\n          </div>\n        ))}\n      </div>\n      {matches.length === 0 && (\n        <div className=\"border-foreground/[0.07] border-t p-1.5\">\n          <span className=\"text-foreground/30 block px-2 py-4 text-center text-xs break-words\">\n            No command matches “{query}”\n          </span>\n        </div>\n      )}\n    </div>\n  );\n}\n"
    }
  ]
}