{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "filter-bar",
  "title": "Filter Bar",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "Busca e filtro numa peça só: campo, menu de facetas, chips aplicados e a contagem que some quando nada foi filtrado.",
  "dependencies": [
    "motion",
    "react"
  ],
  "registryDependencies": [
    "utils",
    "@visant/chip",
    "@visant/motion",
    "@visant/search-field",
    "@visant/filter-menu"
  ],
  "files": [
    {
      "path": "registry/visant/ui/filter-bar.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { DUR, EASE_OUT, INSTANT } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\nimport { Chip } from \"@/components/ui/chip\";\nimport { FilterMenu, type FilterGroup } from \"@/components/ui/filter-menu\";\nimport { SearchField } from \"@/components/ui/search-field\";\n\n/**\n * FilterBar — o esqueleto de uma barra de filtro: controles em cima, filtros\n * aplicados embaixo.\n *\n * Colhido de uma barra que só servia ao produto de origem: recebia o objeto\n * `FilterState` inteiro, montava os chips lendo `statuses`/`priorities`/`labels`\n * e traduzia prioridade para português na mão. Trocar de domínio era reescrever.\n *\n * Aqui a barra **não sabe o que é um filtro**. Ela recebe chips já formados e\n * slots para os controles. Quem sabe de status e prioridade é a tela; quem sabe\n * de layout, foco e resumo é este arquivo. É a divisão que faz a peça viajar.\n *\n * ── Por que os controles são slots e não props de dados ─────────────────────\n * A tentação é receber `statuses`, `sortOptions`, `viewModes` e renderizar tudo.\n * Aí o primeiro cliente que precisa de um seletor de data no meio da fila força\n * uma prop nova, e em seis meses a barra tem doze props booleanas de arranjo.\n * Slot é a única API que não cresce: `<FilterBar filters={<FilterMenu …/>} />`.\n *\n * ── Detalhes que decidem se a barra é útil ──────────────────────────────────\n *\n * 1. **A contagem só aparece quando filtra.** \"48 de 48\" é uma linha de texto\n *    para dizer que nada aconteceu. Some quando os números batem.\n * 2. **A contagem é `aria-live=\"polite\"`.** Filtrar é uma mudança que acontece\n *    longe do foco — sem isso, quem usa leitor de tela digita na busca e não\n *    recebe nenhum sinal de que a lista encolheu.\n * 3. **A segunda linha abre e fecha, não pula.** Sem chips ela tem altura zero, e\n *    o `grid-template-rows` de `0fr` para `1fr` empurra o conteúdo no mesmo\n *    compasso do chip que entra. Chip entra e sai com a mesma curva curta, e os\n *    vizinhos deslizam para o vão (`layout`), em vez de saltar.\n * 4. **A busca ocupa a sobra.** É o controle que recebe texto de tamanho\n *    imprevisível; os outros têm largura de rótulo.\n *\n * ── Busca e filtro numa peça só ──────────────────────────────────────────────\n * Com `query` e `groups`, a barra monta sozinha o `search-field`, o `filter-menu`\n * e os chips, e `size=\"lg\"` junta os dois numa cápsula. Os slots continuam valendo\n * para quem precisa de outro controle, e os dois itens seguem instaláveis.\n *\n * Compõe com `sticky-filterbar`, que resolve grudar e condensar e não sabe\n * montar nada: `<StickyFilterbar><FilterBar … /></StickyFilterbar>`.\n */\n\nexport interface FilterBarChip {\n  key: string;\n  label: string;\n  onRemove: () => void;\n}\n\nexport interface FilterBarProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Filtros já aplicados. Já vêm formatados: a barra não traduz domínio. */\n  chips?: FilterBarChip[];\n  search?: React.ReactNode;\n  filters?: React.ReactNode;\n  sort?: React.ReactNode;\n  view?: React.ReactNode;\n  extras?: React.ReactNode;\n  filteredCount?: number;\n  totalCount?: number;\n  onClearAll?: () => void;\n  clearAllLabel?: string;\n  /** `(filtrados, total) => texto`. Trocável para plural, idioma ou substantivo. */\n  countLabel?: (filtered: number, total: number) => string;\n  /** Texto da busca. Com `onQueryChange`, a barra monta o campo sozinha. */\n  query?: string;\n  onQueryChange?: (query: string) => void;\n  queryPlaceholder?: string;\n  /** Facetas. Com `selected` e `onToggle`, a barra monta o menu e os chips sozinha. */\n  groups?: FilterGroup[];\n  selected?: Record<string, string[]>;\n  onToggle?: (groupKey: string, optionId: string) => void;\n  filtersLabel?: string;\n  /** `lg` junta busca e filtro numa cápsula centrada. */\n  size?: \"md\" | \"lg\";\n}\n\n// Dentro da cápsula quem desenha a borda e o foco é ela; o campo só leva o texto.\nconst CAMPO_DA_CAPSULA =\n  \"min-w-0 [&_input]:h-12 [&_input]:rounded-none [&_input]:border-0 [&_input]:bg-transparent [&_input]:pl-10 [&_input]:text-base [&_input:focus-visible]:[box-shadow:none] [&_svg:first-child]:left-3.5\";\nconst FILTRO_DA_CAPSULA =\n  \"h-10 rounded-[var(--r-pill,9999px)] border-transparent bg-[var(--muted)] pointer-coarse:h-11\";\n\nexport function FilterBar({\n  chips = [],\n  search,\n  filters,\n  sort,\n  view,\n  extras,\n  filteredCount,\n  totalCount,\n  onClearAll,\n  clearAllLabel = \"Limpar tudo\",\n  countLabel = (filtered, total) => `${filtered} de ${total}`,\n  query,\n  onQueryChange,\n  queryPlaceholder = \"Buscar...\",\n  groups,\n  selected = {},\n  onToggle,\n  filtersLabel,\n  size = \"md\",\n  className,\n  ...props\n}: FilterBarProps) {\n  // \"48 de 48\" é ruído: só informa quando os números divergem.\n  const showCount =\n    typeof filteredCount === \"number\" &&\n    typeof totalCount === \"number\" &&\n    filteredCount !== totalCount;\n\n  const capsula = size === \"lg\";\n  const campo =\n    search ??\n    (onQueryChange ? (\n      <SearchField\n        value={query ?? \"\"}\n        onChange={onQueryChange}\n        placeholder={queryPlaceholder}\n        delay={120}\n        className={capsula ? CAMPO_DA_CAPSULA : undefined}\n      />\n    ) : null);\n  const menu =\n    filters ??\n    (groups && onToggle ? (\n      <FilterMenu\n        groups={groups}\n        selected={selected}\n        onToggle={onToggle}\n        onClear={onClearAll}\n        label={filtersLabel}\n        className={capsula ? FILTRO_DA_CAPSULA : undefined}\n      />\n    ) : null);\n  // Sem chips prontos, eles saem do que está marcado: o menu já sabe o rótulo de cada opção.\n  const chipsVistos: FilterBarChip[] =\n    chips.length > 0 || !groups || !onToggle\n      ? chips\n      : groups.flatMap((g) =>\n          g.options\n            .filter((o) => selected[g.key]?.includes(o.id))\n            .map((o) => ({ key: `${g.key}-${o.id}`, label: o.label, onRemove: () => onToggle(g.key, o.id) })),\n        );\n  const hasChips = chipsVistos.length > 0;\n  const reduzir = useReducedMotion();\n  const passo = reduzir ? INSTANT : { duration: DUR.fast, ease: EASE_OUT };\n\n  return (\n    <div className={cn(\"flex w-full flex-col\", className)} {...props}>\n      <div\n        className={cn(\n          \"flex items-center\",\n          capsula\n            ? cn(\n                \"min-h-14 gap-1 rounded-[var(--r-pill,9999px)] border border-[var(--border)] bg-[var(--card,var(--background))] pr-2\",\n                \"[box-shadow:var(--e-raised,0_8px_24px_-16px_rgb(0_0_0/0.35))]\",\n                \"transition-[border-color,box-shadow] [transition-duration:var(--dur-base,200ms)] [transition-timing-function:var(--ease-out)]\",\n                \"focus-within:border-[color-mix(in_oklab,var(--ring)_60%,var(--foreground))] focus-within:[box-shadow:0_0_0_3px_color-mix(in_oklab,var(--ring)_25%,transparent)]\",\n              )\n            : \"gap-2 sm:gap-3\",\n        )}\n      >\n        {/* A busca recebe texto de tamanho imprevisível — fica com a sobra. */}\n        {campo ? <div className=\"min-w-0 flex-1\">{campo}</div> : null}\n        {menu}\n        {sort}\n        {view}\n        {extras}\n      </div>\n\n      <div\n        className={cn(\n          \"grid transition-[grid-template-rows,opacity]\",\n          \"[transition-duration:var(--dur-base,200ms)]\",\n          \"[transition-timing-function:var(--ease-out)]\",\n          \"motion-reduce:transition-none\",\n          hasChips || showCount ? \"grid-rows-[1fr]\" : \"pointer-events-none grid-rows-[0fr] opacity-0\",\n        )}\n      >\n        {/* `-m-1 p-1`: o recorte que dobra a linha não pode comer o anel de foco do chip. */}\n        <div className=\"-m-1 min-h-0 overflow-hidden p-1\">\n          <div className={cn(\"flex flex-wrap items-center gap-1.5 pt-2\", capsula && \"justify-center pt-3\")}>\n            <AnimatePresence initial={false} mode=\"popLayout\">\n              {chipsVistos.map((chip) => (\n                <motion.span\n                  key={chip.key}\n                  layout={reduzir ? false : \"position\"}\n                  className=\"inline-flex\"\n                  initial={{ opacity: 0, scale: 0.95 }}\n                  animate={{ opacity: 1, scale: 1 }}\n                  exit={{ opacity: 0, scale: 0.95 }}\n                  transition={passo}\n                >\n                  <Chip size=\"sm\" pressed onRemove={chip.onRemove}>\n                    {chip.label}\n                  </Chip>\n                </motion.span>\n              ))}\n            </AnimatePresence>\n\n            {hasChips && onClearAll ? (\n              <button\n                type=\"button\"\n                onClick={onClearAll}\n                className={cn(\n                  \"px-1.5 py-1 pointer-coarse:min-h-11 text-xs font-semibold text-[var(--muted-foreground)] underline-offset-4\",\n                  \"hover:text-[var(--foreground)] hover:underline\",\n                  \"outline-none focus-visible:[box-shadow:0_0_0_1px_color-mix(in_oklab,var(--ring)_60%,var(--foreground))]\",\n                  \"rounded-[var(--r-control,0.375rem)]\",\n                )}\n              >\n                {clearAllLabel}\n              </button>\n            ) : null}\n\n            {showCount ? (\n              // Filtrar muda a lista longe do foco. Sem `aria-live`, quem usa\n              // leitor de tela digita e não recebe sinal nenhum de que encolheu.\n              <span\n                aria-live=\"polite\"\n                className={cn(\"shrink-0 text-xs tabular-nums text-[var(--muted-foreground)]\", !capsula && \"ml-auto\", capsula && \"ml-2\")}\n              >\n                {countLabel(filteredCount as number, totalCount as number)}\n              </span>\n            ) : null}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "controles",
    "filtro",
    "busca",
    "chips"
  ],
  "type": "registry:ui"
}