{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "command-palette",
  "type": "registry:ui",
  "title": "Command palette",
  "description": "⌘K: a 560 frost plate with a 44 field well, sections of 36 rows with matches marked, a raised selected row with a green bar, destructive rows in red, and a footer of keycaps. Hover moves the selection; ↩ runs, ⇧↩ pins. Built on Base UI Dialog and an inline Combobox.",
  "dependencies": [
    "@base-ui/react@^1.8.0"
  ],
  "registryDependencies": [
    "https://metalui.dev/r/tokens.json"
  ],
  "files": [
    {
      "path": "packages/metalui/src/components/command-palette/command-palette.tsx",
      "type": "registry:ui",
      "target": "components/metalui/command-palette/command-palette.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport { Dialog } from '@base-ui/react/dialog';\nimport { Combobox } from '@base-ui/react/combobox';\nimport { Kbd } from '../kbd/kbd';\n\n/* ─────────────────────────────────────────────────────────\n * COMMAND PALETTE (the reference's openPalette)\n * Base UI Dialog around an inline Combobox (the list is always open inside the plate).\n *   open      rises one nest (y −6, scale .985) on the surface spring over a page scrim at .25;\n *             focus lands in the field with the query already there (initial query)\n *   type      the list refilters at once; the first row is selected again (demo: idx = 0)\n *   ↑ ↓       move the selection; hover moves it too (demo: mousemove sets idx)\n *   ↩         runs the selected row and closes; ⇧↩ runs it pinned (a lens kept as a region)\n *   ⎋ / click outside   close at once, nothing runs\n *   selected  a raised cap with a 2.5 green-deep bar; instant (the list is scanned, not watched)\n * ───────────────────────────────────────────────────────── */\n\n/* Every value is the palette group. The plate rises one nest from its trigger on the surface spring over\n * a page scrim, and closes on release. */\nconst SCRIM = 'mu-palette-scrim fixed inset-0 palette-scrim transition-opacity duration-surface ease-surface data-starting-style:opacity-0 data-ending-style:opacity-0 data-ending-style:duration-release data-ending-style:ease-release';\nconst POPUP = 'mu-palette fixed palette-at p-palette-pad rounded-card outline-none material-frost-plate palette-motion data-starting-style:palette-away data-ending-style:palette-away data-ending-style:palette-motion-release';\n/* The field: a 44 well in the content role, caret green-deep. */\nconst FIELD = 'mu-palette-field flex items-center gap-palette-field-gap h-palette-field-height pl-palette-field-pad-start pr-palette-field-pad-end rounded-palette-field-radius material-well cursor-text';\nconst FIELD_GLYPH = 'mu-palette-field-glyph inline-grid flex-none text-ink3 [&>svg]:size-palette-field-glyph';\nconst INPUT = 'mu-palette-input flex-1 min-w-0 p-0 border-0 outline-none bg-transparent text-ink caret-green-deep placeholder:text-ink3 type-content';\n/* The list: sections of 36 rows; scrolls past 52 % of the window. The selected row's bar sits 2 outside\n * the row: room for it inside the scroll clip. */\nconst LIST = 'mu-palette-list palette-list-max overflow-auto mx-palette-bar-left pt-palette-list-pad-top px-palette-bar-outset pb-palette-list-pad-bottom scroll-py-palette-list-pad-top scroll-px-0 outline-none empty:hidden';\nconst ENG = 'mu-palette-eng palette-eng';\nconst SEC = 'mu-palette-sec flex justify-between pt-palette-sec-pad-top px-palette-row-pad pb-palette-sec-pad-bottom type-label';\n/* Selected: a raised cap with a green-deep bar at the left. Instant: the list is scanned, not watched. */\nconst ROW = 'mu-palette-row group/prow relative flex items-center gap-palette-row-gap h-palette-row-height px-palette-row-pad rounded-row text-ink cursor-pointer outline-none select-none type-ui data-highlighted:palette-row-on data-highlighted:before:palette-row-bar data-danger:text-red data-disabled:opacity-40 data-disabled:cursor-default';\nconst ROW_GLYPH = 'mu-palette-row-glyph inline-grid flex-none text-ink2 group-data-danger/prow:text-red [&>svg]:size-palette-row-glyph';\nconst ROW_TEXT = 'mu-palette-row-text min-w-0 overflow-hidden text-ellipsis whitespace-nowrap';\nconst ROW_HINT = 'mu-palette-row-hint flex flex-none items-center gap-palette-hint-gap ml-auto';\nconst HINT_TEXT = 'mu-palette-eng palette-eng type-label';\nconst MARK = 'mu-palette-mark palette-mark';\nconst EMPTY = 'mu-palette-empty type-ui not-empty:py-palette-empty-pad-y not-empty:px-palette-row-pad not-empty:text-ink3';\n/* The footer: keys above an engraved rule. */\nconst FOOT = 'mu-palette-foot flex items-center gap-palette-foot-gap mt-palette-foot-margin-top pt-palette-foot-pad-top px-palette-row-pad pb-palette-foot-pad-bottom palette-foot-rule type-label';\nconst FOOT_KEYS = 'flex items-center gap-palette-foot-key-gap';\nconst STATUS = 'mu-palette-status palette-eng flex items-center gap-palette-foot-key-gap ml-auto';\n\n/** The palette's part classes, for stills of it outside its dialog (docs, previews). */\nexport const paletteParts = { POPUP, FIELD, FIELD_GLYPH, INPUT, LIST, ENG, SEC, ROW, ROW_GLYPH, ROW_TEXT, ROW_HINT, MARK, FOOT, FOOT_KEYS } as const;\n\nexport interface CommandPaletteItem {\n  /** Unique within the palette. */\n  id: string;\n  /** What the row says. Matches of the query are marked in it. */\n  label: string;\n  /** The section heading this row sits under: LENS, LENSES, BLOCKS, ACTIONS. Rows of one section must be adjacent. */\n  section: string;\n  /** The 14 glyph at the left, e.g. <LensIcon size={14} />. */\n  icon?: React.ReactNode;\n  /** The right side: a keycap (<Kbd>⌘Z</Kbd>) or a readout (\"8:52\", \"RULES\"). A string renders as an engraving. */\n  hint?: React.ReactNode;\n  /** More words the filter matches but the row does not show. */\n  keywords?: string[];\n  /** A destructive action: the row is red. */\n  danger?: boolean;\n}\n\nexport interface CommandPaletteProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  /** The rows. By default the palette filters them by the query; pass `filter={false}` when the host filters. */\n  items: CommandPaletteItem[];\n  /** Runs a row. `pin` is true for ⇧↩. The palette closes first. */\n  onRun: (item: CommandPaletteItem, options: { pin: boolean }) => void;\n  /** The query, controlled. The host usually listens to add rows that depend on it (“See “q””). */\n  query?: string;\n  onQueryChange?: (query: string) => void;\n  /** The query the palette opens with (uncontrolled). */\n  defaultQuery?: string;\n  /** false: show `items` as given (the host filtered them). */\n  filter?: boolean;\n  placeholder?: string;\n  /** The 15 search glyph in the field. */\n  icon?: React.ReactNode;\n  /** Right of the footer keys: where answers come from, e.g. \"NATURAL LANGUAGE\". */\n  status?: string;\n  /** Whether ⇧↩ pins (shows in the footer). */\n  pinnable?: boolean;\n  /** What the list says when nothing matches. */\n  empty?: React.ReactNode;\n  'aria-label'?: string;\n}\n\nfunction escape(s: string) {\n  return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/** The label with each query word marked: weight 650, a 1.5 green underline. */\nfunction Marked({ text, query }: { text: string; query: string }) {\n  const words = query.trim().split(/\\s+/).filter(Boolean);\n  if (!words.length) return <>{text}</>;\n  const parts = text.split(new RegExp(`(${words.map(escape).join('|')})`, 'gi'));\n  return <>{parts.map((p, i) => (i % 2 ? <mark key={i} className={MARK}>{p}</mark> : p))}</>;\n}\n\nfunction matches(item: CommandPaletteItem, query: string) {\n  const q = query.trim().toLowerCase();\n  if (!q) return true;\n  const hay = [item.label, ...(item.keywords ?? [])].join(' ').toLowerCase();\n  return q.split(/\\s+/).every((w) => hay.includes(w));\n}\n\n/** ⌘K: lenses and actions. One field, sections of rows, a footer of keys. */\nexport function CommandPalette({\n  open,\n  onOpenChange,\n  items,\n  onRun,\n  query: queryProp,\n  onQueryChange,\n  defaultQuery = '',\n  filter = true,\n  placeholder = 'Try “open tasks about the poster”',\n  icon,\n  status,\n  pinnable = true,\n  empty = 'Nothing matches',\n  'aria-label': ariaLabel = 'Lenses and actions',\n}: CommandPaletteProps) {\n  const [own, setOwn] = React.useState(defaultQuery);\n  const query = queryProp ?? own;\n  const setQuery = (q: string) => {\n    if (queryProp === undefined) setOwn(q);\n    onQueryChange?.(q);\n  };\n  React.useEffect(() => {\n    if (open && queryProp === undefined) setOwn(defaultQuery);\n  }, [open]); // eslint-disable-line react-hooks/exhaustive-deps\n\n  const shown = React.useMemo(() => (filter ? items.filter((it) => matches(it, query)) : items), [items, query, filter]);\n  const sections = React.useMemo(() => {\n    const out: { name: string; rows: { item: CommandPaletteItem; index: number }[] }[] = [];\n    shown.forEach((item, index) => {\n      const last = out[out.length - 1];\n      if (last && last.name === item.section) last.rows.push({ item, index });\n      else out.push({ name: item.section, rows: [{ item, index }] });\n    });\n    return out;\n  }, [shown]);\n\n  const highlighted = React.useRef<CommandPaletteItem | undefined>(undefined);\n  const run = (item: CommandPaletteItem | undefined, pin: boolean) => {\n    if (!item) return;\n    onOpenChange(false);\n    onRun(item, { pin });\n  };\n\n  return (\n    <Dialog.Root open={open} onOpenChange={(o) => onOpenChange(o)}>\n      <Dialog.Portal>\n        <Dialog.Backdrop className={SCRIM} />\n        <Dialog.Popup aria-label={ariaLabel} className={POPUP}>\n          <Combobox.Root\n            inline\n            open\n            items={shown}\n            filteredItems={shown}\n            itemToStringLabel={(it: CommandPaletteItem) => it.label}\n            isItemEqualToValue={(a: CommandPaletteItem, b: CommandPaletteItem) => a.id === b.id}\n            highlightItemOnHover\n            // The first row is selected from the moment the palette opens, and the selection stays when\n            // the pointer leaves (the demo's idx). Root's types narrow these; it forwards them unchanged.\n            {...({ autoHighlight: 'always', keepHighlight: true } as object)}\n            inputValue={query}\n            onInputValueChange={(v) => setQuery(v)}\n            value={null}\n            onValueChange={(v) => run(v ?? undefined, false)}\n            onItemHighlighted={(v) => { highlighted.current = v; }}\n          >\n            <label className={FIELD}>\n              {icon && <span aria-hidden className={FIELD_GLYPH}>{icon}</span>}\n              <Combobox.Input\n                className={INPUT}\n                placeholder={placeholder}\n                autoComplete=\"off\"\n                spellCheck={false}\n                aria-label={ariaLabel}\n                onKeyDown={(e) => {\n                  // ⇧↩ runs pinned; Base UI handles plain ↩ through onValueChange.\n                  if (e.key === 'Enter' && e.shiftKey && pinnable) {\n                    e.preventDefault();\n                    e.stopPropagation();\n                    run(highlighted.current ?? shown[0], true);\n                  }\n                }}\n              />\n              <Kbd size=\"small\" label=\"Escape closes\">⎋</Kbd>\n            </label>\n            <Combobox.List className={LIST}>\n              {sections.map((sec) => (\n                <Combobox.Group key={sec.name} className=\"mu-palette-group\">\n                  <Combobox.GroupLabel className={SEC}>\n                    <span className={ENG}>{sec.name}</span>\n                    <span className={ENG} aria-hidden>{sec.rows.length}</span>\n                  </Combobox.GroupLabel>\n                  {sec.rows.map(({ item, index }) => (\n                    <Combobox.Item key={item.id} value={item} index={index} className={`${ROW} mu-icon-trigger`} data-danger={item.danger ? '' : undefined}>\n                      {item.icon && <span aria-hidden className={ROW_GLYPH}>{item.icon}</span>}\n                      <span className={ROW_TEXT}><Marked text={item.label} query={query} /></span>\n                      {item.hint != null && (\n                        <span className={ROW_HINT}>\n                          {typeof item.hint === 'string' ? <span className={HINT_TEXT}>{item.hint}</span> : item.hint}\n                        </span>\n                      )}\n                    </Combobox.Item>\n                  ))}\n                </Combobox.Group>\n              ))}\n            </Combobox.List>\n            <Combobox.Empty className={EMPTY}>{empty}</Combobox.Empty>\n            <div className={FOOT} aria-hidden>\n              <span className={FOOT_KEYS}><Kbd size=\"small\">↑</Kbd><Kbd size=\"small\">↓</Kbd><span className={ENG}>MOVE</span></span>\n              <span className={FOOT_KEYS}><Kbd size=\"small\">↩</Kbd><span className={ENG}>OPEN</span></span>\n              {pinnable && <span className={FOOT_KEYS}><Kbd size=\"small\">⇧↩</Kbd><span className={ENG}>PIN</span></span>}\n              {status && <span className={STATUS}>{status}</span>}\n            </div>\n          </Combobox.Root>\n        </Dialog.Popup>\n      </Dialog.Portal>\n    </Dialog.Root>\n  );\n}\n"
    }
  ],
  "docs": "Agent guide: https://metalui.dev/r/command-palette.md. SwiftUI: MetalCommandPalette, MetalCommandPaletteItem in the MetalUI Swift package."
}
