{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gleam",
  "title": "Gleam",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "A superfície que sabe onde o cursor está: luz sob o ponteiro, borda acesa do lado da luz e inclinação de 5 graus. Quatro números por quadro no JS, o desenho inteiro no CSS. Nasce inclinado e aceso (rest).",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/visant/ui/gleam.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Gleam — a superfície que sabe onde o cursor está.\n *\n * Três coisas ao mesmo tempo, e é a soma que faz o card parecer objeto em vez\n * de retângulo: a luz que segue o ponteiro, a borda que acende só do lado de\n * onde a luz vem, e a inclinação de 5 graus que dá espessura à peça.\n *\n * ── Por que não é o `use-pointer-parallax` ─────────────────────────────────\n *\n * O hook da casa escreve `--px/--py` a partir do ponteiro na JANELA, para uma\n * colagem inteira se mexer junto — profundidade de cena. Aqui a coordenada é\n * LOCAL ao elemento (0→1 dentro da própria caixa), porque a luz tem que nascer\n * exatamente sob o cursor em cada card de uma grade de doze. São medidas\n * diferentes do mesmo gesto; usar o hook de cena aqui acenderia os doze cards\n * no mesmo canto.\n *\n * Também não é o `@visant/repulsive-hover`, que FOGE do cursor. Este reconhece\n * o cursor e fica.\n *\n * ── A divisão de trabalho ──────────────────────────────────────────────────\n *\n * O JS escreve quatro números e para: `--gx/--gy` (0→1, onde está o cursor) e\n * `--rx/--ry` (graus de inclinação). Todo o desenho é CSS. Duas consequências\n * boas: trocar a estética não encosta em JavaScript, e o navegador compõe o\n * `transform` sem passar pelo layout.\n *\n * ── As armadilhas ──────────────────────────────────────────────────────────\n *\n * 1. **Sem ponteiro fino, o código é morto e nocivo.** Num telefone `pointermove`\n *    dispara a cada toque e a \"luz\" fica presa no último ponto tocado. A guarda\n *    de `(hover: hover) and (pointer: fine)` é checada ANTES de assinar o\n *    listener, não dentro dele.\n * 2. **`transition` no transform durante o arrasto do ponteiro atrasa a luz.**\n *    A suavização fica em 120ms só — o suficiente pra tirar o degrau do\n *    primeiro quadro, curto demais pra virar arrasto. Já a VOLTA ao repouso\n *    (`pointerleave`) é mais longa de propósito: a saída é que precisa de calma.\n * 3. **A luz não pode capturar clique.** A camada de brilho é `pointer-events:\n *    none`, senão o card inteiro vira uma área morta sobre o próprio link.\n * 4. **`perspective` mora no elemento externo, o giro no interno.** Perspectiva\n *    no mesmo nó que rotaciona não produz profundidade — produz um losango.\n * 5. **O foco de teclado não tem cursor.** Quem chega de Tab não recebe luz\n *    nenhuma; recebe o `--focus` da casa, que é o contrato de foco visível.\n *    Efeito de ponteiro nunca substitui estado de foco.\n * 6. **Raio de luz em pixel fixo não é holofote, é lavagem.** A primeira versão\n *    usava `240px circle`. Medido na vitrine: card de 152×87, canto mais\n *    distante a ~174px — a \"luz\" cobria a peça inteira com cor uniforme e o\n *    efeito de holofote não existia em card nenhum de grade. O gradiente agora\n *    não declara raio: sem tamanho explícito o padrão é `farthest-corner`, e as\n *    paradas em porcentagem passam a ser relativas à PRÓPRIA caixa. Um card de\n *    150px e um herói de 900px ganham a mesma leitura sem prop nenhuma.\n * 7. **`prefers-reduced-motion` desliga a inclinação, não a peça.** A luz de\n *    hover é opacidade — informação de \"estou aqui\" — e fica. O que sai é o\n *    movimento no espaço, que é o que embrulha o estômago.\n *\n * 8. **Parado, o card já é objeto.** O report pediu \"nascer em perspectiva e\n *    brilhando\": sem ponteiro em cima, um card plano e apagado é só um\n *    retângulo, e no toque (onde não existe hover) ele nunca acendia. Com\n *    `rest` (ligado por padrão) a peça repousa numa pose: a luz no canto de\n *    cima à esquerda e a inclinação que essa luz daria. O ponteiro assume\n *    quando entra e, ao sair, o card volta a essa pose em vez de apagar.\n *    `rest={false}` devolve o comportamento antigo (plano e apagado).\n *    Sob `prefers-reduced-motion` e em `flat`, a pose fica só com a luz.\n *\n * Sem cor própria: o brilho é `color-mix` sobre `--brand`, então o card acende\n * na cor da marca instalada e em nenhuma outra.\n */\n\nexport interface GleamProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Graus de inclinação no canto. Acima de ~8 o card lê como brinquedo. */\n  tilt?: number;\n  /** Força da luz, 0→1. */\n  glow?: number;\n  /**\n   * Cor da luz e da borda acesa. Qualquer cor CSS; o padrão é a marca.\n   *\n   * Existe para o caso em que uma superfície precisa acender numa cor que NÃO é\n   * a da marca — o card de erro, o de destaque sazonal, a grade em que cada\n   * categoria tem a sua tinta. Sem isto a única saída era redefinir `--brand`\n   * num ancestral, o que repinta também o conteúdo do card (botão, pastilha,\n   * ícone), e aí \"mudar a cor da luz\" vira \"trocar a marca dentro desse pedaço\n   * da página\".\n   *\n   * A troca é escrita nas DUAS camadas de luz, e só nelas: o `--brand` local\n   * morre no `<span>` do brilho e não desce para os filhos.\n   */\n  tint?: string;\n  /** Desliga o giro e mantém luz e borda. Para card dentro de grade densa. */\n  flat?: boolean;\n  /** Elemento raiz. `a` quando o card inteiro leva a algum lugar. */\n  as?: \"div\" | \"a\" | \"article\" | \"li\";\n  /**\n   * Pose de repouso: sem ponteiro em cima, o card fica inclinado e aceso, com a\n   * luz no canto de cima à esquerda. `false` deixa plano e apagado.\n   */\n  rest?: boolean;\n}\n\n/** Onde a luz repousa (0→1 na caixa). Canto de cima à esquerda, de onde a luz costuma vir. */\nconst REPOUSO = { x: 0.24, y: 0.18 } as const;\n\n/** A inclinação que a luz em (x, y) daria. Uma conta só pro ponteiro e pro repouso. */\nconst giro = (x: number, y: number, tilt: number) => ({\n  ry: (x - 0.5) * 2 * tilt,\n  rx: (0.5 - y) * 2 * tilt,\n});\n\nexport function Gleam({\n  tilt = 5,\n  glow = 1,\n  tint,\n  flat = false,\n  rest = true,\n  as: Tag = \"div\",\n  className,\n  children,\n  style,\n  ...props\n}: GleamProps) {\n  const ref = React.useRef<HTMLDivElement>(null);\n\n  React.useEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n    // A pose de repouso é escrita aqui, e não no `style` do JSX: o mesmo nó\n    // recebe as escritas do ponteiro, e o React reescreveria a pose por cima\n    // delas no próximo render.\n    const repousar = () => {\n      if (!rest) {\n        el.style.setProperty(\"--rx\", \"0\");\n        el.style.setProperty(\"--ry\", \"0\");\n        el.dataset.lit = \"\";\n        return;\n      }\n      const { rx, ry } = giro(REPOUSO.x, REPOUSO.y, tilt);\n      el.style.setProperty(\"--gx\", String(REPOUSO.x));\n      el.style.setProperty(\"--gy\", String(REPOUSO.y));\n      el.style.setProperty(\"--rx\", !reduced && !flat ? rx.toFixed(3) : \"0\");\n      el.style.setProperty(\"--ry\", !reduced && !flat ? ry.toFixed(3) : \"0\");\n      el.dataset.lit = \"1\";\n    };\n    repousar();\n\n    if (!window.matchMedia(\"(hover: hover) and (pointer: fine)\").matches) return;\n    let frame = 0;\n    let pending: { x: number; y: number } | null = null;\n\n    // Uma escrita por quadro. `pointermove` chega mais rápido que o navegador\n    // desenha, e escrever custom property duas vezes no mesmo quadro é trabalho\n    // que ninguém vê.\n    const flush = () => {\n      frame = 0;\n      if (!pending) return;\n      const { x, y } = pending;\n      el.style.setProperty(\"--gx\", x.toFixed(4));\n      el.style.setProperty(\"--gy\", y.toFixed(4));\n      if (!reduced && !flat) {\n        const { rx, ry } = giro(x, y, tilt);\n        el.style.setProperty(\"--ry\", ry.toFixed(3));\n        el.style.setProperty(\"--rx\", rx.toFixed(3));\n      }\n    };\n\n    const onMove = (e: PointerEvent) => {\n      const r = el.getBoundingClientRect();\n      pending = { x: (e.clientX - r.left) / r.width, y: (e.clientY - r.top) / r.height };\n      if (!frame) frame = requestAnimationFrame(flush);\n    };\n\n    const onLeave = () => {\n      if (frame) cancelAnimationFrame(frame);\n      frame = 0;\n      pending = null;\n      // Sem `rest`, a luz não volta ao centro: ela apaga onde estava. Voltar ao\n      // centro desenharia um risco atravessando o card no caminho de saída. Com\n      // `rest`, ela volta ao canto de repouso, e a transição de 320ms da saída\n      // leva o giro junto.\n      repousar();\n    };\n\n    const onEnter = () => {\n      el.dataset.lit = \"1\";\n    };\n\n    el.addEventListener(\"pointerenter\", onEnter);\n    el.addEventListener(\"pointermove\", onMove);\n    el.addEventListener(\"pointerleave\", onLeave);\n    return () => {\n      if (frame) cancelAnimationFrame(frame);\n      el.removeEventListener(\"pointerenter\", onEnter);\n      el.removeEventListener(\"pointermove\", onMove);\n      el.removeEventListener(\"pointerleave\", onLeave);\n    };\n  }, [tilt, flat, rest]);\n\n  // `React.ElementType` em vez de genérico completo: a peça aceita quatro tags\n  // conhecidas e o ganho de tipar cada uma delas separadamente não paga a\n  // assinatura ilegível que isso produz na ficha do componente.\n  const Root = Tag as React.ElementType;\n\n  // Redefinir `--brand` NA camada, não no card: o CSS do brilho já lê `--brand`,\n  // então a troca é de uma propriedade só, e ela não vaza para o conteúdo.\n  const cor = tint ? ({ \"--brand\": tint } as React.CSSProperties) : undefined;\n\n  return (\n    <div className=\"gleam-stage\" style={{ \"--glow\": glow } as React.CSSProperties}>\n      <Root ref={ref} className={cn(\"gleam\", className)} style={style} {...props}>\n        <span aria-hidden=\"true\" className=\"gleam-light\" style={cor} />\n        <span aria-hidden=\"true\" className=\"gleam-edge\" style={cor} />\n        {children}\n      </Root>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "css": {
    "@layer components": {
      ".gleam-stage": {
        "perspective": "900px"
      },
      ".gleam": {
        "position": "relative",
        "isolation": "isolate",
        "border-radius": "var(--r-surface, 16px)",
        "border": "1px solid var(--border)",
        "background": "var(--card)",
        "transform-style": "preserve-3d",
        "transform": "rotateX(calc(var(--rx, 0) * 1deg)) rotateY(calc(var(--ry, 0) * 1deg))",
        "transition": "transform 120ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1)), box-shadow var(--dur-base, 200ms) var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1))"
      },
      ".gleam:not([data-lit=\"1\"])": {
        "transition-duration": "320ms"
      },
      ".gleam:focus-visible": {
        "outline": "none",
        "box-shadow": "var(--focus, 0 0 0 2px var(--ring))"
      },
      ".gleam-light": {
        "position": "absolute",
        "inset": "0",
        "border-radius": "inherit",
        "pointer-events": "none",
        "z-index": "0",
        "opacity": "0",
        "transition": "opacity var(--dur-base, 200ms) var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1))",
        "background": "radial-gradient(circle at calc(var(--gx, 0.5) * 100%) calc(var(--gy, 0.5) * 100%), color-mix(in oklab, var(--brand) 26%, transparent), transparent 55%)"
      },
      ".gleam[data-lit=\"1\"] .gleam-light": {
        "opacity": "var(--glow, 1)"
      },
      ".gleam-edge": {
        "position": "absolute",
        "inset": "0",
        "border-radius": "inherit",
        "pointer-events": "none",
        "z-index": "1",
        "padding": "1px",
        "background": "radial-gradient(circle at calc(var(--gx, 0.5) * 100%) calc(var(--gy, 0.5) * 100%), color-mix(in oklab, var(--brand) 65%, transparent), transparent 45%)",
        "-webkit-mask": "linear-gradient(black 0 0) content-box, linear-gradient(black 0 0)",
        "mask": "linear-gradient(black 0 0) content-box, linear-gradient(black 0 0)",
        "-webkit-mask-composite": "xor",
        "mask-composite": "exclude",
        "opacity": "0",
        "transition": "opacity var(--dur-base, 200ms) var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1))"
      },
      ".gleam[data-lit=\"1\"] .gleam-edge": {
        "opacity": "1"
      },
      ".gleam > :not(.gleam-light):not(.gleam-edge)": {
        "position": "relative",
        "z-index": "2"
      }
    },
    "@media (prefers-reduced-motion: reduce)": {
      ".gleam": {
        "transform": "none",
        "transition": "none"
      }
    }
  },
  "meta": {
    "uso": {
      "quando": "Grade de cards em que cada um acende luz sob o cursor e inclina leve, em vitrine ou pricing",
      "evite": "Card único que inclina forte em 3D: tilt-card; profundidade da cena inteira, use-pointer-parallax"
    }
  },
  "categories": [
    "efeitos",
    "hover",
    "card",
    "ponteiro",
    "tilt",
    "luz",
    "spotlight",
    "magnetico"
  ],
  "type": "registry:ui"
}