{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "ledger-tables",
    "finance",
    "accounting"
  ],
  "dependencies": [],
  "description": "URL-param-backed filters with resolved-value chips.",
  "files": [
    {
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/ledger/cn\";\nimport { Button } from \"@/components/ledger/button\";\nimport { FunnelIcon, XMarkIcon } from \"@/components/ledger/icons\";\n\nexport type FilterChip = {\n  key: string;\n  label: string;\n  /** The resolved value — \"1–30 Sep 2026\", never \"Last 30 days\". */\n  value: string;\n};\n\nexport interface FilterBarProps {\n  children?: React.ReactNode;\n  chips: FilterChip[];\n  className?: string;\n  onClearAll: () => void;\n  onRemove: (key: string) => void;\n  resultCount: number;\n}\n\n/**\n * URL-param-backed, so a filtered view is shareable and survives a refresh.\n * Chips show resolved values. Clear-all is always present. Filter state\n * changes are instant — no animation.\n */\nexport function FilterBar({ chips, onRemove, onClearAll, resultCount, children, className }: FilterBarProps) {\n  return (\n    <div className={cn(\"flex flex-wrap items-center gap-2\", className)}>\n      <FunnelIcon aria-hidden=\"true\" className=\"size-4 shrink-0 text-text-tertiary\" />\n      {children}\n      {chips.map((chip) => (\n        <span\n          className=\"inline-flex h-8 items-center gap-1.5 rounded-sm bg-surface-sunken pl-3 pr-1 text-label\"\n          key={chip.key}\n        >\n          <span className=\"text-text-secondary\">{chip.label}:</span>\n          <span className=\"font-medium\">{chip.value}</span>\n          <button\n            aria-label={`Remove filter ${chip.label} ${chip.value}`}\n            className=\"press grid size-6 place-items-center rounded-xs text-text-tertiary hover:bg-surface-hover\"\n            onClick={() => onRemove(chip.key)}\n            type=\"button\"\n          >\n            <XMarkIcon aria-hidden=\"true\" className=\"size-3\" />\n          </button>\n        </span>\n      ))}\n      {chips.length > 0 ? (\n        <Button onClick={onClearAll} size=\"sm\" variant=\"ghost\">\n          Clear all\n        </Button>\n      ) : null}\n      <span aria-live=\"polite\" className=\"numeric ml-auto text-caption text-text-secondary\">\n        {resultCount.toLocaleString()} results\n      </span>\n    </div>\n  );\n}\n\n/** URL-backed filter state. A shared link opens the same view. */\nexport function useUrlFilters(): {\n  filters: Record<string, string>;\n  set: (key: string, value: string | null) => void;\n  clear: () => void;\n} {\n  const [filters, setFilters] = React.useState<Record<string, string>>({});\n\n  React.useEffect(() => {\n    const params = new URLSearchParams(window.location.search);\n    const next: Record<string, string> = {};\n    params.forEach((v, k) => {\n      next[k] = v;\n    });\n    setFilters(next);\n  }, []);\n\n  const sync = React.useCallback((next: Record<string, string>) => {\n    setFilters(next);\n    const params = new URLSearchParams(next);\n    const query = params.toString();\n    window.history.replaceState(\n      null,\n      \"\",\n      query ? `${window.location.pathname}?${query}` : window.location.pathname,\n    );\n  }, []);\n\n  return {\n    clear: () => sync({}),\n    filters,\n    set: (key, value) => {\n      const next = { ...filters };\n      if (value === null) delete next[key];\n      else next[key] = value;\n      sync(next);\n    },\n  };\n}\n",
      "path": "components/ledger/filter-bar.tsx",
      "target": "components/ledger/filter-bar.tsx",
      "type": "registry:component"
    }
  ],
  "name": "filter-bar",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/button",
    "@lopifi/cn",
    "@lopifi/icons"
  ],
  "title": "FilterBar",
  "type": "registry:component"
}
