{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "px-command",
  "title": "Command menu",
  "description": "⌘K / Ctrl+K command palette with grouped commands, fuzzy search and a springing pixel highlight; focus stays in it while open.",
  "registryDependencies": [
    "https://elements.lumesec.ai/r/lumesec-theme.json",
    "https://elements.lumesec.ai/r/lumesec-fuzzy.json",
    "https://elements.lumesec.ai/r/lumesec-motion.json",
    "https://elements.lumesec.ai/r/lumesec-pixel-canvas.json",
    "https://elements.lumesec.ai/r/lumesec-roll-text.json",
    "https://elements.lumesec.ai/r/lumesec-use-controllable-state.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/components/lumesec/px-command.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { fuzzyMatch, FuzzyHighlight, type FuzzyMatch } from \"@/lib/lumesec/fuzzy\";\nimport { prefersReducedMotion, springEuler, type Spring } from \"@/lib/lumesec/motion\";\nimport { usePixelCanvas } from \"@/lib/lumesec/pixel-canvas\";\nimport { RollText } from \"@/lib/lumesec/roll-text\";\nimport { useControllableState } from \"@/lib/lumesec/use-controllable-state\";\nimport { cn } from \"@/lib/utils\";\n\n/** One command in the palette. */\nexport interface PxCommandItem {\n  /** What the command does; this text is searched. */\n  label: string;\n  /** Group heading the command is listed under while the query is empty. */\n  group: string;\n  /** Keyboard shortcut hint shown at the end of the row, such as `G P`. */\n  shortcut?: string;\n}\n\n/** Props of `PxCommand`. Other `div` props go to the root. */\nexport interface PxCommandProps extends Omit<React.ComponentProps<\"div\">, \"children\" | \"onChange\"> {\n  /** The commands, in display order. Consecutive commands with the same `group` share a heading. */\n  items: readonly PxCommandItem[];\n  /** Controlled open state. Use with `onOpenChange`. */\n  open?: boolean;\n  /** Initial open state when uncontrolled. */\n  defaultOpen?: boolean;\n  /** Called when the palette opens or closes. */\n  onOpenChange?: (open: boolean) => void;\n  /** Ctrl+K or ⌘K anywhere on the page opens and closes the palette. */\n  hotkey?: boolean;\n  /** Called when the user runs a command. The palette closes first. */\n  onCommand?: (item: PxCommandItem) => void;\n}\n\ninterface Result {\n  item: PxCommandItem;\n  match: FuzzyMatch;\n}\n\n/** Consecutive results under one heading; `group` is null while searching, when headings are hidden. */\ninterface Section {\n  group: string | null;\n  entries: { result: Result; index: number }[];\n}\n\nconst KBD =\n  \"whitespace-nowrap rounded-[5px] border border-border px-1.5 py-px font-mono text-[11px] text-muted-foreground\";\n\n/**\n * ⌘K / Ctrl+K command palette with grouped commands, fuzzy search and a springing pixel highlight. A\n * trigger button opens it, a status line under the trigger reports the command that ran, and focus stays\n * in the search input while it is open and returns where it was when it closes.\n *\n * @example\n * <PxCommand\n *   hotkey\n *   items={[\n *     { group: \"Actions\", label: \"New chat\", shortcut: \"N\" },\n *     { group: \"Go to\", label: \"Settings\", shortcut: \"G S\" },\n *   ]}\n *   onCommand={(item) => console.log(item.label)}\n * />\n */\nexport function PxCommand({\n  items,\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  hotkey = false,\n  onCommand,\n  className,\n  ...props\n}: PxCommandProps) {\n  const [open, setOpen] = useControllableState({ value: openProp, defaultValue: defaultOpen, onChange: onOpenChange });\n  const [lastRun, setLastRun] = React.useState<string | null>(null);\n  const shortcut = React.useSyncExternalStore(subscribeNever, platformShortcut, () => \"Ctrl K\");\n  const triggerRef = React.useRef<HTMLButtonElement>(null);\n\n  React.useEffect(() => {\n    if (!hotkey) return;\n    const onKeyDown = (event: KeyboardEvent) => {\n      if (!isPaletteShortcut(event) || event.defaultPrevented) return;\n      event.preventDefault();\n      // holding the keys must not flicker the palette open and closed\n      if (!event.repeat) setOpen((current) => !current);\n    };\n    document.addEventListener(\"keydown\", onKeyDown);\n    return () => document.removeEventListener(\"keydown\", onKeyDown);\n  }, [hotkey, setOpen]);\n\n  const run = (item: PxCommandItem) => {\n    setOpen(false);\n    setLastRun(item.label);\n    onCommand?.(item);\n  };\n\n  const status = lastRun !== null ? `Ran “${lastRun}”` : hotkey ? \"Press the shortcut anywhere on the page\" : \"\";\n\n  return (\n    <div\n      data-slot=\"px-command\"\n      data-state={open ? \"open\" : \"closed\"}\n      className={cn(\"block text-foreground\", className)}\n      {...props}\n    >\n      <button\n        ref={triggerRef}\n        type=\"button\"\n        aria-haspopup=\"dialog\"\n        aria-expanded={open}\n        aria-keyshortcuts={hotkey ? \"Control+K Meta+K\" : undefined}\n        className=\"flex h-10 w-full cursor-pointer items-center gap-2.5 rounded-[10px] border border-border bg-card pr-2.5 pl-[22px] text-left text-[13.5px] leading-normal text-muted-foreground outline-none select-none hover:border-[color-mix(in_srgb,var(--foreground)_25%,var(--border))] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lumesec focus-visible:outline-solid\"\n        onClick={() => setOpen(true)}\n      >\n        <span className=\"flex-1\">Search or jump to…</span>\n        {hotkey && <kbd className={KBD}>{shortcut}</kbd>}\n      </button>\n      <span className=\"mt-2 block text-[12px] leading-normal text-muted-foreground\">\n        <RollText text={status} aria-live=\"polite\" />\n      </span>\n      {open && <CommandDialog items={items} fallbackFocusRef={triggerRef} onClose={() => setOpen(false)} onRun={run} />}\n    </div>\n  );\n}\n\ninterface CommandDialogProps {\n  items: readonly PxCommandItem[];\n  /** Receives focus on close when the element focused before opening is gone (or was the page itself). */\n  fallbackFocusRef: React.RefObject<HTMLElement | null>;\n  onClose: () => void;\n  onRun: (item: PxCommandItem) => void;\n}\n\n/** The open palette. It mounts on open, so every opening starts with an empty query. */\nfunction CommandDialog({ items, fallbackFocusRef, onClose, onRun }: CommandDialogProps) {\n  const baseId = React.useId();\n  const listId = `${baseId}-list`;\n  const optionId = (index: number) => `${baseId}-option-${index}`;\n  const [query, setQuery] = React.useState(\"\");\n  const [highlighted, setActive] = React.useState(0);\n  const results = React.useMemo(() => search(items, query), [items, query]);\n  // the items can change while the palette is open\n  const active = Math.min(highlighted, results.length - 1);\n  const sections = groupResults(results, query);\n\n  const backdropRef = React.useRef<HTMLDivElement>(null);\n  const dialogRef = React.useRef<HTMLDivElement>(null);\n  const inputRef = React.useRef<HTMLInputElement>(null);\n  const listRef = React.useRef<HTMLDivElement>(null);\n  const optionRefs = React.useRef<(HTMLDivElement | null)[]>([]);\n  const highlight = React.useRef<Spring | null>(null);\n  /** Whether the current press started on the backdrop, so a drag out of the dialog does not close it. */\n  const pressOnBackdrop = React.useRef(false);\n\n  const { ref: canvasRef, redraw } = usePixelCanvas((frame) => {\n    const { palette, delta, width } = frame;\n    const option = optionRefs.current[active];\n    frame.clear();\n    if (!option) return false;\n    const target = option.offsetTop - 6;\n    const height = option.offsetHeight;\n    let state = highlight.current;\n    if (!state || prefersReducedMotion()) {\n      state = { position: target, velocity: 0 };\n      highlight.current = state;\n    } else {\n      springEuler(state, target, delta, 380, 0.8);\n    }\n    const top = state.position;\n    for (let y = top + 3; y < top + height - 2; y += 4) {\n      for (let x = 6; x < width - 3; x += 4) frame.dot(x, y, palette.accent, 0.1, 2.2);\n    }\n    for (let y = top + 9; y < top + height - 8; y += 3) frame.dot(2, y, palette.accent, 1, 2.2);\n    return Math.abs(target - state.position) > 0.3 || Math.abs(state.velocity) > 1;\n  });\n\n  // opening focuses the input and pops the dialog in; closing returns focus to where it was\n  React.useLayoutEffect(() => {\n    const dialog = dialogRef.current;\n    const returnTo = document.activeElement;\n    const fallback = fallbackFocusRef.current;\n    inputRef.current?.focus();\n    if (!prefersReducedMotion()) {\n      backdropRef.current?.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 180, easing: \"ease\" });\n      dialog?.animate(\n        [\n          { opacity: 0, transform: \"translateY(-8px) scale(.97)\" },\n          { opacity: 1, transform: \"none\" },\n        ],\n        {\n          duration: 220,\n          easing: \"cubic-bezier(.2,.9,.3,1.1)\",\n        },\n      );\n    }\n    return () => {\n      // a command that moved focus somewhere else keeps it there\n      const focused = document.activeElement;\n      if (focused && focused !== document.body && !dialog?.contains(focused)) return;\n      const target =\n        returnTo instanceof HTMLElement && returnTo !== document.body && returnTo.isConnected ? returnTo : fallback;\n      target?.focus();\n    };\n  }, [fallbackFocusRef]);\n\n  React.useLayoutEffect(() => {\n    const canvas = canvasRef.current;\n    const list = listRef.current;\n    // the canvas covers the whole list, so the highlight scrolls with the options\n    if (canvas && list) canvas.style.height = `${list.offsetHeight}px`;\n    optionRefs.current[active]?.scrollIntoView({ block: \"nearest\" });\n    redraw();\n  }, [results, active, canvasRef, redraw]);\n\n  const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n    // keys that confirm an IME composition belong to the composition\n    if (event.nativeEvent.isComposing) return;\n    if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") {\n      event.preventDefault();\n      const step = event.key === \"ArrowDown\" ? 1 : -1;\n      if (results.length) setActive((active + step + results.length) % results.length);\n    } else if (event.key === \"Enter\") {\n      event.preventDefault();\n      const result = results[active];\n      if (result) onRun(result.item);\n    } else if (event.key === \"Escape\") {\n      event.preventDefault();\n      onClose();\n    } else if (event.key === \"Tab\") {\n      // focus stays in the input while the palette is open\n      event.preventDefault();\n    }\n  };\n\n  const renderOption = ({ result: { item, match }, index }: Section[\"entries\"][number]) => (\n    <div\n      key={`${item.group}/${item.label}`}\n      ref={(node) => {\n        optionRefs.current[index] = node;\n      }}\n      id={optionId(index)}\n      role=\"option\"\n      aria-selected={index === active}\n      className=\"relative flex h-[38px] cursor-pointer items-center rounded-lg pr-2.5 pl-3.5 text-[14px] leading-normal\"\n      onPointerMove={() => {\n        if (active !== index) setActive(index);\n      }}\n      onClick={() => onRun(item)}\n    >\n      <span>\n        <FuzzyHighlight\n          text={item.label}\n          indices={match.indices}\n          markClassName=\"bg-transparent font-semibold text-lumesec\"\n        />\n      </span>\n      {item.shortcut && <kbd className={cn(KBD, \"ml-auto\")}>{item.shortcut}</kbd>}\n    </div>\n  );\n\n  return (\n    <div\n      ref={backdropRef}\n      className=\"fixed inset-0 z-[1000] grid items-start justify-items-center bg-[rgb(0_0_0/0.35)] px-4 pt-[max(12vh,24px)] pb-4\"\n      onPointerDown={(event) => {\n        pressOnBackdrop.current = event.target === event.currentTarget;\n      }}\n      onClick={(event) => {\n        // closing on click rather than on press keeps the click from landing on the page underneath\n        if (pressOnBackdrop.current && event.target === event.currentTarget) onClose();\n        pressOnBackdrop.current = false;\n      }}\n    >\n      <div\n        ref={dialogRef}\n        role=\"dialog\"\n        aria-modal=\"true\"\n        aria-label=\"Command menu\"\n        tabIndex={-1}\n        // a press on a non-focusable part of the dialog lands here; hand focus back to the input so keys keep working\n        onFocus={(event) => {\n          if (event.target === event.currentTarget) inputRef.current?.focus();\n        }}\n        className=\"relative w-[min(560px,100%)] overflow-hidden rounded-[14px] border border-border bg-card text-foreground shadow-[0_30px_60px_-20px_rgba(0,0,0,.55)] outline-none\"\n      >\n        <div className=\"flex items-center gap-2.5 border-b border-border px-3.5\">\n          <svg\n            viewBox=\"0 0 16 16\"\n            aria-hidden=\"true\"\n            className=\"size-4 flex-none fill-none stroke-muted-foreground stroke-[1.7] [stroke-linecap:round]\"\n          >\n            <circle cx=\"7\" cy=\"7\" r=\"4.6\" />\n            <path d=\"M10.5 10.5l3.5 3.5\" />\n          </svg>\n          <input\n            ref={inputRef}\n            type=\"text\"\n            role=\"combobox\"\n            aria-expanded=\"true\"\n            aria-controls={listId}\n            aria-autocomplete=\"list\"\n            aria-activedescendant={active >= 0 ? optionId(active) : undefined}\n            aria-label=\"Command\"\n            placeholder=\"Type a command or search\"\n            autoComplete=\"off\"\n            spellCheck={false}\n            value={query}\n            className=\"h-[52px] min-w-0 flex-1 border-0 bg-transparent px-0.5 py-px font-[inherit] text-[15px] text-inherit outline-none placeholder:text-muted-foreground\"\n            onChange={(event) => {\n              setQuery(event.target.value);\n              setActive(0);\n            }}\n            onKeyDown={onKeyDown}\n          />\n          <kbd className={KBD}>Esc</kbd>\n        </div>\n        <div className=\"relative max-h-[min(360px,60vh)] overflow-auto p-1.5\">\n          <canvas\n            ref={canvasRef}\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute top-1.5 left-1.5 block w-[calc(100%-12px)] [image-rendering:pixelated]\"\n          />\n          <div ref={listRef} id={listId} role=\"listbox\" aria-label=\"Commands\">\n            {sections.map((section, sectionIndex) => {\n              if (section.group === null)\n                return <React.Fragment key=\"matches\">{section.entries.map(renderOption)}</React.Fragment>;\n              const headingId = `${baseId}-group-${sectionIndex}`;\n              return (\n                <div key={`${sectionIndex}:${section.group}`} role=\"group\" aria-labelledby={headingId}>\n                  <div\n                    id={headingId}\n                    role=\"presentation\"\n                    className=\"px-2.5 pt-2.5 pb-1 font-mono text-[11px] leading-normal tracking-[.06em] text-muted-foreground uppercase\"\n                  >\n                    {section.group}\n                  </div>\n                  {section.entries.map(renderOption)}\n                </div>\n              );\n            })}\n          </div>\n          {results.length === 0 && (\n            <div className=\"px-3 py-[18px] text-[13.5px] leading-normal text-muted-foreground\">\n              No commands match. Try another word.\n            </div>\n          )}\n        </div>\n        <div className=\"flex gap-3.5 border-t border-border px-3.5 py-2 text-[11.5px] leading-normal text-muted-foreground\">\n          <span>↑ ↓ to move</span>\n          <span>↵ to run</span>\n          <span>Esc to close</span>\n        </div>\n      </div>\n    </div>\n  );\n}\n\n/** Matching commands; best match first while there is a query, list order otherwise. */\nfunction search(items: readonly PxCommandItem[], query: string): Result[] {\n  const results: Result[] = [];\n  for (const item of items) {\n    const match = fuzzyMatch(query, item.label);\n    if (match) results.push({ item, match });\n  }\n  if (query) results.sort((a, b) => b.match.score - a.match.score);\n  return results;\n}\n\n/** Runs of consecutive results with the same group while the query is empty; one flat run while searching. */\nfunction groupResults(results: readonly Result[], query: string): Section[] {\n  if (query)\n    return results.length ? [{ group: null, entries: results.map((result, index) => ({ result, index })) }] : [];\n  const sections: Section[] = [];\n  results.forEach((result, index) => {\n    const last = sections[sections.length - 1];\n    if (last && last.group === result.item.group) last.entries.push({ result, index });\n    else sections.push({ group: result.item.group, entries: [{ result, index }] });\n  });\n  return sections;\n}\n\n/** Ctrl+K or ⌘K, without Shift or Alt (Ctrl+Shift+K is a browser shortcut). */\nfunction isPaletteShortcut(event: KeyboardEvent) {\n  return (event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey && event.key.toLowerCase() === \"k\";\n}\n\n/** The platform never changes while the page is open. */\nconst subscribeNever = () => () => {};\n\n/** `⌘ K` on Apple platforms, `Ctrl K` elsewhere. */\nconst platformShortcut = () => (/Mac|iPhone|iPad/.test(navigator.platform) ? \"⌘ K\" : \"Ctrl K\");\n",
      "type": "registry:component",
      "target": "@components/lumesec/px-command.tsx"
    }
  ],
  "meta": {
    "tag": "px-command",
    "collection": "pixel-ui",
    "react": "PxCommand",
    "kind": "react"
  },
  "docs": "Command menu docs: https://elements.lumesec.ai/components/pixel-ui/command",
  "categories": [
    "pixel-ui",
    "navigation-data"
  ],
  "type": "registry:component"
}