{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"grid-background","type":"registry:ui","title":"Grid Background","description":"A fine grid where random cells softly light up and fade out, framed by a radial fade. Canvas 2D.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/grid-background.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nexport interface GridBackgroundProps {\n  /** Px per cell. */\n  cellSize?: number;\n  /** Glow color of lit cells. */\n  color?: string;\n  /** Grid line color. */\n  lineColor?: string;\n  /** Fraction of cells lit at any moment, 0 to 0.3. */\n  density?: number;\n  /** Fade speed multiplier; 0 freezes the grid. */\n  speed?: number;\n  /** Radius of the visible area as a fraction of the box, fades out past it. */\n  fade?: number;\n  className?: string;\n}\n\nfunction rgb(hex: string) {\n  let h = hex.replace(\"#\", \"\");\n  if (h.length === 3) h = h.replace(/./g, \"$&$&\");\n  const n = parseInt(h.padEnd(6, \"0\").slice(0, 6), 16) || 0;\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\ninterface Blip {\n  i: number;\n  j: number;\n  age: number;\n  dur: number;\n}\n\nexport function GridBackground({\n  cellSize = 36,\n  color = \"#c6ff3d\",\n  lineColor = \"#52525b\",\n  density = 0.05,\n  speed = 1,\n  fade = 0.75,\n  className,\n}: GridBackgroundProps) {\n  const ref = useRef<HTMLCanvasElement>(null);\n  const opts = useRef({ cellSize, c: rgb(color), line: rgb(lineColor), density, speed });\n  const redraw = useRef<() => void>(undefined);\n\n  useEffect(() => {\n    opts.current = { cellSize, c: rgb(color), line: rgb(lineColor), density, speed };\n    redraw.current?.();\n  }, [cellSize, color, lineColor, density, speed]);\n\n  useEffect(() => {\n    const canvas = ref.current!;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    const blips: Blip[] = [];\n    let w = 0;\n    let h = 0;\n    let raf = 0;\n    let last = 0;\n\n    const newBlip = (cols: number, rows: number, age = 0): Blip => ({\n      i: Math.floor(Math.random() * cols),\n      j: Math.floor(Math.random() * rows),\n      age,\n      dur: 1.6 + Math.random() * 2.8,\n    });\n\n    const draw = (dt: number) => {\n      const o = opts.current;\n      const s = Math.max(8, o.cellSize);\n      // Center the grid so the radial fade frames it symmetrically.\n      const ox = ((w / 2) % s) - s;\n      const oy = ((h / 2) % s) - s;\n      const cols = Math.ceil((w - ox) / s);\n      const rows = Math.ceil((h - oy) / s);\n      const target = Math.round(cols * rows * Math.min(Math.max(o.density, 0), 0.3));\n      while (blips.length < target) blips.push(newBlip(cols, rows, Math.random() * 3));\n      blips.length = Math.min(blips.length, target);\n\n      ctx.clearRect(0, 0, w, h);\n      const [cr, cg, cb] = o.c;\n      for (let k = 0; k < blips.length; k++) {\n        const b = blips[k];\n        b.age += dt * o.speed;\n        if (b.age >= b.dur) {\n          blips[k] = newBlip(cols, rows);\n          continue;\n        }\n        const p = b.age / b.dur;\n        // Quick swell, long tail.\n        const a = Math.sin(Math.PI * Math.pow(p, 0.6)) ** 2;\n        if (a < 0.01) continue;\n        const x = ox + b.i * s;\n        const y = oy + b.j * s;\n        const g = ctx.createRadialGradient(x + s / 2, y + s / 2, 0, x + s / 2, y + s / 2, s * 1.6);\n        g.addColorStop(0, `rgba(${cr},${cg},${cb},${0.22 * a})`);\n        g.addColorStop(1, `rgba(${cr},${cg},${cb},0)`);\n        ctx.fillStyle = g;\n        ctx.fillRect(x - s, y - s, s * 3, s * 3);\n        ctx.fillStyle = `rgba(${cr},${cg},${cb},${0.16 * a})`;\n        ctx.fillRect(x + 1, y + 1, s - 1, s - 1);\n        ctx.strokeStyle = `rgba(${cr},${cg},${cb},${0.75 * a})`;\n        ctx.lineWidth = 1;\n        ctx.strokeRect(x + 0.5, y + 0.5, s, s);\n      }\n\n      const [lr, lg, lb] = o.line;\n      ctx.strokeStyle = `rgba(${lr},${lg},${lb},0.55)`;\n      ctx.lineWidth = 1;\n      ctx.beginPath();\n      for (let x = ox; x <= w; x += s) {\n        ctx.moveTo(Math.round(x) + 0.5, 0);\n        ctx.lineTo(Math.round(x) + 0.5, h);\n      }\n      for (let y = oy; y <= h; y += s) {\n        ctx.moveTo(0, Math.round(y) + 0.5);\n        ctx.lineTo(w, Math.round(y) + 0.5);\n      }\n      ctx.stroke();\n    };\n\n    const loop = (now: number) => {\n      draw(last ? Math.min((now - last) / 1000, 0.1) : 0);\n      last = now;\n      raf = requestAnimationFrame(loop);\n    };\n\n    const resize = () => {\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      w = canvas.offsetWidth;\n      h = canvas.offsetHeight;\n      canvas.width = Math.max(1, Math.round(w * dpr));\n      canvas.height = Math.max(1, Math.round(h * dpr));\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      blips.length = 0;\n      draw(0);\n    };\n\n    const ro = new ResizeObserver(resize);\n    ro.observe(canvas);\n    const io = new IntersectionObserver(([e]) => {\n      cancelAnimationFrame(raf);\n      raf = 0;\n      last = 0;\n      if (e.isIntersecting && !reduced) raf = requestAnimationFrame(loop);\n    });\n    io.observe(canvas);\n    redraw.current = () => {\n      if (!raf) draw(0);\n    };\n\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      io.disconnect();\n      redraw.current = undefined;\n    };\n  }, []);\n\n  const mask = `radial-gradient(ellipse ${fade * 100}% ${fade * 100}% at 50% 50%, #000 20%, transparent 70%)`;\n  return (\n    <canvas\n      ref={ref}\n      aria-hidden\n      className={className}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        width: \"100%\",\n        height: \"100%\",\n        pointerEvents: \"none\",\n        maskImage: mask,\n        WebkitMaskImage: mask,\n      }}\n    />\n  );\n}\n"}]}