{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "command-palette",
  "title": "Command Palette",
  "description": "A search dialog over anything you can list, opened from a keyboard shortcut.",
  "dependencies": [
    "lucide-react@^1.31.0"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/command-palette/command-palette.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Search } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\n\nexport type CommandItem = {\n  id: string\n  label: string\n  description?: string\n  group?: string\n  /** Extra words that should match, without being shown. */\n  keywords?: string[]\n}\n\nexport type CommandPaletteProps = Omit<\n  React.HTMLAttributes<HTMLDialogElement>,\n  \"children\" | \"onSelect\"\n> & {\n  items: CommandItem[]\n  onSelect?: (item: CommandItem) => void\n  open?: boolean\n  defaultOpen?: boolean\n  onOpenChange?: (open: boolean) => void\n  /** Key used with Meta or Control to toggle. Pass false to bind nothing. */\n  shortcut?: string | false\n  placeholder?: string\n  label?: string\n  emptyMessage?: (query: string) => React.ReactNode\n  maxResults?: number\n  /** Called as the query changes, for fetching the results yourself. */\n  onQueryChange?: (query: string) => void\n  /** Says results are on their way. Pair it with onQueryChange. */\n  loading?: boolean\n  loadingMessage?: React.ReactNode\n  /**\n   * Rank and filter the items here. Turn it off when they arrive already\n   * matched and ordered, so a server's ranking is not overruled.\n   */\n  filter?: boolean\n  /**\n   * Score an item against the query yourself: fuzzy matching, a field we do\n   * not know about, a weighting of your own. Lower is a better match, and\n   * false or nothing drops the item.\n   */\n  rank?: CommandRanker\n}\n\nexport type CommandRanker = (\n  item: CommandItem,\n  query: string\n) => number | false | null | undefined\n\n/**\n * Lower is a better match; Infinity, false, or nothing means no match. It is\n * exported so a ranker of your own can fall back to it rather than reproduce\n * it.\n */\nexport function rankCommandItem(item: CommandItem, rawQuery: string) {\n  const query = rawQuery.trim().toLowerCase()\n  const label = item.label.toLowerCase()\n\n  if (label === query) return 0\n  if (label.startsWith(query)) return 1\n  if (label.includes(query)) return 2\n  if (item.group?.toLowerCase().includes(query)) return 3\n  if (item.keywords?.some((word) => word.toLowerCase().includes(query)))\n    return 4\n  if (item.description?.toLowerCase().includes(query)) return 5\n\n  return Number.POSITIVE_INFINITY\n}\n\nexport function CommandPalette({\n  items,\n  onSelect,\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  shortcut = \"k\",\n  placeholder = \"Search…\",\n  label = \"Search\",\n  emptyMessage = (query) => `Nothing matches “${query}”.`,\n  onQueryChange,\n  loading = false,\n  loadingMessage = \"Searching…\",\n  filter = true,\n  rank = rankCommandItem,\n  maxResults = 8,\n  className,\n  ...dialogProps\n}: CommandPaletteProps) {\n  const listId = React.useId()\n  const dialogRef = React.useRef<HTMLDialogElement>(null)\n  const fieldRef = React.useRef<HTMLInputElement>(null)\n\n  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)\n  const [query, setQuery] = React.useState(\"\")\n  const [highlighted, setHighlighted] = React.useState(0)\n\n  const isOpen = open ?? uncontrolledOpen\n\n  const results = React.useMemo(() => {\n    // Already matched and ordered by whoever fetched them.\n    if (!filter) return items.slice(0, maxResults)\n\n    // The ranker is handed the query as typed, so one of your own can be\n    // case-sensitive if it wants to be. The built-in lowercases internally.\n    const trimmed = query.trim()\n\n    if (!trimmed) return items.slice(0, maxResults)\n\n    return items\n      .map((item) => ({ item, score: rank(item, trimmed) }))\n      .filter(\n        (entry): entry is { item: CommandItem; score: number } =>\n          typeof entry.score === \"number\" && Number.isFinite(entry.score)\n      )\n      .sort(\n        (a, b) => a.score - b.score || a.item.label.localeCompare(b.item.label)\n      )\n      .slice(0, maxResults)\n      .map(({ item }) => item)\n  }, [items, query, maxResults, filter, rank])\n\n  const active = results[Math.min(highlighted, results.length - 1)]\n\n  const setOpen = React.useCallback(\n    (next: boolean) => {\n      if (open === undefined) setUncontrolledOpen(next)\n      onOpenChange?.(next)\n    },\n    [open, onOpenChange]\n  )\n\n  // The dialog element owns its own visibility, so it is driven from state\n  // rather than rendered conditionally, which keeps focus handling native.\n  React.useEffect(() => {\n    const dialog = dialogRef.current\n    if (!dialog) return\n\n    if (isOpen && !dialog.open) {\n      setQuery(\"\")\n      setHighlighted(0)\n      dialog.showModal()\n      fieldRef.current?.focus()\n    } else if (!isOpen && dialog.open) {\n      dialog.close()\n    }\n  }, [isOpen])\n\n  React.useEffect(() => {\n    if (shortcut === false) return\n\n    const onKeyDown = (event: KeyboardEvent) => {\n      // Another palette, or the page itself, may already have claimed this\n      // chord. Without the check every listener bound to it opens, and the\n      // reader gets a stack of modals from one keypress.\n      if (event.defaultPrevented) return\n      if (event.key.toLowerCase() !== shortcut.toLowerCase()) return\n      if (!event.metaKey && !event.ctrlKey) return\n\n      event.preventDefault()\n      setOpen(!dialogRef.current?.open)\n    }\n\n    window.addEventListener(\"keydown\", onKeyDown)\n    return () => window.removeEventListener(\"keydown\", onKeyDown)\n  }, [shortcut, setOpen])\n\n  const choose = (item: CommandItem) => {\n    setOpen(false)\n    onSelect?.(item)\n  }\n\n  return (\n    <dialog\n      ref={dialogRef}\n      data-slot=\"command-palette\"\n      aria-label={label}\n      className={cn(\n        \"bg-transparent p-0 text-inherit backdrop:bg-black/45 backdrop:backdrop-blur-[3px] open:m-0 open:max-h-dvh open:w-dvw open:max-w-dvw\",\n        className\n      )}\n      onClose={() => setOpen(false)}\n      onClick={(event) => {\n        // A click on the backdrop lands on the dialog itself.\n        if (event.target === dialogRef.current) setOpen(false)\n      }}\n      {...dialogProps}\n    >\n      <div\n        data-slot=\"command-palette-panel\"\n        className=\"border-border bg-card text-card-foreground mx-auto mt-20 w-[calc(100vw-2rem)] max-w-[34rem] overflow-hidden rounded-2xl border shadow-2xl\"\n      >\n        <div\n          data-slot=\"command-palette-field\"\n          className=\"border-border text-muted-foreground flex items-center gap-2.5 border-b px-4\"\n        >\n          <Search aria-hidden=\"true\" size={15} className=\"shrink-0\" />\n          <input\n            ref={fieldRef}\n            type=\"text\"\n            role=\"combobox\"\n            aria-expanded\n            aria-controls={listId}\n            aria-autocomplete=\"list\"\n            aria-activedescendant={\n              active ? `${listId}-${active.id}` : undefined\n            }\n            aria-label={label}\n            autoComplete=\"off\"\n            placeholder={placeholder}\n            value={query}\n            className=\"text-foreground placeholder:text-muted-foreground min-h-13 w-full min-w-0 bg-transparent text-[0.95rem] outline-none\"\n            onChange={(event) => {\n              setQuery(event.target.value)\n              setHighlighted(0)\n              onQueryChange?.(event.target.value)\n            }}\n            onKeyDown={(event) => {\n              if (event.key === \"ArrowDown\") {\n                event.preventDefault()\n                setHighlighted((current) =>\n                  Math.min(current + 1, results.length - 1)\n                )\n              } else if (event.key === \"ArrowUp\") {\n                event.preventDefault()\n                setHighlighted((current) => Math.max(current - 1, 0))\n              } else if (event.key === \"Enter\" && active) {\n                event.preventDefault()\n                choose(active)\n              }\n            }}\n          />\n        </div>\n\n        <ul\n          id={listId}\n          role=\"listbox\"\n          aria-busy={loading || undefined}\n          aria-label={label}\n          className=\"m-0 grid max-h-[22rem] list-none overflow-y-auto overscroll-contain p-1.5\"\n        >\n          {results.map((item) => (\n            <li\n              key={item.id}\n              id={`${listId}-${item.id}`}\n              role=\"option\"\n              aria-selected={item.id === active?.id}\n              data-active={item.id === active?.id || undefined}\n              className=\"[&[data-active]>button]:bg-muted\"\n            >\n              <button\n                type=\"button\"\n                className=\"hover:bg-muted grid w-full grid-cols-[minmax(0,1fr)_auto] gap-x-3 gap-y-0.5 rounded-[calc(var(--radius)-0.15rem)] px-3 py-2 text-left transition-colors duration-150 motion-reduce:transition-none\"\n                onClick={() => choose(item)}\n              >\n                <span\n                  data-slot=\"command-palette-label\"\n                  className=\"truncate text-sm font-semibold\"\n                >\n                  {item.label}\n                </span>\n\n                {item.group ? (\n                  <span\n                    data-slot=\"command-palette-group\"\n                    className=\"text-muted-foreground justify-self-end font-[family-name:var(--font-mono),monospace] text-[0.65rem] tracking-[0.06em] uppercase\"\n                  >\n                    {item.group}\n                  </span>\n                ) : null}\n\n                {item.description ? (\n                  <span\n                    data-slot=\"command-palette-description\"\n                    className=\"text-muted-foreground col-span-full line-clamp-2 text-xs leading-relaxed\"\n                  >\n                    {item.description}\n                  </span>\n                ) : null}\n              </button>\n            </li>\n          ))}\n        </ul>\n\n        {/* While results are coming, say so rather than claiming there are\n            none: an empty list mid-flight is not an answer. */}\n        {loading ? (\n          <p\n            data-slot=\"command-palette-loading\"\n            role=\"status\"\n            className=\"text-muted-foreground px-4 py-5 text-sm\"\n          >\n            {loadingMessage}\n          </p>\n        ) : results.length === 0 ? (\n          <p\n            data-slot=\"command-palette-empty\"\n            role=\"status\"\n            className=\"text-muted-foreground px-4 py-5 text-sm\"\n          >\n            {emptyMessage(query.trim())}\n          </p>\n        ) : null}\n      </div>\n    </dialog>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "navigation",
    "search",
    "dialog",
    "command"
  ],
  "type": "registry:ui"
}