{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "grid-dots",
  "title": "Grid Dots",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "Malha de pontos de fundo, o papel milimetrado que dá profundidade sem competir.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/visant/ui/grid-dots.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Malha de pontos de fundo — o \"papel milimetrado\" das telas de ferramenta.\n *\n * Um `radial-gradient` repetido, não SVG e não canvas: zero nós no DOM, zero\n * requisição, e o navegador desenha no compositor. Numa página com sidebar,\n * canvas e grade de resultados, isso é a diferença entre textura de graça e mais\n * um elemento para o layout engine resolver.\n *\n * A cor vem de `currentColor` (default: `--foreground`), então inverte sozinha\n * entre tema claro e escuro sem hook de tema nem prop de cor.\n *\n * Opacidade default 0.07 é o teto útil: acima disso a malha compete com o\n * conteúdo em vez de dar profundidade.\n *\n * ── `spotlight`: a malha que responde ao ponteiro ───────────────────────────\n *\n * Malha parada é papel de parede — dá profundidade e para por aí. Com\n * `spotlight={raio}` a peça ganha o trabalho que justifica existir num hero: os\n * pontos ACENDEM na cor da marca dentro de um círculo que segue o ponteiro, e o\n * resto da superfície continua no cinza de fundo. É o mesmo gesto de \"a luz\n * mostra que a superfície tem textura\" — o fundo deixa de ser decoração e passa\n * a reagir a quem está lendo.\n *\n * Três decisões que fazem isso não virar peso:\n *\n * · **O halo é uma máscara, não um segundo desenho.** A camada acesa é a mesma\n *   malha com `mask-image` radial. Nenhum ponto é recalculado; o compositor\n *   move um gradiente.\n * · **Uma escrita por quadro.** `pointermove` dispara dezenas de vezes por\n *   segundo; escrever custom property em cada evento força recálculo de estilo\n *   fora do quadro.\n * · **Só onde há ponteiro fino.** Em toque não existe hover: o halo ficaria\n *   preso no último ponto tocado, o que é pior que não ter halo. Sem ponteiro\n *   fino a camada acesa nem monta, e a peça é a malha de sempre.\n *\n * O repouso — ponteiro nunca entrou, ou já saiu — é o halo no centro, apagado.\n * Nunca uma superfície em branco esperando gesto.\n */\nexport function GridDots({\n  opacity = 0.07,\n  dotSize = 1.5,\n  spacing = 20,\n  spotlight,\n  spotlightOpacity = 0.55,\n  className,\n}: {\n  /** 0–1. Acima de ~0.1 vira ruído. */\n  opacity?: number;\n  /** Raio do ponto, em px. */\n  dotSize?: number;\n  /** Distância entre pontos, em px. */\n  spacing?: number;\n  /**\n   * Raio em px do halo que segue o ponteiro. Sem isto a malha é estática —\n   * que continua sendo o uso certo em superfície de trabalho.\n   */\n  spotlight?: number;\n  /** 0–1. Alpha dos pontos acesos dentro do halo. */\n  spotlightOpacity?: number;\n  className?: string;\n}) {\n  const ref = React.useRef<HTMLDivElement>(null);\n  const [fino, setFino] = React.useState(false);\n  const aceso = spotlight != null && spotlight > 0 && fino;\n\n  React.useEffect(() => {\n    if (spotlight == null || spotlight <= 0) return;\n    try {\n      setFino(window.matchMedia(\"(hover: hover) and (pointer: fine)\").matches);\n    } catch {\n      setFino(false);\n    }\n  }, [spotlight]);\n\n  React.useEffect(() => {\n    const el = ref.current;\n    // A superfície que recebe o gesto é o pai — o halo é fundo dela, e o próprio\n    // fundo é `pointer-events-none` justamente pra não roubar clique do conteúdo.\n    const alvo = el?.parentElement;\n    if (!el || !alvo || !aceso) return;\n\n    let raf = 0;\n    let x = 0;\n    let y = 0;\n    let lit = 0;\n    const pintar = () => {\n      raf = 0;\n      el.style.setProperty(\"--gx\", `${x}px`);\n      el.style.setProperty(\"--gy\", `${y}px`);\n      el.style.setProperty(\"--lit\", `${lit}`);\n    };\n    const agendar = () => {\n      if (raf) return;\n      raf = requestAnimationFrame(pintar);\n    };\n\n    const mover = (e: PointerEvent) => {\n      if (e.pointerType && e.pointerType !== \"mouse\") return;\n      const r = alvo.getBoundingClientRect();\n      x = e.clientX - r.left;\n      y = e.clientY - r.top;\n      lit = 1;\n      agendar();\n    };\n    const sair = () => {\n      lit = 0;\n      agendar();\n    };\n\n    alvo.addEventListener(\"pointermove\", mover, { passive: true });\n    alvo.addEventListener(\"pointerleave\", sair, { passive: true });\n    return () => {\n      cancelAnimationFrame(raf);\n      alvo.removeEventListener(\"pointermove\", mover);\n      alvo.removeEventListener(\"pointerleave\", sair);\n    };\n  }, [aceso]);\n\n  const malha = {\n    backgroundImage: `radial-gradient(circle, currentColor ${dotSize}px, transparent ${dotSize}px)`,\n    backgroundSize: `${spacing}px ${spacing}px`,\n  } as const;\n\n  return (\n    <div ref={ref} aria-hidden=\"true\" className={cn(\"pointer-events-none absolute inset-0\", className)}>\n      <div className=\"absolute inset-0 text-foreground\" style={{ opacity, ...malha }} />\n      {aceso ? (\n        <div\n          // `text-[var(--brand,…)]`: `brand` não é cor do tema do Tailwind, e a\n          // classe curta `text-brand` não geraria regra nenhuma.\n          className=\"absolute inset-0 text-[var(--brand,currentColor)]\"\n          style={{\n            ...malha,\n            // Sem gesto ainda: halo no centro e `--lit` em 0. O que se vê é a\n            // malha de sempre, nunca uma superfície esperando ponteiro.\n            opacity: `calc(${spotlightOpacity} * var(--lit, 0))`,\n            transition: \"opacity var(--dur-base, 220ms) var(--ease-out, ease)\",\n            maskImage: `radial-gradient(circle ${spotlight}px at var(--gx, 50%) var(--gy, 50%), #000 0%, #000 35%, transparent 100%)`,\n            WebkitMaskImage: `radial-gradient(circle ${spotlight}px at var(--gx, 50%) var(--gy, 50%), #000 0%, #000 35%, transparent 100%)`,\n          }}\n        />\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Fundo parado de malha de pontos que dá profundidade sem competir com o conteúdo",
      "evite": "Fundo que reage ao ponteiro: spring-dots; textura animada, turbulence-field ou shader-voronoi"
    }
  },
  "categories": [
    "efeitos",
    "fundo",
    "grid"
  ],
  "type": "registry:ui"
}