{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "command-palette",
  "title": "Command palette",
  "description": "A searchable list of commands in a modal, opened with Cmd+K. Built on the native dialog.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/manniche/command-palette/command-palette.tsx",
      "content": "import { Search } from 'lucide-react'\nimport { useEffect, useId, useMemo, useRef, useState, type ReactNode } from 'react'\nimport { cn } from '@/lib/utils'\n\nexport type Command = {\n  id: string\n  label: string\n  /** Heading the command is listed under. */\n  group?: string\n  /** Extra words that should also find it. */\n  keywords?: string[]\n  /** Shown on the right, e.g. a shortcut. */\n  hint?: ReactNode\n  icon?: ReactNode\n  onSelect: () => void\n}\n\nexport type CommandPaletteProps = {\n  commands: Command[]\n  open: boolean\n  onOpenChange: (open: boolean) => void\n  placeholder?: string\n  /** Open and close with Cmd+K / Ctrl+K. */\n  hotkey?: boolean\n  className?: string\n}\n\n/**\n * A searchable list of commands in a modal. Built on the native <dialog>, so focus is trapped\n * and Esc closes it. The input is a combobox; arrow keys move, Enter runs the command.\n */\nexport function CommandPalette({ commands, open, onOpenChange, placeholder = 'Type a command or search', hotkey = true, className }: CommandPaletteProps) {\n  const dialog = useRef<HTMLDialogElement>(null)\n  const listId = useId()\n  const [query, setQuery] = useState('')\n  const [active, setActive] = useState(0)\n\n  useEffect(() => {\n    const d = dialog.current\n    if (!d) return\n    if (open && !d.open) {\n      setQuery('')\n      setActive(0)\n      d.showModal()\n    } else if (!open && d.open) d.close()\n  }, [open])\n\n  useEffect(() => {\n    if (!hotkey) return\n    const onKey = (e: KeyboardEvent) => {\n      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {\n        e.preventDefault()\n        onOpenChange(!open)\n      }\n    }\n    addEventListener('keydown', onKey)\n    return () => removeEventListener('keydown', onKey)\n  }, [hotkey, open, onOpenChange])\n\n  const shown = useMemo(() => {\n    const q = query.trim().toLowerCase()\n    if (!q) return commands\n    return commands.filter((c) => [c.label, c.group, ...(c.keywords ?? [])].some((s) => s?.toLowerCase().includes(q)))\n  }, [commands, query])\n\n  const groups = useMemo(() => {\n    const map = new Map<string, Command[]>()\n    for (const c of shown) map.set(c.group ?? '', [...(map.get(c.group ?? '') ?? []), c])\n    return [...map]\n  }, [shown])\n\n  const current = Math.min(active, shown.length - 1)\n\n  function run(c: Command | undefined) {\n    if (!c) return\n    onOpenChange(false)\n    c.onSelect()\n  }\n\n  useEffect(() => {\n    document.getElementById(`${listId}-${current}`)?.scrollIntoView({ block: 'nearest' })\n  }, [current, listId])\n\n  let index = 0\n  return (\n    <dialog\n      ref={dialog}\n      onClose={() => onOpenChange(false)}\n      onClick={(e) => e.target === dialog.current && onOpenChange(false)}\n      aria-label=\"Command palette\"\n      className={cn(\n        'm-0 mx-auto mt-[14vh] w-[min(36rem,calc(100vw-2rem))] max-w-none overflow-hidden rounded-2xl border bg-card p-0 text-card-foreground shadow-2xl',\n        'backdrop:bg-black/40 open:animate-[manniche-pop_220ms_cubic-bezier(0.23,1,0.32,1)] motion-reduce:open:animate-none',\n        className,\n      )}\n    >\n      <style href=\"manniche-pop\" precedence=\"default\">\n        {'@keyframes manniche-pop { from { opacity: 0; transform: translateY(8px) scale(.98) } }'}\n      </style>\n      <div className=\"flex items-center gap-3 border-b px-4\">\n        <Search className=\"size-4 shrink-0 text-muted-foreground\" aria-hidden />\n        <input\n          autoFocus\n          value={query}\n          onChange={(e) => {\n            setQuery(e.target.value)\n            setActive(0)\n          }}\n          onKeyDown={(e) => {\n            if (!shown.length) return\n            if (e.key === 'ArrowDown') {\n              e.preventDefault()\n              setActive((current + 1) % shown.length)\n            } else if (e.key === 'ArrowUp') {\n              e.preventDefault()\n              setActive((current - 1 + shown.length) % shown.length)\n            } else if (e.key === 'Enter') {\n              e.preventDefault()\n              run(shown[current])\n            }\n          }}\n          role=\"combobox\"\n          aria-expanded=\"true\"\n          aria-controls={listId}\n          aria-autocomplete=\"list\"\n          aria-activedescendant={shown.length ? `${listId}-${current}` : undefined}\n          aria-label={placeholder}\n          placeholder={placeholder}\n          className=\"h-14 min-w-0 flex-1 bg-transparent text-base outline-none placeholder:text-muted-foreground focus-visible:outline-none focus-visible:shadow-[inset_0_-2px_0_var(--color-ring)]\"\n        />\n      </div>\n\n      <div id={listId} role=\"listbox\" aria-label=\"Commands\" className=\"max-h-80 overflow-y-auto p-1.5\">\n        {shown.length === 0 && <p className=\"px-3 py-8 text-center text-sm text-muted-foreground\">No commands match “{query}”.</p>}\n        {groups.map(([group, list]) => (\n          <div key={group} role=\"group\" aria-label={group || undefined}>\n            {group && <p className=\"px-3 pt-2 pb-1 text-xs font-medium tracking-wide text-muted-foreground uppercase\">{group}</p>}\n            {list.map((c) => {\n              const i = index++\n              return (\n                <div\n                  key={c.id}\n                  id={`${listId}-${i}`}\n                  role=\"option\"\n                  aria-selected={i === current}\n                  onPointerMove={() => i !== current && setActive(i)}\n                  onClick={() => run(c)}\n                  className=\"flex min-h-11 cursor-pointer items-center gap-3 rounded-xl px-3 text-[0.9375rem] aria-selected:bg-accent aria-selected:text-accent-foreground\"\n                >\n                  {c.icon && <span className=\"text-muted-foreground [&_svg]:size-4\">{c.icon}</span>}\n                  <span className=\"min-w-0 flex-1 truncate\">{c.label}</span>\n                  {c.hint && <span className=\"text-sm text-muted-foreground\">{c.hint}</span>}\n                </div>\n              )\n            })}\n          </div>\n        ))}\n      </div>\n    </dialog>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "overlays"
  ],
  "type": "registry:component"
}