{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "ledger-tables",
    "finance",
    "accounting"
  ],
  "dependencies": [
    "@base-ui-components/react"
  ],
  "description": "One cut through a table, with subtotals per group.",
  "files": [
    {
      "content": "\"use client\";\n\nimport { Popover } from \"@base-ui-components/react/popover\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/ledger/cn\";\nimport { CheckIcon, Squares2X2Icon } from \"@/components/ledger/icons\";\n\nexport type GroupOption<K extends string = string> = {\n  key: K;\n  label: string;\n  /** Shown under the label. Say what the subtotal will mean. */\n  hint?: string;\n};\n\nexport interface GroupPickerProps<K extends string = string> {\n  className?: string;\n  /** What the ungrouped state is called. \"No grouping\" for a transaction list. */\n  noneLabel?: string;\n  onValueChange: (key: K | undefined) => void;\n  options: GroupOption<K>[];\n  /** `undefined` is \"no grouping\", and it is a real, selectable choice. */\n  value?: K;\n}\n\n/**\n * What the table is cut by.\n *\n * A single choice, not a stack of them. Nested grouping — category inside\n * entity inside month — is the feature every table like this grows and the\n * one nobody uses twice: three levels of header rows leave about four data\n * rows on screen, and the answer it gives is a pivot table, which is a\n * different tool with a different layout. One cut, with subtotals, is the\n * question people actually ask.\n *\n * \"No grouping\" sits in the list rather than being a separate clear button,\n * because ungrouped is a view like the others and the reader returns to it\n * constantly. It is first, and it is where the tick starts.\n *\n * The trigger says what is in force rather than \"Group\" — a control whose\n * label never changes is a control the reader has to open to read.\n */\nexport function GroupPicker<K extends string = string>({\n  options,\n  value,\n  onValueChange,\n  noneLabel = \"No grouping\",\n  className,\n}: GroupPickerProps<K>) {\n  const [open, setOpen] = React.useState(false);\n  const active = options.find((o) => o.key === value);\n\n  const choose = (key: K | undefined) => {\n    onValueChange(key);\n    setOpen(false);\n  };\n\n  const row = (key: K | undefined, label: string, hint?: string) => {\n    const selected = key === value;\n    return (\n      <li key={key ?? \"__none\"}>\n        <button\n          aria-pressed={selected}\n          className={cn(\n            \"flex w-full items-start gap-2 rounded-xs px-2 py-2 text-left\",\n            \"transition-colors duration-1 ease-standard hover:bg-surface-hover\",\n            selected && \"bg-surface-selected\",\n          )}\n          onClick={() => choose(key)}\n          type=\"button\"\n        >\n          {/* The tick keeps its column whether or not it is drawn, so the\n              labels do not shift by 20px as the selection moves. */}\n          <span className=\"grid size-4 shrink-0 place-items-center pt-0.5\">\n            {selected ? <CheckIcon aria-hidden=\"true\" className=\"size-3.5 text-accent\" /> : null}\n          </span>\n          <span className=\"flex-1\">\n            <span className=\"block text-body\">{label}</span>\n            {hint ? <span className=\"block text-caption text-text-secondary\">{hint}</span> : null}\n          </span>\n        </button>\n      </li>\n    );\n  };\n\n  return (\n    <Popover.Root onOpenChange={setOpen} open={open}>\n      <Popover.Trigger\n        aria-label={`Grouping: ${active?.label ?? noneLabel}`}\n        className={cn(\n          \"inline-flex h-8 items-center gap-2 rounded-sm border border-border bg-surface px-3 text-label font-medium\",\n          \"transition-colors duration-1 ease-standard hover:bg-surface-hover\",\n          className,\n        )}\n      >\n        <Squares2X2Icon aria-hidden=\"true\" className=\"size-3.5 text-text-tertiary\" />\n        {active ? `Grouped by ${active.label.toLowerCase()}` : \"Group\"}\n      </Popover.Trigger>\n      <Popover.Portal>\n        <Popover.Positioner align=\"end\" className=\"z-dropdown\" sideOffset={8}>\n          <Popover.Popup\n            className=\"w-[280px] rounded-md border border-border bg-surface-raised p-2 shadow-elevation-3\"\n            style={{ transformOrigin: \"var(--transform-origin)\" }}\n          >\n            <ul>\n              {row(undefined, noneLabel)}\n              {options.map((o) => row(o.key, o.label, o.hint))}\n            </ul>\n            <p className=\"border-t border-border-subtle px-2 pt-2 text-caption text-text-secondary\">\n              Each group subtotals the rows under it — not the filtered set behind them.\n            </p>\n          </Popover.Popup>\n        </Popover.Positioner>\n      </Popover.Portal>\n    </Popover.Root>\n  );\n}\n",
      "path": "components/ledger/group-picker.tsx",
      "target": "components/ledger/group-picker.tsx",
      "type": "registry:component"
    }
  ],
  "name": "group-picker",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/cn",
    "@lopifi/icons"
  ],
  "title": "GroupPicker",
  "type": "registry:component"
}
