{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "filter-menu",
  "title": "Filter Menu",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "Menu de facetas que não conhece o domínio: grupos múltiplos ou excludentes, e o contador conta grupos, não opções.",
  "dependencies": [
    "@radix-ui/react-dropdown-menu",
    "react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/visant/ui/filter-menu.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as DropdownMenu from \"@radix-ui/react-dropdown-menu\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * FilterMenu — o menu de facetas atrás de um gatilho, com contagem.\n *\n * Colhido de uma tela de demandas onde o menu sabia demais: conhecia o tipo\n * `FilterState`, a tabela de prioridades 0–4 em português e um `dateFilter.range`\n * com quatro janelas fixas. Funcionava num produto e em nenhum outro.\n *\n * Aqui o menu não sabe o que é status, prioridade ou período — só sabe desenhar\n * **grupos de opções**. O domínio entra por `groups`, e por isso a mesma peça\n * serve uma loja, um CRM e um board.\n *\n * ── Decisões que não são cosméticas ─────────────────────────────────────────\n *\n * 1. **O contador conta GRUPOS, não opções.** Três status marcados são um eixo\n *    de filtragem, não três. Contar opções faz o selo dizer \"7\" e não informar\n *    nada sobre o quanto a lista foi estreitada.\n * 2. **`mode: \"single\"` vira `RadioGroup` de verdade**, não um checkbox que se\n *    comporta como rádio. Um período é excludente; anunciá-lo como caixa de\n *    marcar faz o leitor de tela prometer múltipla escolha que a UI não cumpre.\n * 3. **`dot` é cor do DADO, não do componente.** A bolinha do status vem da API\n *    e entra por `style`. Nenhum hexadecimal mora neste arquivo — é o que deixa\n *    o menu passar no portão de cor do registry e trocar de marca sem tocar aqui.\n * 4. **\"Limpar\" só existe quando há o que limpar.** Uma ação permanentemente\n *    desabilitada é ruído que ensina a ignorar aquele canto do menu.\n *\n * Estado vazio: sem `groups` com opções, o menu abre dizendo que não há filtro —\n * em vez de uma caixa vazia que parece bug.\n */\n\nexport interface FilterOption {\n  id: string;\n  label: string;\n  /** Cor vinda do dado (status, etiqueta). Renderiza uma bolinha antes do rótulo. */\n  dot?: string;\n}\n\nexport interface FilterGroup {\n  key: string;\n  label: string;\n  /** `multi` (padrão) marca várias; `single` é excludente. */\n  mode?: \"multi\" | \"single\";\n  options: FilterOption[];\n}\n\nexport interface FilterMenuProps {\n  groups: FilterGroup[];\n  /** Por chave de grupo, os `id` marcados. Em `single`, use array de um item. */\n  selected: Record<string, string[]>;\n  onToggle: (groupKey: string, optionId: string) => void;\n  onClear?: () => void;\n  label?: string;\n  clearLabel?: string;\n  emptyLabel?: string;\n  align?: \"start\" | \"center\" | \"end\";\n  className?: string;\n  /** Selo com quantos grupos estão filtrando. Desligue em menus de escolha única (ordenar,\n   *  agrupar), onde sempre há um marcado e o \"1\" não diz nada. */\n  count?: boolean;\n}\n\nconst itemClass = cn(\n  \"relative flex cursor-pointer select-none items-center gap-2 rounded-[var(--r-control,0.375rem)]\",\n  \"py-1.5 pl-7 pr-2 text-sm outline-none\",\n  \"text-[var(--popover-foreground,var(--foreground))]\",\n  \"data-[highlighted]:bg-[var(--accent,var(--muted))]\",\n  \"data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n);\n\nfunction Tick() {\n  return (\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenu.ItemIndicator>\n        <svg\n          aria-hidden=\"true\"\n          viewBox=\"0 0 12 12\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          className=\"h-3 w-3\"\n        >\n          <path d=\"M2 6.5l2.5 2.5L10 3.5\" />\n        </svg>\n      </DropdownMenu.ItemIndicator>\n    </span>\n  );\n}\n\nfunction Dot({ color }: { color?: string }) {\n  if (!color) return null;\n  // Cor do dado, não do componente. Ver o bloco de decisões.\n  return (\n    <span\n      aria-hidden=\"true\"\n      className=\"h-2 w-2 shrink-0 rounded-full\"\n      style={{ backgroundColor: color }}\n    />\n  );\n}\n\nexport function FilterMenu({\n  groups,\n  selected,\n  onToggle,\n  onClear,\n  label = \"Filtros\",\n  clearLabel = \"Limpar filtros\",\n  emptyLabel = \"Nada para filtrar aqui.\",\n  align = \"end\",\n  className,\n  count = true,\n}: FilterMenuProps) {\n  // Grupos, não opções: três status marcados são UM eixo de filtragem.\n  const activeGroups = groups.filter((g) => (selected[g.key]?.length ?? 0) > 0).length;\n  const usable = groups.filter((g) => g.options.length > 0);\n\n  return (\n    <DropdownMenu.Root>\n      <DropdownMenu.Trigger\n        className={cn(\n          \"inline-flex h-[var(--control-h,2.25rem)] pointer-coarse:h-11 shrink-0 items-center gap-2\",\n          \"rounded-[var(--r-control,0.5rem)] border border-[var(--border)] px-3\",\n          \"text-sm font-medium text-[var(--foreground)]\",\n          \"transition-[background-color,border-color,box-shadow]\",\n          \"[transition-duration:var(--dur-fast,160ms)]\",\n          \"[transition-timing-function:var(--ease-out)]\",\n          \"hover:bg-[var(--muted)]\",\n          \"outline-none focus-visible:border-[color-mix(in_oklab,var(--ring)_60%,var(--foreground))] focus-visible:[box-shadow:0_0_0_1px_color-mix(in_oklab,var(--ring)_30%,transparent)]\",\n          className,\n        )}\n      >\n        <svg\n          aria-hidden=\"true\"\n          viewBox=\"0 0 16 16\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"1.5\"\n          strokeLinecap=\"round\"\n          className=\"h-3.5 w-3.5\"\n        >\n          <path d=\"M2 4h12M4.5 8h7M7 12h2\" />\n        </svg>\n        <span>{label}</span>\n        {count && activeGroups > 0 ? (\n          <span\n            className={cn(\n              \"ml-0.5 inline-flex h-5 min-w-5 items-center justify-center rounded-full px-1.5\",\n              \"bg-[var(--foreground)] text-[11px] font-bold tabular-nums text-[var(--background)]\",\n            )}\n          >\n            {activeGroups}\n          </span>\n        ) : null}\n      </DropdownMenu.Trigger>\n\n      <DropdownMenu.Portal>\n        <DropdownMenu.Content\n          align={align}\n          sideOffset={6}\n          collisionPadding={8}\n          className={cn(\n            \"z-50 max-h-[min(24rem,var(--radix-dropdown-menu-content-available-height))]\",\n            \"w-60 overflow-y-auto p-1\",\n            \"rounded-[var(--r-surface,0.625rem)] border border-[var(--border)]\",\n            \"bg-[var(--popover,var(--background))] text-[var(--popover-foreground,var(--foreground))]\",\n            \"[box-shadow:var(--e-overlay,0_12px_32px_-12px_rgb(0_0_0/0.35))]\",\n            // Abre do gatilho e fecha de volta nele; sair também tem percurso.\n            \"origin-[var(--radix-dropdown-menu-content-transform-origin)]\",\n            \"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95\",\n            \"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95\",\n            \"[animation-duration:var(--dur-fast,160ms)] [animation-timing-function:var(--ease-out)]\",\n          )}\n        >\n          {usable.length === 0 ? (\n            // Estado vazio nomeado: caixa em branco parece defeito.\n            <p className=\"px-2 py-6 text-center text-sm text-[var(--muted-foreground)]\">\n              {emptyLabel}\n            </p>\n          ) : null}\n\n          {usable.map((group, index) => {\n            const marked = selected[group.key] ?? [];\n            return (\n              <React.Fragment key={group.key}>\n                {index > 0 ? (\n                  <DropdownMenu.Separator className=\"my-1 h-px bg-[var(--border)]\" />\n                ) : null}\n                <DropdownMenu.Label className=\"px-2 py-1.5 text-[11px] font-bold uppercase tracking-[0.08em] text-[var(--muted-foreground)]\">\n                  {group.label}\n                </DropdownMenu.Label>\n\n                {group.mode === \"single\" ? (\n                  <DropdownMenu.RadioGroup\n                    value={marked[0] ?? \"\"}\n                    onValueChange={(id) => onToggle(group.key, id)}\n                  >\n                    {group.options.map((option) => (\n                      <DropdownMenu.RadioItem\n                        key={option.id}\n                        value={option.id}\n                        onSelect={(event) => event.preventDefault()}\n                        className={itemClass}\n                      >\n                        <Tick />\n                        <Dot color={option.dot} />\n                        <span className=\"min-w-0 truncate\">{option.label}</span>\n                      </DropdownMenu.RadioItem>\n                    ))}\n                  </DropdownMenu.RadioGroup>\n                ) : (\n                  group.options.map((option) => (\n                    <DropdownMenu.CheckboxItem\n                      key={option.id}\n                      checked={marked.includes(option.id)}\n                      // Sem isto o menu fecha a cada marcação, e marcar três\n                      // status vira três aberturas.\n                      onSelect={(event) => event.preventDefault()}\n                      onCheckedChange={() => onToggle(group.key, option.id)}\n                      className={itemClass}\n                    >\n                      <Tick />\n                      <Dot color={option.dot} />\n                      <span className=\"min-w-0 truncate\">{option.label}</span>\n                    </DropdownMenu.CheckboxItem>\n                  ))\n                )}\n              </React.Fragment>\n            );\n          })}\n\n          {/* Ação permanentemente desabilitada ensina a ignorar o canto do menu. */}\n          {onClear && activeGroups > 0 ? (\n            <>\n              <DropdownMenu.Separator className=\"my-1 h-px bg-[var(--border)]\" />\n              <DropdownMenu.Item\n                onSelect={() => onClear()}\n                className={cn(itemClass, \"pl-2 text-[var(--destructive)]\")}\n              >\n                {clearLabel}\n              </DropdownMenu.Item>\n            </>\n          ) : null}\n        </DropdownMenu.Content>\n      </DropdownMenu.Portal>\n    </DropdownMenu.Root>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "controles",
    "filtro",
    "menu",
    "facetas"
  ],
  "type": "registry:ui"
}