{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "system-states",
    "finance",
    "accounting"
  ],
  "dependencies": [
    "@base-ui-components/react",
    "cmdk"
  ],
  "description": "Motionless. Money-aware search.",
  "files": [
    {
      "content": "\"use client\";\n\nimport { Dialog } from \"@base-ui-components/react/dialog\";\nimport { Command } from \"cmdk\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/ledger/cn\";\nimport { formatDateShort } from \"@/lib/ledger/fiscal-period\";\nimport { parseMoneyInput } from \"@/lib/ledger/format-money\";\nimport type { MoneyValue } from \"@/lib/ledger/money.types\";\nimport { Money } from \"@/components/ledger/money\";\n\nexport type CommandItem = {\n  id: string;\n  group: string;\n  label: string;\n  hint?: string;\n  amount?: MoneyValue;\n  date?: Date;\n  onSelect: () => void;\n};\n\nexport interface CommandPaletteProps {\n  items: CommandItem[];\n  onOpenChange: (open: boolean) => void;\n  open: boolean;\n}\n\n/**\n * **Zero animation. Opens instantly.** Used hundreds of times a day — this\n * is the canonical Tier 0 component and the clearest demonstration of the\n * frequency law.\n *\n * Money-aware search: `1240` finds transactions with that amount, `>1000`\n * filters by magnitude, and a date range parses.\n */\nexport function CommandPalette({ items, open, onOpenChange }: CommandPaletteProps) {\n  const [query, setQuery] = React.useState(\"\");\n\n  React.useEffect(() => {\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"k\" && (e.metaKey || e.ctrlKey)) {\n        e.preventDefault();\n        onOpenChange(!open);\n      }\n    };\n    document.addEventListener(\"keydown\", onKey);\n    return () => document.removeEventListener(\"keydown\", onKey);\n  }, [open, onOpenChange]);\n\n  const filtered = React.useMemo(() => moneyAwareFilter(items, query), [items, query]);\n  const groups = React.useMemo(() => {\n    const map = new Map<string, CommandItem[]>();\n    for (const item of filtered) {\n      const list = map.get(item.group) ?? [];\n      list.push(item);\n      map.set(item.group, list);\n    }\n    return [...map.entries()];\n  }, [filtered]);\n\n  return (\n    <Dialog.Root onOpenChange={onOpenChange} open={open}>\n      <Dialog.Portal>\n        {/* No animation classes anywhere in this component. Deliberate. */}\n        <Dialog.Backdrop className=\"fixed inset-0 z-overlay bg-[oklch(0.21_0.02_265/0.4)]\" />\n        <Dialog.Popup\n          className={cn(\n            \"fixed left-1/2 top-[15vh] z-dialog w-[calc(100vw-32px)] max-w-[560px] -translate-x-1/2\",\n            \"overflow-hidden rounded-md border border-border bg-surface-raised shadow-elevation-4\",\n          )}\n        >\n          <Dialog.Title className=\"sr-only\">Command palette</Dialog.Title>\n          <Dialog.Description className=\"sr-only\">\n            Search transactions, accounts and actions. Type an amount to find it, or a comparison like greater\n            than 1000.\n          </Dialog.Description>\n          <Command loop shouldFilter={false}>\n            <Command.Input\n              className=\"h-12 w-full border-b border-border-subtle bg-transparent px-4 text-body outline-none placeholder:text-text-disabled\"\n              onValueChange={setQuery}\n              placeholder=\"Search, or type an amount (1240) or a comparison (>1000)\"\n              value={query}\n            />\n            <Command.List className=\"max-h-[320px] overflow-y-auto p-2 scrollbar-thin\">\n              <Command.Empty className=\"px-3 py-6 text-center text-body text-text-secondary\">\n                Nothing matches “{query}”.\n              </Command.Empty>\n              {groups.map(([group, groupItems]) => (\n                <Command.Group\n                  className=\"[&_[cmdk-group-heading]]:px-3 [&_[cmdk-group-heading]]:py-1 [&_[cmdk-group-heading]]:text-caption [&_[cmdk-group-heading]]:text-text-tertiary\"\n                  heading={group}\n                  key={group}\n                >\n                  {groupItems.map((item) => (\n                    <Command.Item\n                      className=\"flex cursor-pointer items-center gap-3 rounded-xs px-3 py-2 text-body data-[selected=true]:bg-surface-hover\"\n                      key={item.id}\n                      onSelect={() => {\n                        item.onSelect();\n                        onOpenChange(false);\n                      }}\n                      value={item.id}\n                    >\n                      <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n                      {item.date ? (\n                        <span className=\"numeric shrink-0 text-caption text-text-tertiary\">\n                          {formatDateShort(item.date)}\n                        </span>\n                      ) : null}\n                      {item.amount ? <Money accounting value={item.amount} /> : null}\n                      {item.hint ? (\n                        <span className=\"shrink-0 text-caption text-text-tertiary\">{item.hint}</span>\n                      ) : null}\n                    </Command.Item>\n                  ))}\n                </Command.Group>\n              ))}\n            </Command.List>\n          </Command>\n        </Dialog.Popup>\n      </Dialog.Portal>\n    </Dialog.Root>\n  );\n}\n\n/** `>1000`, `<50`, `1240`, or plain text. */\nfunction moneyAwareFilter(items: CommandItem[], query: string): CommandItem[] {\n  const q = query.trim();\n  if (!q) return items;\n\n  const comparison = /^([<>])\\s*([\\d.,]+)$/.exec(q);\n  if (comparison) {\n    const threshold = parseMoneyInput(comparison[2], \"USD\");\n    if (threshold != null) {\n      return items.filter((i) => {\n        if (!i.amount) return false;\n        const magnitude = Math.abs(Number(i.amount.amount));\n        return comparison[1] === \">\" ? magnitude > threshold : magnitude < threshold;\n      });\n    }\n  }\n\n  const asAmount = /^[\\d.,]+$/.test(q) ? parseMoneyInput(q, \"USD\") : null;\n  const lower = q.toLowerCase();\n\n  return items.filter((i) => {\n    if (i.label.toLowerCase().includes(lower)) return true;\n    if (asAmount != null && i.amount) {\n      return Math.abs(Number(i.amount.amount)) === Math.abs(asAmount);\n    }\n    return false;\n  });\n}\n",
      "path": "components/ledger/command-palette.tsx",
      "target": "components/ledger/command-palette.tsx",
      "type": "registry:component"
    }
  ],
  "name": "command-palette",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/cn",
    "@lopifi/fiscal-period",
    "@lopifi/format-money",
    "@lopifi/money",
    "@lopifi/money.types"
  ],
  "title": "CommandPalette",
  "type": "registry:component"
}
