{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ds-grid-pulse",
  "title": "Grid Pulse",
  "description": "Grid de pontos que pulsa de tamanho e opacidade perto do cursor, ocioso fora da interação — fundo de hero interativo.",
  "dependencies": [
    "class-variance-authority"
  ],
  "registryDependencies": [
    "@lema-ds/tokens"
  ],
  "files": [
    {
      "path": "components/ds/ds-grid-pulse.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\n// ── Types ──\n\nexport interface GridPulseProps\n  extends\n    React.HTMLAttributes<HTMLDivElement>,\n    VariantProps<typeof gridPulseVariants> {}\n\n// ── Variants ──\n\nexport const gridPulseVariants = cva(\"absolute inset-0 overflow-hidden\", {\n  variants: {\n    tone: {\n      primary: \"[--gp-color:var(--primary)]\",\n      violet: \"[--gp-color:var(--highlight-violet)]\",\n      sky: \"[--gp-color:var(--highlight-sky)]\",\n      neutral: \"[--gp-color:var(--foreground)]\",\n    },\n    radius: {\n      sm: \"\",\n      md: \"\",\n      lg: \"\",\n    },\n  },\n  defaultVariants: { tone: \"primary\", radius: \"md\" },\n})\n\nconst RADIUS_CONFIG = {\n  sm: { spacing: 14, influence: 70, maxScale: 2.2 },\n  md: { spacing: 20, influence: 100, maxScale: 2.6 },\n  lg: { spacing: 28, influence: 140, maxScale: 3 },\n} as const\n\ninterface Dot {\n  x: number\n  y: number\n}\n\n// ── Component ──\n\n/**\n * Grid of dots that pulses in size and opacity near the cursor. Stays\n * idle (no RAF) while the pointer is not over the container, so CPU\n * cost outside interaction is practically zero. Under\n * `prefers-reduced-motion`, it only draws the static grid and does not listen\n * to pointer events.\n */\nexport const GridPulse = React.forwardRef<HTMLDivElement, GridPulseProps>(\n  ({ className, tone = \"primary\", radius = \"md\", ...props }, ref) => {\n    const containerRef = React.useRef<HTMLDivElement>(null)\n    const canvasRef = React.useRef<HTMLCanvasElement>(null)\n\n    const setRefs = React.useCallback(\n      (node: HTMLDivElement | null) => {\n        containerRef.current = node\n        if (typeof ref === \"function\") ref(node)\n        else if (ref)\n          (ref as React.RefObject<HTMLDivElement | null>).current = node\n      },\n      [ref]\n    )\n\n    React.useEffect(() => {\n      const container = containerRef.current\n      const canvas = canvasRef.current\n      if (!container || !canvas) return\n\n      const ctx = canvas.getContext(\"2d\", { alpha: true })\n      if (!ctx) return\n\n      const reduceMotion = window.matchMedia(\n        \"(prefers-reduced-motion: reduce)\"\n      ).matches\n      const { spacing, influence, maxScale } = RADIUS_CONFIG[radius ?? \"md\"]\n      const color =\n        getComputedStyle(container).getPropertyValue(\"--gp-color\").trim() ||\n        \"currentColor\"\n\n      let dots: Dot[] = []\n      let width = 0\n      let height = 0\n      let dpr = 1\n      let frameId = 0\n      let running = false\n      let visible = true\n      const pointer = { x: -Infinity, y: -Infinity, active: false }\n\n      const buildGrid = () => {\n        dots = []\n        for (let x = spacing / 2; x < width; x += spacing) {\n          for (let y = spacing / 2; y < height; y += spacing) {\n            dots.push({ x, y })\n          }\n        }\n      }\n\n      const resize = () => {\n        dpr = Math.min(window.devicePixelRatio || 1, 2)\n        width = container.clientWidth\n        height = container.clientHeight\n        canvas.width = Math.max(1, Math.floor(width * dpr))\n        canvas.height = Math.max(1, Math.floor(height * dpr))\n        canvas.style.width = `${width}px`\n        canvas.style.height = `${height}px`\n        ctx.setTransform(dpr, 0, 0, dpr, 0, 0)\n        buildGrid()\n      }\n\n      const draw = () => {\n        ctx.clearRect(0, 0, width, height)\n        ctx.fillStyle = color\n        for (const dot of dots) {\n          let scale = 1\n          let opacity = 0.25\n          if (pointer.active) {\n            const dx = dot.x - pointer.x\n            const dy = dot.y - pointer.y\n            const dist = Math.sqrt(dx * dx + dy * dy)\n            if (dist < influence) {\n              const t = 1 - dist / influence\n              scale = 1 + t * (maxScale - 1)\n              opacity = 0.25 + t * 0.55\n            }\n          }\n          ctx.globalAlpha = opacity\n          ctx.beginPath()\n          ctx.arc(dot.x, dot.y, 1.4 * scale, 0, Math.PI * 2)\n          ctx.fill()\n        }\n        ctx.globalAlpha = 1\n      }\n\n      const tick = () => {\n        if (!running) return\n        draw()\n        frameId = requestAnimationFrame(tick)\n      }\n\n      const start = () => {\n        if (running) return\n        running = true\n        frameId = requestAnimationFrame(tick)\n      }\n\n      const stop = () => {\n        running = false\n        if (frameId) cancelAnimationFrame(frameId)\n      }\n\n      const handlePointerMove = (event: PointerEvent) => {\n        if (!visible) return\n        const rect = container.getBoundingClientRect()\n        pointer.x = event.clientX - rect.left\n        pointer.y = event.clientY - rect.top\n        pointer.active = true\n        start()\n      }\n\n      const handlePointerLeave = () => {\n        pointer.active = false\n        stop()\n        draw()\n      }\n\n      resize()\n      draw()\n\n      if (!reduceMotion) {\n        container.addEventListener(\"pointermove\", handlePointerMove)\n        container.addEventListener(\"pointerleave\", handlePointerLeave)\n      }\n\n      const resizeObserver = new ResizeObserver(() => {\n        resize()\n        draw()\n      })\n      resizeObserver.observe(container)\n\n      const intersectionObserver = new IntersectionObserver(\n        ([entry]) => {\n          visible = entry.isIntersecting\n          if (!visible) {\n            pointer.active = false\n            stop()\n          }\n        },\n        { threshold: 0 }\n      )\n      intersectionObserver.observe(container)\n\n      return () => {\n        stop()\n        container.removeEventListener(\"pointermove\", handlePointerMove)\n        container.removeEventListener(\"pointerleave\", handlePointerLeave)\n        resizeObserver.disconnect()\n        intersectionObserver.disconnect()\n      }\n    }, [radius, tone])\n\n    return (\n      <div\n        ref={setRefs}\n        aria-hidden=\"true\"\n        data-slot=\"grid-pulse\"\n        className={cn(gridPulseVariants({ tone, radius }), className)}\n        {...props}\n      >\n        <canvas ref={canvasRef} className=\"block size-full\" />\n      </div>\n    )\n  }\n)\nGridPulse.displayName = \"GridPulse\"\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}
