{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "logo-wall",
  "title": "Logo Wall",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "A faixa de \"confiam na gente\" com os logos normalizados: altura óptica por item, teto de largura pro wordmark comprido e dessaturação em repouso que vira cor no hover.",
  "dependencies": [
    "gsap"
  ],
  "registryDependencies": [
    "utils",
    "https://visant-ui.vercel.app/r/marquee.json",
    "@visant/marquee"
  ],
  "files": [
    {
      "path": "registry/visant/ui/logo-wall.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Marquee } from \"@/components/ui/marquee\";\n\n/**\n * LogoWall — a faixa de \"confiam na gente\", com os logos NORMALIZADOS.\n *\n * ── Por que não é só um marquee com uns `<img>` dentro ──────────────────────\n *\n * O motor de faixa infinita já existe e é o `@visant/marquee`: GSAP, três modos\n * de scroll, emenda sem tranco, `prefers-reduced-motion`, pausa fora da tela.\n * Reescrever aquele loop aqui seria criar um segundo marquee pra manter. Esta\n * peça COMPÕE em cima dele e resolve o problema que ele não tem como resolver,\n * porque não é problema de movimento:\n *\n * **logo de cliente chega caótico.** Um vem em SVG vetorial preto, outro em PNG\n * colorido de 2000px, um é wordmark de 6:1 e o vizinho é monograma quadrado.\n * Jogados numa fileira com `height: 32px`, o monograma vira um selo enorme ao\n * lado de um wordmark que virou um fiapo — e a faixa que deveria dizer \"estes\n * são os nossos clientes\" diz \"montaram isto às pressas\".\n *\n * Três normalizações, e as três são obrigatórias juntas:\n *\n * 1. **Altura ÓPTICA, não altura de caixa.** Caixa de mesma altura não é peso de\n *    mesma altura: um monograma quadrado ocupa a altura inteira e um wordmark com\n *    ascendente e descendente ocupa metade dela. Por isso cada item aceita\n *    `scale` — um multiplicador de correção, tipicamente entre 0,8 e 1,25, que\n *    quem monta a faixa ajusta OLHANDO. Não existe algoritmo pra isto: é a mesma\n *    correção óptica que um tipógrafo faz num \"O\" pra ele parecer do tamanho do\n *    \"H\". O que a peça garante é que a correção seja UM número por logo, no lugar\n *    certo, em vez de um `className` diferente em cada `<img>`.\n * 2. **Teto de largura.** Sem ele o wordmark comprido come três vagas da faixa e\n *    o ritmo do gap morre. Com ele, o logo comprido encolhe pra caber e continua\n *    na mesma linha de base dos outros.\n * 3. **Dessaturação em repouso, cor no hover.** Em repouso a faixa tem que ler\n *    como uma TEXTURA de marcas, não como oito anúncios competindo pela atenção\n *    do olho no meio da sua página. A cor volta no hover, e o hover TAMBÉM para a\n *    faixa (é o `pauseOnHover` do marquee) — os dois gestos são um só: a pessoa\n *    aponta, a esteira freia, o logo acende.\n *\n * ── Cor, tema e a verdade sobre PNG ────────────────────────────────────────\n *\n * `finish=\"mono\"` pinta o logo com `currentColor` e é o único acabamento\n * **seguro no tema escuro** — mas só funciona com SVG inline, porque é o único\n * formato que aceita ser recolorido. Logo de cliente em PNG preto simplesmente\n * some num fundo escuro, e nenhum filtro conserta isso sem estragar o logo\n * colorido do vizinho. A peça não finge que resolve: `grayscale` é o padrão\n * porque funciona com qualquer arquivo, e a saída certa pro tema escuro é pedir\n * o SVG ao cliente. Está escrito aqui pra não ser descoberto no deploy.\n *\n * ── O nome do cliente é conteúdo, o logo é desenho ─────────────────────────\n *\n * `name` é obrigatório e vai num `sr-only`; o slot do logo vai em `aria-hidden`.\n * A alternativa — confiar no `alt` do `<img>` — quebra no primeiro logo que\n * chegar como SVG inline, como `next/image` sem alt ou como componente do\n * projeto. Assim há exatamente UM nome acessível por item, venha o que vier no\n * slot. (A cópia que o marquee duplica pra fechar o loop já nasce `aria-hidden`,\n * então nada é anunciado duas vezes.)\n *\n * Requer `gsap` e o `@visant/marquee` quando `marquee` está ligado.\n */\n\nexport interface LogoWallItem {\n  id: string;\n  /** O nome do cliente. Obrigatório: é ele que o leitor de tela anuncia. */\n  name: string;\n  /** O logo, por slot: SVG inline, `<img>`, `next/image`, componente do projeto. */\n  logo: React.ReactNode;\n  /**\n   * Correção óptica de tamanho. 1 é a altura base; 1,2 sobe 20%.\n   *\n   * Ajuste olhando, um logo por vez, e só depois de a faixa estar montada — a\n   * correção certa depende dos vizinhos.\n   */\n  scale?: number;\n  /** Vira link. Um logo clicável precisa ser âncora, não `div` com `onClick`. */\n  href?: string;\n}\n\nexport interface LogoWallProps extends React.HTMLAttributes<HTMLDivElement> {\n  items: LogoWallItem[];\n  /** Altura óptica base, em rem. É sobre ela que o `scale` de cada item age. */\n  height?: number;\n  /** Teto de largura de cada logo, em rem. Segura o wordmark comprido. */\n  maxWidth?: number;\n  /**\n   * O acabamento em repouso.\n   *\n   * - `\"grayscale\"` (padrão): dessatura e apaga; a cor volta no hover. Serve\n   *   qualquer arquivo.\n   * - `\"mono\"`: pinta com `currentColor` — só SVG inline, e o único seguro no\n   *   tema escuro. Ver a nota do cabeçalho.\n   * - `\"none\"`: o logo como veio, sempre.\n   */\n  finish?: \"grayscale\" | \"mono\" | \"none\";\n  /** Desligue pra uma grade parada. A normalização continua valendo. */\n  marquee?: boolean;\n  /** Segundos por volta. Faixa de logo pede lento: 60 a 90. */\n  speed?: number;\n  gap?: number;\n  /** Some nas bordas em vez de ser cortada a faca. */\n  fade?: boolean;\n  reverse?: boolean;\n  /** Sangra pra fora do container. Passe o mesmo respiro do pai. */\n  bleed?: number | false;\n  /**\n   * O rótulo da faixa: \"Confiam na gente\", \"Clientes desde 2019\".\n   *\n   * Ele mora FORA da esteira, e não é preciosismo: dentro dela viraria conteúdo\n   * do loop e andaria junto com os logos, reaparecendo a cada volta. Foi a\n   * lição que a demo do marquee já tinha aprendido.\n   */\n  heading?: React.ReactNode;\n  /** Nome da lista pro leitor de tela, quando não há `heading` visível. */\n  label?: string;\n}\n\nexport function LogoWall({\n  items,\n  height = 2,\n  maxWidth = 9,\n  finish = \"grayscale\",\n  marquee = true,\n  speed = 72,\n  gap = 64,\n  fade = true,\n  reverse = false,\n  bleed = false,\n  heading,\n  label = \"Clientes\",\n  className,\n  ...props\n}: LogoWallProps) {\n  const celulas = items.map((item) => (\n    <Logo key={item.id} item={item} height={height} maxWidth={maxWidth} finish={finish} />\n  ));\n\n  return (\n    <div className={cn(\"flex flex-col gap-5\", className)} {...props}>\n      {heading ? (\n        <p className=\"text-center font-mono text-[length:var(--text-rotulo,0.6875rem)] uppercase tracking-[0.16em] text-[var(--muted-foreground)]\">\n          {heading}\n        </p>\n      ) : null}\n\n      {marquee ? (\n        <Marquee\n          items={celulas}\n          speed={speed}\n          gap={gap}\n          fade={fade}\n          reverse={reverse}\n          bleed={bleed}\n          // `\"boost\"` e não `\"drive\"`: a faixa de clientes tem que existir mesmo\n          // com a página parada — ela é prova, e prova que só aparece quando a\n          // pessoa rola é prova que metade das visitas não vê.\n          scroll=\"boost\"\n          aria-label={label}\n        />\n      ) : (\n        // Grade parada: `justify-center` e `gap` iguais aos da esteira, pra\n        // trocar de modo não mudar o ritmo da fileira.\n        <ul\n          aria-label={label}\n          className=\"flex flex-wrap items-center justify-center\"\n          style={{ gap: `${gap / 2}px ${gap}px` }}\n        >\n          {items.map((item) => (\n            <li key={item.id}>\n              <Logo item={item} height={height} maxWidth={maxWidth} finish={finish} />\n            </li>\n          ))}\n        </ul>\n      )}\n    </div>\n  );\n}\n\nfunction Logo({\n  item,\n  height,\n  maxWidth,\n  finish,\n}: {\n  item: LogoWallItem;\n  height: number;\n  maxWidth: number;\n  finish: NonNullable<LogoWallProps[\"finish\"]>;\n}) {\n  const Tag = (item.href ? \"a\" : \"span\") as \"span\";\n\n  return (\n    <Tag\n      {...(item.href ? { href: item.href } : null)}\n      className={cn(\n        \"group/logo inline-flex shrink-0 items-center justify-center no-underline\",\n        \"outline-none focus-visible:[box-shadow:var(--focus)] focus-visible:rounded-[var(--r-control,0.5rem)]\",\n        // Só o que muda, nunca `transition-all`: filtro e opacidade arrastariam\n        // layout junto se a lista fosse ampla demais.\n        \"transition-[filter,opacity,color] [transition-duration:var(--dur-fast,140ms)]\",\n        \"[transition-timing-function:var(--ease-out)]\",\n        \"motion-reduce:transition-none\",\n        finish === \"grayscale\" && [\n          \"grayscale opacity-55\",\n          \"hover:grayscale-0 hover:opacity-100\",\n          \"group-focus-visible/logo:grayscale-0 focus-visible:grayscale-0 focus-visible:opacity-100\",\n          // Onde não existe hover, o repouso dessaturado seria permanente — e aí\n          // a faixa inteira ficaria cinza no celular, que é onde metade das\n          // visitas acontece. Em ponteiro grosso a cor é o estado normal.\n          \"[@media(hover:none)]:grayscale-0 [@media(hover:none)]:opacity-100\",\n        ],\n        finish === \"mono\" && [\n          \"text-[var(--muted-foreground)] opacity-90\",\n          \"hover:text-[var(--foreground)] focus-visible:text-[var(--foreground)]\",\n          \"[@media(hover:none)]:text-[var(--foreground)]\",\n          // `currentColor` é o que faz o `mono` funcionar, e só o SVG inline\n          // obedece. `fill` e `stroke` juntos porque logo desenhado vem dos dois\n          // jeitos, e o que estiver ausente simplesmente não é atingido.\n          \"[&_svg_[fill]:not([fill=none])]:fill-current [&_svg_[stroke]:not([stroke=none])]:stroke-current\",\n          \"[&_svg]:fill-current\",\n        ],\n      )}\n      style={{\n        // A altura é a óptica: base × correção do item. O teto de largura mora\n        // aqui e não numa classe porque ele é número de quem monta a faixa.\n        height: `${height * (item.scale ?? 1)}rem`,\n        maxWidth: `${maxWidth}rem`,\n      }}\n    >\n      {/*\n       * O slot inteiro é decoração; o nome acessível vem do `sr-only` ao lado.\n       * `object-contain` e não `cover`: logo cortado é logo errado, e o contrato\n       * de marca de qualquer cliente proíbe.\n       */}\n      <span\n        aria-hidden=\"true\"\n        className={cn(\n          \"flex h-full w-full items-center justify-center\",\n          \"[&_img]:h-full [&_img]:w-auto [&_img]:max-w-full [&_img]:object-contain\",\n          \"[&_svg]:h-full [&_svg]:w-auto [&_svg]:max-w-full\",\n        )}\n      >\n        {item.logo}\n      </span>\n      <span className=\"sr-only\">{item.name}</span>\n    </Tag>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Seção de clientes ou parceiros numa landing, com logos de proporções diferentes normalizados",
      "evite": "Texto ou itens que não são logo: marquee; prova social com citação, testimonial-wall"
    }
  },
  "categories": [
    "conteudo",
    "logos",
    "clientes",
    "prova-social",
    "faixa",
    "marquee",
    "gsap"
  ],
  "type": "registry:ui"
}