{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "empty-state",
  "title": "Empty State",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "O estado vazio canônico: arte por slot, frase do que vai aparecer ali e no máximo uma ação. Com ou sem caixa.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/visant/ui/empty-state.tsx",
      "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Empty State — o estado vazio da casa, com a imagem trabalhando.\n *\n * ── Por que isto é um item, e não uma `<div>` que cada tela escreve ────────\n *\n * Na origem eram quatro rotas com quatro caixas `card px-6 py-12 text-center`\n * escritas à mão, cada uma com uma frase e mais nada. Quatro caixas iguais é\n * uma caixa duplicada quatro vezes: bastava alguém melhorar uma pras outras\n * três ficarem pra trás. E o vazio é justamente a tela que ninguém volta pra\n * melhorar, porque em desenvolvimento quase nunca se vê.\n *\n * A regra de entrada do registry exige estado vazio em todo item. Faltava o\n * estado vazio **canônico**.\n *\n * ── Por que a arte entra AQUI e não no meio do conteúdo ────────────────────\n *\n * Estado vazio é o único momento em que a tela não tem o que mostrar e ainda\n * precisa dizer o que vai acontecer ali. É a superfície onde a imagem trabalha\n * em vez de enfeitar. Com conteúdo na tela, a mesma imagem viraria concorrente\n * do que a pessoa veio ver.\n *\n * O véu sobre a arte não é decoração: sem ele o texto fica legível em três das\n * cinco imagens — as que têm luz de janela — e ninguém descobre isso até um\n * cliente reclamar. Contraste garantido custa uma `<div>`.\n *\n * ── O véu estava apagando a arte que ele existe pra proteger ───────────────\n *\n * Medido na vitrine: o gradiente ia de `--card` a **20% de opacidade no topo**\n * até `--card` **opaco em 96%** da faixa. Ou seja, a arte nunca aparecia mais\n * que 20%, e nos últimos 4% já era só a cor do card. Na tela a faixa lia como\n * uma tira bege vazia — o defeito que virou o \"mal posicionado\" no report: não\n * era posição, era uma superfície que sumiu e deixou um vão em cima do texto.\n *\n * O véu certo é escada, não cortina: transparente onde não tem texto, opaco só\n * onde o texto pousa. `transparent → transparent 42% → --card 100%`. A arte\n * trabalha na metade de cima e a frase ganha fundo sólido na de baixo.\n *\n * ── Por que a régua é o CONTAINER e não a viewport ─────────────────────────\n *\n * Estado vazio quase nunca ocupa a página: ele mora numa coluna, num painel, na\n * segunda metade de um split. Um `px-6` cravado come 48px dos 260px de uma\n * coluna lateral — e `sm:`/`md:` não ajudam, porque eles perguntam o tamanho da\n * JANELA, que naquele momento pode ser 1440px. Aqui a peça é um `@container` e\n * o respiro, a arte e a medida do texto crescem com a caixa que a recebeu.\n *\n * ── A anatomia, e as duas coisas que ela deliberadamente NÃO faz ───────────\n *\n * `arte → eyebrow → título → corpo → ação`, e no máximo **uma** ação. Vazio com\n * três botões não é ajuda, é a interface passando pra frente a decisão que ela\n * não soube tomar.\n *\n * · **Sem tombo, sem tilt.** Aqui a imagem é fundo de uma mensagem, não objeto\n *   pra pegar na mão, e fundo torto atrapalha a leitura da frase por cima.\n * · **A imagem entra por slot.** Cada app tem o seu componente de imagem\n *   otimizada; um item que impõe `<img>` cru obriga o consumidor a abrir mão\n *   dele. Sem `art`, o vazio ainda funciona — vira a caixa de texto centrada,\n *   que é o piso honesto.\n *\n * ── E por que a arte padrão passou a ser DESENHADA, não fotografada ────────\n *\n * Foto num estado vazio é a decisão que envelhece pior: ela vem de um acervo\n * que a marca do cliente não escolheu, custa bytes numa tela que existe pra\n * dizer \"ainda não tem nada\", e some do ar quando alguém troca a pasta de\n * assets. Pior: quatro rotas vazias com a mesma foto viram quatro telas que\n * parecem a mesma tela.\n *\n * `<EmptyArt>` resolve os três: é geometria em SVG derivada de `var(--brand)`\n * por `color-mix`, então ela É a marca de quem instalou — e muda de composição\n * conforme a semente, então \"favoritos\" e \"arquivados\" não se confundem. Zero\n * byte de rede, mesmo desenho em SSR e no cliente, e nada pra quebrar quando o\n * CDN cai. Mesma tese do `@visant/cover-art`, aplicada à faixa em vez da capa.\n * O slot `art` continua aberto: quem tem ilustração de marca passa a ilustração.\n */\n\n// `title` é ReactNode aqui, e o `title` do DOM é string (a tooltip nativa).\n// Omitir é o certo: aceitar os dois nomes no mesmo lugar faria o consumidor\n// escrever um e receber o outro.\nexport interface EmptyStateProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n  /** Slot da arte. `next/image`, `<img>`, ou um `@visant/cover-art`. */\n  art?: React.ReactNode;\n  eyebrow?: React.ReactNode;\n  title: React.ReactNode;\n  /** Uma frase que diz o que vai aparecer ali, não que lamenta a ausência. */\n  children?: React.ReactNode;\n  /** UMA ação. Ver a nota acima. */\n  action?: React.ReactNode;\n  /**\n   * Caixa com borda e fundo de card, arte sangrando no topo. `false` tira a\n   * caixa: o vazio pousa direto na superfície que o recebeu (painel, coluna,\n   * lista), com a arte como um selo pequeno em cima do texto. É o modo certo\n   * quando o vazio mora DENTRO de um card, pra não virar caixa dentro de caixa.\n   */\n  framed?: boolean;\n}\n\nexport function EmptyState({\n  art,\n  eyebrow,\n  title,\n  action,\n  framed = true,\n  className,\n  children,\n  ...props\n}: EmptyStateProps) {\n  return (\n    <div\n      className={cn(\n        // `min-w-0`: sem isto a peça não sabe encolher dentro de um flex ou de\n        // uma coluna de grid, e empurra o irmão do lado em vez de ceder.\n        \"@container min-w-0\",\n        framed && \"overflow-hidden rounded-[var(--r-surface,0.875rem)] border border-border bg-card\",\n        className,\n      )}\n      {...props}\n    >\n      {art && !framed && (\n        // Sem caixa a arte é selo, não faixa: centrada, do tamanho que veio, e\n        // sem véu, porque nenhum texto pousa em cima dela.\n        <div className=\"flex justify-center pt-2\">{art}</div>\n      )}\n\n      {art && framed && (\n        <div className=\"relative\">\n          {art}\n          {/* Contraste garantido sobre qualquer arte, sem apagá-la. Ver a nota. */}\n          <div\n            aria-hidden=\"true\"\n            className=\"absolute inset-0\"\n            style={{\n              background:\n                // 58% e não 42%: com a arte ganhando altura, a cortina começava\n                // ainda no meio do desenho e comia justamente a parte que o\n                // report pediu pra crescer. O contrato continua o mesmo — opaco\n                // onde o texto pousa, transparente onde não pousa.\n                \"linear-gradient(to bottom, transparent 0%, transparent 58%, var(--card) 100%)\",\n            }}\n          />\n        </div>\n      )}\n\n      {/*\n       * O bloco de texto NÃO sobe por cima da arte com margem negativa.\n       * `-mt-6` fazia o eyebrow pousar exatamente na emenda: com arte curta ele\n       * caía no meio do gradiente e com arte alta sobrava um vão. Agora a arte\n       * termina onde termina e o texto começa com o próprio respiro, que é o\n       * único jeito de a peça funcionar com arte de qualquer altura.\n       */}\n      <div\n        className={cn(\n          \"relative px-5 pb-8 text-center @[22rem]:px-7 @[22rem]:pb-10\",\n          art ? \"pt-4\" : \"pt-8 @[22rem]:pt-10\",\n          !framed && \"pb-2 @[22rem]:pb-2\",\n        )}\n      >\n        {eyebrow && (\n          // Caixa de frase, sem caixa-alta espaçada: o eyebrow é contexto (\"em\n          // Favoritos\"), não um selo gritando a categoria.\n          <div className=\"mb-1.5 text-[length:var(--text-apoio,0.75rem)] text-muted-foreground\">\n            {eyebrow}\n          </div>\n        )}\n        {/*\n         * `text-balance` no título: em coluna estreita uma frase de cinco\n         * palavras quebrava com uma órfã sozinha na segunda linha, e num bloco\n         * centrado a órfã é a coisa que mais denuncia texto não desenhado.\n         */}\n        <p className=\"text-balance text-[length:var(--text-destaque,0.9375rem)] font-semibold text-foreground\">\n          {title}\n        </p>\n        {children && (\n          // A medida do corpo em `cqi` e não em `max-w-sm`: 384px fixos não são\n          // medida nenhuma dentro de uma coluna de 260px — ali o `max-w` nunca\n          // morde e a linha vai de borda a borda.\n          <div\n            className=\"mx-auto mt-2 text-pretty text-[length:var(--text-apoio,0.75rem)] leading-relaxed text-muted-foreground\"\n            style={{ maxWidth: \"min(24rem, 92cqi)\" }}\n          >\n            {children}\n          </div>\n        )}\n        {action && <div className=\"mt-5 flex justify-center\">{action}</div>}\n      </div>\n    </div>\n  );\n}\n\n/* ── a arte generativa ─────────────────────────────────────────────────────── */\n\n/**\n * djb2, e ele mora aqui em vez de vir de um `@visant/cover-look`.\n *\n * São cinco linhas contra uma dependência a mais no `shadcn add` de um item que\n * hoje não puxa nada. Se um dia um terceiro item precisar do mesmo hash, aí sim\n * ele vira lib — três consumidores é a régua da casa, não dois.\n *\n * O importante não é o algoritmo e sim a propriedade: mesma semente, mesmo\n * desenho, no servidor e no cliente. `Math.random()` aqui daria hidratação\n * divergente e uma tela vazia que muda de cara a cada F5.\n */\nfunction hashDe(s: string): number {\n  let h = 5381;\n  for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) | 0;\n  return Math.abs(h);\n}\n\n/**\n * A rampa inteira sai de `--brand` misturada com o papel do card.\n *\n * `color-mix` e não opacidade: um `fill` translúcido sobre `--card` dá o mesmo\n * resultado no tema claro e some no escuro, porque ali o papel é escuro e o\n * traço clareia. Misturando com a PRÓPRIA cor do card, a arte acompanha o tema\n * sem uma segunda paleta pra manter. `currentColor` é a rede pra quem instalou\n * e ainda não definiu `--brand`.\n */\nconst tom = (pct: number) =>\n  `color-mix(in oklab, var(--brand, currentColor) ${pct}%, var(--card))`;\n\nexport interface EmptyArtProps extends React.HTMLAttributes<HTMLDivElement> {\n  /**\n   * A semente. Passe o nome da coleção vazia (`\"favoritos\"`, `\"arquivados\"`) e\n   * cada tela vazia do app ganha uma composição própria — de graça, e estável.\n   */\n  seed?: string;\n  /** Densidade do desenho. Menos é mais em faixa baixa. */\n  intensity?: \"quieta\" | \"cheia\";\n}\n\n/**\n * EmptyArt — a faixa desenhada em código pro slot `art`.\n *\n * Três composições, escolhidas pela semente: arcos (o que continua fora do\n * quadro), malha (o inventário que ainda não chegou) e faixas (o corte).\n * Nenhuma delas desenha texto, ícone ou objeto reconhecível de propósito — a\n * frase embaixo é que diz o que a tela é; a arte só dá superfície e marca.\n *\n * `aria-hidden`: é ornamento. Um `role=\"img\"` aqui obrigaria a inventar uma\n * descrição pra uma coisa que não descreve nada, e o leitor de tela passaria a\n * ouvir \"gráfico decorativo\" antes da única frase que importa.\n */\nexport function EmptyArt({\n  seed = \"vazio\",\n  intensity = \"quieta\",\n  className,\n  style,\n  ...props\n}: EmptyArtProps) {\n  const h = hashDe(seed);\n  const familia = h % 3;\n  // Cada aspecto lê uma fatia diferente do MESMO número — nada aqui precisa de\n  // segunda semente, e é isso que mantém o desenho reproduzível.\n  const giro = ((h >> 3) % 21) - 10;\n  const cheia = intensity === \"cheia\";\n\n  return (\n    <div\n      aria-hidden=\"true\"\n      className={cn(\"relative overflow-hidden bg-card\", className)}\n      style={{\n        // O clarão de fundo sai do mesmo token: sem ele o campo fica chapado e\n        // a geometria parece colada, não iluminada.\n        backgroundImage: `radial-gradient(120% 90% at ${20 + (h % 60)}% 0%, ${tom(16)}, var(--card) 70%)`,\n        ...style,\n      }}\n      {...props}\n    >\n      {/* `slice` e não `meet`: a arte é CAMPO, não figura. Ela recorta pra\n          preencher a faixa que receber, em qualquer proporção, em vez de\n          encolher e deixar duas margens vazias — que é como um SVG decorativo\n          denuncia que ninguém pensou na caixa. */}\n      <svg\n        viewBox=\"0 0 400 200\"\n        preserveAspectRatio=\"xMidYMid slice\"\n        className=\"absolute inset-0 h-full w-full\"\n      >\n        {familia === 0 &&\n          Array.from({ length: cheia ? 7 : 5 }).map((_, i) => (\n            // Centro fora do quadro: o arco que não fecha é o que sugere que a\n            // coleção continua além da tela vazia.\n            <circle\n              key={i}\n              cx={64 + (h % 40)}\n              cy={214}\n              r={54 + i * 44}\n              fill=\"none\"\n              stroke={tom(Math.max(6, 34 - i * 5))}\n              strokeWidth={6 + ((h >> (i + 1)) % 5) * 3}\n            />\n          ))}\n\n        {familia === 1 &&\n          Array.from({ length: cheia ? 9 : 7 }).flatMap((_, r) =>\n            Array.from({ length: 15 }).map((_, c) => {\n              const destaque = (r * 15 + c + h) % 13 === 0;\n              return (\n                <circle\n                  key={`${r}-${c}`}\n                  cx={16 + c * 26}\n                  cy={18 + r * 24}\n                  r={destaque ? 7 : 3.5}\n                  fill={tom(destaque ? 60 : 22)}\n                />\n              );\n            }),\n          )}\n\n        {familia === 2 && (\n          <g transform={`rotate(${giro} 200 100)`}>\n            {Array.from({ length: cheia ? 8 : 6 }).map((_, i) => (\n              <rect\n                key={i}\n                x={-40 + i * 74}\n                y={-80}\n                width={16 + ((h >> (i * 2)) % 5) * 11}\n                height={360}\n                fill={tom(12 + (i % 3) * 13)}\n              />\n            ))}\n          </g>\n        )}\n      </svg>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Lista, painel ou busca sem conteúdo ainda: frase do que vai aparecer ali e no máximo uma ação",
      "evite": "Conteúdo carregando: skeleton-rail; vazio com personalidade, ponha eye-tracker no slot da arte"
    }
  },
  "categories": [
    "feedback",
    "vazio",
    "empty",
    "onboarding",
    "primeiro-uso"
  ],
  "type": "registry:ui"
}