{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "accounting-core-gl",
    "finance",
    "accounting"
  ],
  "dependencies": [
    "@base-ui-components/react"
  ],
  "description": "Shows the normal balance. Blocks non-postable accounts.",
  "files": [
    {
      "content": "\"use client\";\n\nimport { Popover } from \"@base-ui-components/react/popover\";\nimport * as React from \"react\";\nimport type { Account } from \"@/lib/ledger/accounting.types\";\nimport { cn } from \"@/lib/ledger/cn\";\nimport { ChevronDownIcon, LockClosedIcon, MagnifyingGlassIcon } from \"@/components/ledger/icons\";\n\nexport interface AccountPickerProps {\n  accounts: Account[];\n  className?: string;\n  /** Inactive accounts are hidden by default but reachable by search. */\n  includeInactive?: boolean;\n  label?: string;\n  onValueChange: (account: Account) => void;\n  value: string | null;\n}\n\n/**\n * Shows the normal balance (Dr / Cr) — a user posting to an unfamiliar\n * account needs to know which side increases it.\n *\n * Non-postable rollup accounts are visibly disabled with the reason:\n * posting to a rollup is a data-integrity failure that surfaces months\n * later at close.\n */\nexport function AccountPicker({\n  accounts,\n  value,\n  onValueChange,\n  includeInactive = false,\n  className,\n  label = \"Account\",\n}: AccountPickerProps) {\n  const [open, setOpen] = React.useState(false);\n  const [query, setQuery] = React.useState(\"\");\n  const selected = accounts.find((a) => a.id === value);\n\n  const filtered = React.useMemo(() => {\n    const q = query.trim().toLowerCase();\n    return accounts.filter((a) => {\n      if (a.status === \"archived\") return false;\n      if (a.status === \"inactive\" && !includeInactive && !q) return false;\n      if (!q) return true;\n      return a.number.toLowerCase().startsWith(q) || a.name.toLowerCase().includes(q) || a.number.includes(q);\n    });\n  }, [accounts, query, includeInactive]);\n\n  const grouped = React.useMemo(() => {\n    const map = new Map<string, Account[]>();\n    for (const a of filtered) {\n      const key = `${a.type}${a.subType ? ` · ${a.subType}` : \"\"}`;\n      const list = map.get(key) ?? [];\n      list.push(a);\n      map.set(key, list);\n    }\n    return [...map.entries()];\n  }, [filtered]);\n\n  return (\n    <Popover.Root onOpenChange={setOpen} open={open}>\n      <Popover.Trigger\n        aria-label={\n          selected ? `${label}: ${selected.number} ${selected.name}` : `Choose an ${label.toLowerCase()}`\n        }\n        className={cn(\n          \"flex h-10 w-full items-center gap-2 rounded-sm border border-border bg-surface px-3\",\n          \"text-left transition-colors duration-1 ease-standard hover:border-border-strong\",\n          className,\n        )}\n      >\n        {selected ? (\n          <>\n            <span className=\"reference-numeric shrink-0 text-caption text-text-tertiary\">\n              {selected.number}\n            </span>\n            <span className=\"min-w-0 flex-1 truncate text-body\">{selected.name}</span>\n            <NormalBadge normal={selected.normalBalance} />\n          </>\n        ) : (\n          <span className=\"flex-1 text-body text-text-tertiary\">Choose an account</span>\n        )}\n        <ChevronDownIcon aria-hidden=\"true\" className=\"size-4 shrink-0 text-text-tertiary\" />\n      </Popover.Trigger>\n      <Popover.Portal>\n        <Popover.Positioner align=\"start\" className=\"z-dropdown\" sideOffset={8}>\n          <Popover.Popup\n            className=\"w-[var(--anchor-width)] min-w-[360px] overflow-hidden rounded-md border border-border bg-surface-raised shadow-elevation-3\"\n            style={{ transformOrigin: \"var(--transform-origin)\" }}\n          >\n            <div className=\"flex items-center gap-2 border-b border-border-subtle px-3\">\n              <MagnifyingGlassIcon aria-hidden=\"true\" className=\"size-4 text-text-tertiary\" />\n              <input\n                aria-label=\"Search accounts\"\n                autoFocus\n                className=\"reference-numeric h-10 flex-1 bg-transparent text-body outline-none\"\n                onChange={(e) => setQuery(e.target.value)}\n                placeholder=\"Account number or name\"\n                value={query}\n              />\n            </div>\n            <div className=\"max-h-[320px] overflow-y-auto py-1 scrollbar-thin\">\n              {grouped.map(([group, list]) => (\n                <div key={group}>\n                  <p className=\"px-3 py-1 text-caption capitalize text-text-tertiary\">{group}</p>\n                  {list.map((a) => (\n                    <button\n                      className={cn(\n                        \"flex w-full items-center gap-2 px-3 py-2 text-left\",\n                        \"transition-colors duration-1 ease-standard\",\n                        a.isPostable ? \"hover:bg-surface-hover\" : \"cursor-not-allowed text-text-disabled\",\n                        a.id === value && \"bg-surface-selected\",\n                      )}\n                      disabled={!a.isPostable}\n                      key={a.id}\n                      onClick={() => {\n                        onValueChange(a);\n                        setOpen(false);\n                      }}\n                      style={{ paddingLeft: 12 + (a.parentId ? 16 : 0) }}\n                      title={\n                        a.isPostable\n                          ? `Maps to ${a.statementMapping}`\n                          : \"Rollup account — entries cannot post here\"\n                      }\n                      type=\"button\"\n                    >\n                      <span className=\"reference-numeric w-12 shrink-0 text-caption text-text-tertiary\">\n                        {a.number}\n                      </span>\n                      <span className=\"min-w-0 flex-1 truncate text-body\">\n                        {a.name}\n                        {a.isContra ? (\n                          <span className=\"ml-1 text-caption text-text-tertiary\">(contra)</span>\n                        ) : null}\n                        {a.status === \"inactive\" ? (\n                          <span className=\"ml-1 text-caption text-text-tertiary\">· inactive</span>\n                        ) : null}\n                      </span>\n                      {a.isPostable ? (\n                        <NormalBadge normal={a.normalBalance} />\n                      ) : (\n                        <span className=\"flex items-center gap-1 text-caption text-text-tertiary\">\n                          <LockClosedIcon aria-hidden=\"true\" className=\"size-3\" />\n                          Rollup\n                        </span>\n                      )}\n                    </button>\n                  ))}\n                </div>\n              ))}\n              {filtered.length === 0 ? (\n                <p className=\"px-3 py-6 text-center text-body text-text-secondary\">\n                  No account matches “{query}”.\n                </p>\n              ) : null}\n            </div>\n          </Popover.Popup>\n        </Popover.Positioner>\n      </Popover.Portal>\n    </Popover.Root>\n  );\n}\n\nfunction NormalBadge({ normal }: { normal: \"debit\" | \"credit\" }) {\n  return (\n    <span className=\"relative shrink-0 rounded-xs bg-surface-sunken px-1.5 py-0.5 text-caption text-text-secondary\">\n      <span aria-hidden=\"true\">{normal === \"debit\" ? \"Dr\" : \"Cr\"}</span>\n      <span className=\"sr-only select-none\">normal balance {normal}</span>\n    </span>\n  );\n}\n",
      "path": "components/ledger/account-picker.tsx",
      "target": "components/ledger/account-picker.tsx",
      "type": "registry:component"
    }
  ],
  "name": "account-picker",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/accounting.types",
    "@lopifi/cn",
    "@lopifi/icons"
  ],
  "title": "AccountPicker",
  "type": "registry:component"
}
