{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "quote-block",
  "title": "Quote Block",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "O depoimento único, em tamanho de dobra: a citação vem primeiro e em corpo de manchete, a atribuição entra depois como assinatura.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/visant/ui/quote-block.tsx",
      "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * QuoteBlock — UM depoimento, do tamanho que ele merece.\n *\n * ── Por que existe, tendo `@visant/testimonial-wall` ao lado ────────────────\n *\n * A parede é prova por VOLUME: doze vozes pequenas, cada citação em 14px, e o\n * argumento é \"muita gente disse\". Ela resolve bem o problema dela e resolve\n * péssimo o oposto — a dobra em que a página para e mostra UMA frase, de um\n * cliente que a pessoa reconhece. Ali o argumento é PESO, e peso é tipografia:\n * a citação precisa ser lida antes da foto, em corpo de manchete, com o nome e\n * o cargo entrando depois como assinatura.\n *\n * Tentar isso com o `QuoteItem` da parede sai errado por construção: lá o card\n * é uma coluna de malha com `break-inside-avoid`, o avatar é 36px e a citação\n * mora em `opacity-80` embaixo do nome. Esticar aquilo produz um card gigante,\n * não um depoimento de capa. Por isso é peça nova e não variante — o que muda\n * é a ORDEM (citação primeiro, atribuição depois) e a hierarquia inteira.\n *\n * O que foi reaproveitado de lá, de propósito, para as duas parecerem irmãs:\n * a tabela `TONE` com os mesmos três tons, a mesma semântica\n * `figure`/`blockquote`/`figcaption`, e o mesmo `object-cover` no slot de mídia.\n *\n * ── As decisões que custam caro quando se erra ──────────────────────────────\n *\n * 1. **A aspa é desenhada, não é caractere.** Um `“` de fonte muda de forma, de\n *    peso e de altura a cada família de marca — e some quando a webfont não\n *    carrega, deixando um buraco no canto do bloco. O SVG daqui é o mesmo em\n *    qualquer marca e pinta com `--brand`, então ele É o pedaço de cor da peça.\n * 2. **A citação não leva `opacity`.** Desbotar a frase principal é o erro mais\n *    comum nesta dobra: o texto que a página inteira existe pra mostrar sai\n *    lavado, e o cargo do autor fica com mais contraste que o argumento. Quem\n *    desbota aqui é a atribuição, que é metadado.\n * 3. **A quebra é do CONTAINER, não da janela.** Sem `sm:`/`lg:` nenhum: o\n *    retrato tem largura declarada e a coluna de texto tem base mínima, então o\n *    `flex-wrap` empilha quando o CONTAINER aperta. É a mesma lição que a parede\n *    aprendeu com `column-width` — bloco de página não sabe onde vai ser montado,\n *    e media query de viewport mente dentro de uma coluna de 700px.\n * 4. **`cite` é do `<blockquote>`, o nome é do `<figcaption>`.** O HTML é\n *    explícito: pôr o autor dentro do `blockquote` faz o leitor de tela anunciar\n *    o nome como parte da fala. A atribuição fica fora, e o `cite` carrega a URL\n *    da fonte quando ela existe.\n * 5. **Sem estrelinha.** Nota de 5 é linguagem de agregador de review e mora na\n *    parede, onde há doze delas pra comparar. Numa citação única a estrela não\n *    acrescenta prova — encolhe o depoimento pra tamanho de e-commerce.\n */\n\ntype Tone = \"card\" | \"muted\" | \"brand\";\n\n/** Mesmos três tons da parede: quem instala as duas vê o mesmo sistema. */\nconst TONE: Record<Tone, string> = {\n  card: \"bg-[var(--card,var(--background))] border border-[var(--border)]\",\n  muted: \"bg-[var(--muted)] text-[var(--foreground)]\",\n  brand: \"bg-[var(--brand)] text-[var(--brand-foreground)]\",\n};\n\nexport interface QuoteBlockProps\n  // `role` sai do HTML porque aqui ele é o CARGO de quem falou — a mesma\n  // colisão que o `rail` documenta em `heading` vs `title`. `cite` sai porque a\n  // URL da fonte tem nome próprio (`source`) e vai no `<blockquote>`, não na raiz.\n  extends Omit<React.HTMLAttributes<HTMLElement>, \"cite\" | \"role\"> {\n  /** A fala. Curta: acima de ~40 palavras isto deixa de ser capa e vira parede. */\n  quote: React.ReactNode;\n  author: React.ReactNode;\n  /** Cargo. Junto com `company` vira a linha \"Cargo, Empresa\". */\n  role?: React.ReactNode;\n  company?: React.ReactNode;\n  /** O retrato, por slot: `<img>`, `next/image`, o que o projeto usar. */\n  portrait?: React.ReactNode;\n  /** Marca do cliente. Em B2B, o logo prova mais que o rosto — os dois cabem. */\n  logo?: React.ReactNode;\n  /** URL da fonte da citação (case, post, entrevista). Vai no `cite` do HTML. */\n  source?: string;\n  tone?: Tone;\n  /** Sem moldura: o depoimento solto na página, sem caixa e sem respiro próprio. */\n  bare?: boolean;\n  /** Largura do retrato, em rem. É ela que decide quando a peça empilha. */\n  portraitWidth?: number;\n  /** Proporção do retrato, formato CSS. */\n  ratio?: string;\n  /** Retrato à direita. A citação continua vindo antes no DOM — e na leitura. */\n  flip?: boolean;\n}\n\nexport function QuoteBlock({\n  quote,\n  author,\n  role,\n  company,\n  portrait,\n  logo,\n  source,\n  tone = \"card\",\n  bare = false,\n  portraitWidth = 13,\n  ratio = \"4/5\",\n  flip = false,\n  className,\n  children,\n  ...props\n}: QuoteBlockProps) {\n  return (\n    <figure\n      data-tone={bare ? undefined : tone}\n      className={cn(\n        \"flex flex-wrap items-start gap-7\",\n        !bare && [\n          \"overflow-hidden rounded-[var(--r-surface,1rem)]\",\n          \"p-[var(--pad-card,1.5rem)] sm:p-8\",\n          TONE[tone],\n        ],\n        className,\n      )}\n      {...props}\n    >\n      {portrait ? (\n        <div\n          className={cn(\n            \"shrink-0 overflow-hidden rounded-[var(--r-surface,1rem)] bg-[var(--muted)]\",\n            // Duas alturas de regra, igual ao `photo-frame`: o filho direto ganha\n            // tamanho e o `img`/`video` em qualquer profundidade ganha o recorte.\n            // `next/image` e `<picture>` embrulham a foto, e sem a segunda regra\n            // o `object-cover` cai no invólucro e a imagem sai esticada.\n            \"[&>*]:size-full [&_img]:size-full [&_img]:object-cover [&_video]:size-full [&_video]:object-cover\",\n            // `order` e não `flex-row-reverse`: a ordem do DOM (fala primeiro)\n            // é a ordem de leitura, e ela não deve mudar com a decoração.\n            flip && \"order-1\",\n          )}\n          style={{ width: `${portraitWidth}rem`, aspectRatio: ratio }}\n        >\n          {portrait}\n        </div>\n      ) : null}\n\n      {/*\n       * `basis` mínima de 18rem: é ela que dispara o `flex-wrap`. Quando o\n       * container não comporta retrato + 18rem de texto, a coluna quebra pra\n       * linha de baixo e ocupa a largura inteira — empilhamento por medida real\n       * do container, sem uma media query no arquivo.\n       */}\n      <div className=\"flex min-w-0 flex-1 basis-[18rem] flex-col gap-6\">\n        <blockquote cite={source} className=\"flex flex-col gap-4\">\n          <QuoteMark />\n          {/*\n           * O peso da peça mora nesta linha. `text-balance` porque a citação é\n           * curta e quer linhas de comprimento parecido; `1.18` de entrelinha\n           * porque em corpo grande a entrelinha padrão abre buraco entre as\n           * linhas e a frase deixa de ser um bloco.\n           */}\n          <p\n            className={cn(\n              \"text-balance [font-family:var(--font-display,inherit)]\",\n              \"text-[clamp(1.375rem,1.05rem+1.35vw,2.125rem)] font-medium leading-[1.18] tracking-[-0.02em]\",\n            )}\n          >\n            {quote}\n          </p>\n        </blockquote>\n\n        {children}\n\n        <figcaption className=\"flex flex-wrap items-center gap-x-4 gap-y-3\">\n          <span className=\"min-w-0\">\n            <span className=\"block text-[length:var(--text-destaque,0.9375rem)] font-semibold leading-tight\">\n              {author}\n            </span>\n            {role || company ? (\n              // A única `opacity` da peça, e ela é aqui: metadado cede contraste\n              // pra fala. Ver a decisão 2 no cabeçalho.\n              <span className=\"block text-[length:var(--text-apoio,0.8125rem)] leading-[1.45] opacity-70\">\n                {role}\n                {role && company ? \", \" : null}\n                {company}\n              </span>\n            ) : null}\n          </span>\n\n          {logo ? (\n            // `ml-auto` empurra o logo pra ponta oposta do nome; a altura fixa\n            // normaliza, porque logo de cliente chega em qualquer tamanho — é o\n            // mesmo problema que o `@visant/logo-wall` resolve na faixa inteira.\n            // Altura de uma linha de texto e teto de largura: o logo assina junto\n            // com o nome, não compete com a citação. Com 28px ele virava a\n            // segunda manchete do bloco.\n            <span\n              aria-hidden=\"true\"\n              className={cn(\n                \"ml-auto shrink-0 opacity-55\",\n                \"[&_img]:h-[1.0625rem] [&_img]:w-auto [&_img]:max-w-[6.5rem] [&_img]:object-contain\",\n                \"[&_svg]:h-[1.0625rem] [&_svg]:w-auto [&_svg]:max-w-[6.5rem]\",\n              )}\n            >\n              {logo}\n            </span>\n          ) : null}\n        </figcaption>\n      </div>\n    </figure>\n  );\n}\n\n/**\n * A aspa de abertura, desenhada.\n *\n * A cor é `--brand`, e ela é o único pedaço de cor da peça — mas sobre o tom\n * `brand` a superfície JÁ é a cor da marca, e uma aspa da mesma cor\n * desapareceria no fundo. O negativo é resolvido por seletor de atributo, como\n * no `@visant/cta-surface`: quem decide não é uma prop repassada, é o tom que o\n * ancestral escreveu. O fallback em `currentColor` cobre o tema sem `--brand`.\n */\nfunction QuoteMark() {\n  return (\n    <svg\n      viewBox=\"0 0 32 24\"\n      aria-hidden=\"true\"\n      className={cn(\n        \"h-5 w-[1.75rem] shrink-0 text-[var(--brand,currentColor)]\",\n        \"[[data-tone=brand]_&]:text-current [[data-tone=brand]_&]:opacity-55\",\n      )}\n      fill=\"currentColor\"\n    >\n      <path d=\"M0 24V13.4C0 6 4.2 1 11.4 0l1.2 3.6C8.6 4.8 6.4 7.4 6.4 10.6h5.4V24H0Zm18.8 0V13.4C18.8 6 23 1 30.2 0L31.4 3.6c-4 1.2-6.2 3.8-6.2 7h5.4V24H18.8Z\" />\n    </svg>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Um depoimento só em tamanho de dobra, citação em corpo de manchete e assinatura depois",
      "evite": "Vários depoimentos de tipos diferentes: testimonial-wall; pilha arrastável, card-stack"
    }
  },
  "categories": [
    "conteudo",
    "depoimento",
    "prova-social",
    "citacao",
    "cliente",
    "retrato"
  ],
  "type": "registry:ui"
}