{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chip",
  "title": "Chip",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "Pastilha de filtro com contagem. A faceta sem resultado apaga em vez de mostrar zero. Com onRemove vira o chip do filtro já aplicado, sem aria-pressed.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/visant/ui/chip.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Chip — pastilha de filtro.\n *\n * Colhida de um concept de e-commerce. É o gesto que o comprador de loja grande já\n * tem no dedo: nada de inventar controle novo numa superfície de compra.\n *\n * Duas coisas que parecem detalhe e não são:\n *\n * 1. **`aria-pressed`, não `aria-selected`.** Um filtro é um botão de dois\n *    estados, não um item de lista selecionado. O leitor de tela anuncia\n *    \"pressionado\", que é a verdade.\n * 2. **`count` nunca renderiza zero.** Zero não é informação — um filtro que\n *    mostra \"(0)\" ocupa espaço para dizer que não serve. Ele some.\n *\n * O `icon` existe para o caso em que o dado da loja cabe dentro do controle:\n * no concept é a curva do bow desenhada em SVG, herdando `currentColor`,\n * então ela acompanha o estado sem uma linha de CSS a mais.\n *\n * ── `onRemove`: o segundo papel ─────────────────────────────────────────────\n * Uma tela de filtro tem dois chips, e o Linear mostra os dois lado a lado: o que\n * **liga e desliga** uma faceta (este, com `pressed`) e o que representa um filtro\n * **já aplicado** e some quando clicado.\n *\n * Eram dois componentes quase idênticos em dois repos. Aqui é uma prop, porque a\n * anatomia é a mesma e só a semântica muda:\n *\n * - com `onRemove`, o chip **perde o `aria-pressed`** — remover é ação, não estado\n *   de dois valores. Anunciar \"pressionado\" mentiria sobre o que o clique faz.\n * - o clique inteiro remove, não só o `×`. Um alvo de 10px reprova em toque, e o\n *   chip aplicado não tem outra ação para disputar o clique.\n * - o nome acessível vira \"Remover filtro X\" quando `children` é texto. Sem isso o\n *   leitor de tela anuncia só o rótulo, e \"Urgente\" não avisa que clicar apaga.\n *   Com `children` composto, passe `aria-label` na mão.\n *\n * Um filtro aplicado normalmente também quer `pressed`. A prop não é implícita de\n * propósito: quem monta a barra decide se o chip aplicado lê como preenchido.\n\n *\n * No removível a marca de conferido não aparece (`mark` vira falso): ela e o `×`\n * dizem a mesma coisa, e o filtro aplicado já lê como escolhido pela cor.\n *\n * ── O que faltava: a pastilha não tinha anatomia ───────────────────────────\n *\n * Ela era uma caixa que trocava de cor. Três buracos, e os três aparecem no\n * primeiro minuto de uso:\n *\n * 1. **Ligado e desligado tinham o mesmo peso.** Invertia fundo e texto em\n *    `--foreground`, que é a mesma tinta do texto de qualquer lugar da página:\n *    o estado ligado lia como \"hover que ficou\". Agora o ligado é a MARCA\n *    (`--brand`), a única tinta da tela que só aparece quando algo está\n *    escolhido — e o par `--brand-foreground` vem junto, então o contraste é o\n *    do tema e não uma aposta.\n * 2. **Ligar não era um evento.** A troca era instantânea e sem gesto. Agora\n *    entra a marca de conferido, e ela ENTRA: a coluna de grade vai de `0fr` a\n *    `1fr`, então a pastilha ganha largura enquanto o traço aparece. É a\n *    técnica padrão de animar largura desconhecida sem `max-width` chutado, e\n *    é o único movimento da peça — quem pediu `prefers-reduced-motion` recebe\n *    o mesmo estado final, sem a transição.\n * 3. **Não sabia sumir de leve nem receber foco.** Facetas vazias ficam\n *    `disabled` numa barra de filtro de verdade (contagem zero é comum quando\n *    dois eixos se cruzam), e o anel de foco não existia — o teclado atravessava\n *    a barra às cegas. O anel é `outline`, não `box-shadow`, porque `shadow-[var(…)]`\n *    morre no valor inicial de `--tw-shadow` (ver `scripts/css-tokens-check.mjs`).\n *\n * O espaçamento interno é margem por parte, e não `gap` do flex, exatamente por\n * causa da marca: um filho de largura zero ainda consome uma goteira, e a\n * pastilha desligada nasceria com 8px de vão à esquerda sem nada dentro.\n */\n\nexport interface ChipProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onSelect\"> {\n  pressed?: boolean;\n  count?: number;\n  icon?: React.ReactNode;\n  /** `sm` para barra densa (linha de facetas), `md` para a barra principal. */\n  size?: \"sm\" | \"md\";\n  /**\n   * A marca de conferido que cresce quando liga.\n   *\n   * Desligue em pastilha que é escolha ÚNICA dentro de um grupo (um segmentado\n   * disfarçado): ali a cor cheia já é a resposta e o traço vira redundância que\n   * mexe na largura de todo mundo a cada troca.\n   */\n  mark?: boolean;\n  /** Torna o chip removível: renderiza o `×` e o clique inteiro remove. */\n  onRemove?: () => void;\n}\n\nexport function Chip({\n  pressed = false,\n  count,\n  icon,\n  size = \"md\",\n  mark = true,\n  onRemove,\n  children,\n  className,\n  onClick,\n  // Sai do `props` de propósito: o `{...props}` é espalhado DEPOIS dos atributos\n  // no JSX, então um `aria-label` que ficasse lá dentro sobrescreveria o rótulo\n  // calculado abaixo — com `undefined`, apagando o nome acessível do chip\n  // removível justamente quando o consumidor não passou nenhum.\n  \"aria-label\": ariaLabel,\n  ...props\n}: ChipProps) {\n  const removable = typeof onRemove === \"function\";\n  const comMarca = mark && !removable;\n\n  return (\n    <button\n      type=\"button\"\n      // Ação não é estado — ver o bloco acima.\n      aria-pressed={removable ? undefined : pressed}\n      aria-label={\n        ariaLabel ??\n        (removable && typeof children === \"string\" ? `Remover filtro ${children}` : undefined)\n      }\n      onClick={(event) => {\n        onClick?.(event);\n        // `preventDefault` no handler de fora cancela a remoção — é a válvula para\n        // quem precisa confirmar antes de apagar.\n        if (event.defaultPrevented) return;\n        onRemove?.();\n      }}\n      data-state={pressed ? \"on\" : \"off\"}\n      className={cn(\n        \"relative inline-flex items-center whitespace-nowrap border font-semibold pointer-coarse:after:absolute pointer-coarse:after:inset-x-0\",\n        size === \"sm\" ? \"px-2.5 py-1 text-[11px] pointer-coarse:after:-inset-y-2.5\" : \"px-3.5 py-2 text-[13px] pointer-coarse:after:-inset-y-1\",\n        \"border-[var(--border)] text-[var(--foreground)]\",\n        \"transition-[background-color,color,border-color,transform]\",\n        \"[transition-duration:var(--dur-press,120ms)]\",\n        \"[transition-timing-function:var(--ease-out)]\",\n        \"hover:border-[var(--foreground)]\",\n        \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:[outline-color:var(--brand,var(--foreground))]\",\n        \"active:scale-[0.97] motion-reduce:active:scale-100\",\n        \"disabled:pointer-events-none disabled:opacity-40\",\n        pressed &&\n          \"border-[var(--brand,var(--foreground))] bg-[var(--brand,var(--foreground))] text-[var(--brand-foreground,var(--background))]\",\n        className,\n      )}\n      {...props}\n    >\n      {comMarca ? (\n        <span\n          aria-hidden=\"true\"\n          className={cn(\n            \"grid transition-[grid-template-columns,opacity]\",\n            \"[transition-duration:var(--dur-fast,160ms)]\",\n            \"[transition-timing-function:var(--ease-out)]\",\n            \"motion-reduce:transition-none\",\n            pressed ? \"grid-cols-[1fr] opacity-100\" : \"grid-cols-[0fr] opacity-0\",\n          )}\n        >\n          <span className=\"overflow-hidden\">\n            <svg\n              viewBox=\"0 0 24 24\"\n              width=\"12\"\n              height=\"12\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"3\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              className=\"mr-1.5 block\"\n            >\n              <path d=\"M4 12.5l5 5L20 6.5\" />\n            </svg>\n          </span>\n        </span>\n      ) : null}\n\n      {icon ? (\n        <span className={cn(\"shrink-0 [&_svg]:overflow-visible\", size === \"sm\" ? \"mr-1.5\" : \"mr-2\")}>\n          {icon}\n        </span>\n      ) : null}\n\n      <span>{children}</span>\n\n      {/* Zero não é informação. */}\n      {typeof count === \"number\" && count > 0 ? (\n        // Fio antes da contagem: ela é outro campo, não a continuação do nome.\n        // `border-current` com opacidade herda o estado — no ligado o fio é da\n        // cor do texto sobre a marca, sem uma segunda regra pro caso invertido.\n        <span\n          className={cn(\n            \"ml-2 border-l border-current/25 pl-2 tabular-nums\",\n            pressed ? \"opacity-80\" : \"opacity-55\",\n          )}\n        >\n          {count}\n        </span>\n      ) : null}\n\n      {removable ? (\n        // `aria-hidden`: o nome acessível já está no botão. Anunciar o × de novo\n        // faria o leitor de tela ler \"Remover filtro Urgente, X\".\n        <svg\n          aria-hidden=\"true\"\n          viewBox=\"0 0 12 12\"\n          className={cn(\"shrink-0 opacity-70\", size === \"sm\" ? \"ml-1.5 h-2 w-2\" : \"ml-2 h-2.5 w-2.5\")}\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n        >\n          <path d=\"M2 2l8 8M10 2l-8 8\" />\n        </svg>\n      ) : null}\n    </button>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Filtro de loja ou catálogo com contagem por faceta, apagando a que não tem resultado",
      "evite": "Escolha única entre poucas opções: segmented-control; tag só de leitura, badge do tema"
    }
  },
  "categories": [
    "controles",
    "filtro",
    "loja"
  ],
  "type": "registry:ui"
}