{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "command-palette",
  "title": "Command Palette",
  "description": "Grouped commands with a loose filter, run entirely from the keyboard.",
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "https://spotcheckui.com/r/spotcheck-base.json"
  ],
  "files": [
    {
      "path": "registry/spotcheck/components/spotcheck/command-palette.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Dialog } from \"@base-ui/react/dialog\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst PLACEHOLDER = \"Type a command\"\n\nexport type Command = {\n  id: string\n  label: string\n  /** Extra words to match, which are never shown. */\n  keywords?: string\n  /** A key hint printed at the end of the row. */\n  shortcut?: string\n  onSelect: () => void\n}\n\nexport type CommandGroup = { label: string; commands: Command[] }\n\ntype CommandPaletteProps = {\n  /** The commands, in display order. */\n  groups: CommandGroup[]\n  /** Whether the palette is showing. */\n  open: boolean\n  /** Runs when the palette should open or close. */\n  onOpenChange: (open: boolean) => void\n  /** Opens with Cmd or Ctrl plus this key. Pass false to bind no key. */\n  hotkey?: string | false\n  /** Names the dialog for screen readers. */\n  label?: string\n  /** Text shown in the empty input. */\n  placeholder?: string\n}\n\nfunction isSubsequence(query: string, text: string) {\n  let found = 0\n  for (const char of text) {\n    if (char === query[found]) found++\n    if (found === query.length) return true\n  }\n  return false\n}\n\n/** Keeps the matching commands, with direct matches ahead of loose ones in each group. */\nexport function filterCommands(groups: CommandGroup[], query: string) {\n  const needle = query.trim().toLowerCase()\n  if (!needle) return groups\n\n  const rank = (command: Command) => {\n    const label = command.label.toLowerCase()\n    if (label.startsWith(needle)) return 0\n    if (label.includes(needle)) return 1\n    if (command.keywords?.toLowerCase().includes(needle)) return 2\n    return isSubsequence(needle, label) ? 3 : null\n  }\n\n  return groups\n    .map((group) => ({\n      ...group,\n      commands: group.commands\n        .map((command) => ({ command, score: rank(command) }))\n        .filter((entry): entry is { command: Command; score: number } => entry.score !== null)\n        .sort((a, b) => a.score - b.score)\n        .map((entry) => entry.command),\n    }))\n    .filter((group) => group.commands.length > 0)\n}\n\nexport function CommandPalette({\n  groups,\n  open,\n  onOpenChange,\n  hotkey = \"k\",\n  label = \"Command palette\",\n  placeholder = PLACEHOLDER,\n}: CommandPaletteProps) {\n  const [query, setQuery] = React.useState(\"\")\n  const [activeIndex, setActiveIndex] = React.useState(0)\n  const input = React.useRef<HTMLInputElement>(null)\n  const list = React.useRef<HTMLDivElement>(null)\n  const id = React.useId()\n\n  const visible = filterCommands(groups, query)\n  const commands = visible.flatMap((group) => group.commands)\n  const active = commands[activeIndex]\n  const optionId = (index: number) => `${id}-option-${index}`\n\n  React.useEffect(() => {\n    if (!hotkey) return\n    const onKeyDown = (event: KeyboardEvent) => {\n      if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === hotkey) {\n        event.preventDefault()\n        onOpenChange(!open)\n      }\n    }\n    window.addEventListener(\"keydown\", onKeyDown)\n    return () => window.removeEventListener(\"keydown\", onKeyDown)\n  }, [hotkey, onOpenChange, open])\n\n  React.useEffect(() => {\n    list.current?.querySelector('[aria-selected=\"true\"]')?.scrollIntoView({ block: \"nearest\" })\n  }, [activeIndex])\n\n  const changeOpen = (next: boolean) => {\n    if (next) {\n      setQuery(\"\")\n      setActiveIndex(0)\n    }\n    onOpenChange(next)\n  }\n\n  const run = (command: Command) => {\n    onOpenChange(false)\n    command.onSelect()\n  }\n\n  const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n    const last = commands.length - 1\n    if (event.key === \"ArrowDown\") {\n      event.preventDefault()\n      setActiveIndex((index) => Math.min(last, index + 1))\n    } else if (event.key === \"ArrowUp\") {\n      event.preventDefault()\n      setActiveIndex((index) => Math.max(0, index - 1))\n    } else if (event.key === \"Enter\" && active) {\n      event.preventDefault()\n      run(active)\n    }\n  }\n\n  let index = -1\n\n  return (\n    <Dialog.Root open={open} onOpenChange={changeOpen}>\n      <Dialog.Portal>\n        <Dialog.Backdrop className=\"fixed inset-0 z-50 bg-[color-mix(in_srgb,var(--ink)_30%,transparent)]\" />\n        <Dialog.Popup\n          initialFocus={input}\n          className=\"border-ink bg-paper text-ink fixed top-[12dvh] left-1/2 z-50 w-[min(520px,calc(100vw-2rem))] -translate-x-1/2 border-(length:--rule) font-(family-name:--f-body) shadow-[8px_8px_0_var(--ink)] transition-[translate,opacity] duration-150 outline-none data-starting-style:-translate-y-2 data-starting-style:opacity-40 motion-reduce:transition-none\"\n        >\n          <Dialog.Title className=\"sr-only\">{label}</Dialog.Title>\n          <input\n            ref={input}\n            role=\"combobox\"\n            aria-expanded\n            aria-autocomplete=\"list\"\n            aria-label={placeholder}\n            aria-controls={`${id}-list`}\n            aria-activedescendant={active ? optionId(activeIndex) : undefined}\n            value={query}\n            placeholder={placeholder}\n            onChange={(event) => {\n              setQuery(event.target.value)\n              setActiveIndex(0)\n            }}\n            onKeyDown={onKeyDown}\n            className=\"border-ink placeholder:text-mute w-full border-b-(length:--rule) bg-transparent px-3.5 py-3 text-lg outline-none\"\n            style={{ fontFamily: \"var(--f-display)\", fontStretch: \"80%\" }}\n          />\n          <div\n            ref={list}\n            id={`${id}-list`}\n            role=\"listbox\"\n            aria-label={label}\n            className=\"max-h-[min(320px,60dvh)] overflow-auto py-1\"\n          >\n            {visible.map((group) => (\n              <div key={group.label} role=\"group\" aria-label={group.label}>\n                <div\n                  aria-hidden\n                  className=\"spotcheck-label px-3.5 pt-2 pb-0.5\"\n                >\n                  {group.label}\n                </div>\n                {group.commands.map((command) => {\n                  index++\n                  const current = index\n                  return (\n                    <div\n                      key={command.id}\n                      id={optionId(current)}\n                      role=\"option\"\n                      aria-selected={current === activeIndex}\n                      onPointerMove={() => setActiveIndex(current)}\n                      onClick={() => run(command)}\n                      className={cn(\n                        \"flex cursor-pointer items-center justify-between gap-3 px-3.5 py-[7px] text-sm\",\n                        current === activeIndex && \"bg-ink text-paper\",\n                      )}\n                    >\n                      <span>{command.label}</span>\n                      {command.shortcut && (\n                        <kbd className=\"border-[1.5px] border-current px-1.5 font-(family-name:--f-mono) text-[11px]\">\n                          {command.shortcut}\n                        </kbd>\n                      )}\n                    </div>\n                  )\n                })}\n              </div>\n            ))}\n            {commands.length === 0 && (\n              <p className=\"text-mute p-3.5 font-(family-name:--f-mono) text-xs\">\n                No command matches “{query}”.\n              </p>\n            )}\n          </div>\n        </Dialog.Popup>\n      </Dialog.Portal>\n    </Dialog.Root>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "screens"
  ],
  "type": "registry:component"
}