{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"pixel-background","type":"registry:ui","title":"Pixel Background","description":"A coarse pixel mosaic whose tiles drift through a palette in slow waves and light up around the cursor. Canvas 2D.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/pixel-background.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nexport interface PixelBackgroundProps {\n  /** Px per tile. */\n  pixelSize?: number;\n  /** Px between tiles. */\n  gap?: number;\n  /** Palette the color waves sweep through, dark to bright. */\n  colors?: string[];\n  /** Wave speed multiplier; 0 freezes the mosaic. */\n  speed?: number;\n  /** Overall tile brightness, 0 to 1. */\n  brightness?: number;\n  /** Tiles brighten near the cursor. */\n  interactive?: boolean;\n  /** Cursor glow radius in px. */\n  radius?: 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\nconst tileHash = (i: number, j: number) => {\n  const s = Math.sin(i * 127.1 + j * 311.7) * 43758.5453;\n  return s - Math.floor(s);\n};\n\nexport function PixelBackground({\n  pixelSize = 26,\n  gap = 2,\n  colors = [\"#0b1026\", \"#1e3a8a\", \"#7c3aed\", \"#f472b6\"],\n  speed = 1,\n  brightness = 0.7,\n  interactive = true,\n  radius = 160,\n  className,\n}: PixelBackgroundProps) {\n  const ref = useRef<HTMLCanvasElement>(null);\n  const pal = (colors.length ? colors : [\"#000000\"]).map(rgb);\n  const opts = useRef({ pal, pixelSize, gap, speed, brightness, interactive, radius });\n  const redraw = useRef<() => void>(undefined);\n\n  useEffect(() => {\n    opts.current = { pal: (colors.length ? colors : [\"#000000\"]).map(rgb), pixelSize, gap, speed, brightness, interactive, radius };\n    redraw.current?.();\n  }, [colors, pixelSize, gap, speed, brightness, interactive, radius]);\n\n  useEffect(() => {\n    const canvas = ref.current!;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n    const area = canvas.parentElement ?? canvas;\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    const mouse = { x: 0, y: 0, on: 0, target: 0 };\n    let w = 0;\n    let h = 0;\n    let t = 7;\n    let raf = 0;\n    let last = 0;\n\n    const draw = (dt: number) => {\n      const o = opts.current;\n      t += dt * o.speed;\n      mouse.on += (mouse.target - mouse.on) * (1 - Math.exp(-dt * 6));\n      const s = Math.max(4, o.pixelSize);\n      const gp = Math.min(Math.max(o.gap, 0), s - 1);\n      const pal = o.pal;\n      const top = pal.length - 1;\n      const r2 = o.radius * o.radius;\n      const cols = Math.ceil(w / s);\n      const rows = Math.ceil(h / s);\n      const ox = (w - cols * s) / 2;\n      const oy = (h - rows * s) / 2;\n      const cx = w / 2;\n      const cy = h / 2;\n\n      ctx.clearRect(0, 0, w, h);\n      for (let j = 0; j < rows; j++) {\n        for (let i = 0; i < cols; i++) {\n          const x = ox + i * s;\n          const y = oy + j * s;\n          const px = x * 0.004;\n          const py = y * 0.004;\n          const dc = Math.hypot(x - cx, y - cy) * 0.006;\n          // Several slow interfering waves give the mosaic its drifting color bands.\n          let v =\n            Math.sin(px * 1.3 + t * 0.35) +\n            Math.sin(py * 1.7 - t * 0.28) +\n            Math.sin((px + py) * 1.1 + t * 0.22) +\n            Math.sin(dc - t * 0.45);\n          v = v * 0.125 + 0.5;\n          const n = tileHash(i, j);\n          v = Math.min(1, Math.max(0, v + (n - 0.5) * 0.14));\n\n          let boost = 0;\n          if (o.interactive && mouse.on > 0.001) {\n            const dx = x + s / 2 - mouse.x;\n            const dy = y + s / 2 - mouse.y;\n            boost = Math.exp(-(dx * dx + dy * dy) / r2) * mouse.on;\n          }\n          const pos = Math.min(1, v + boost * 0.25) * top;\n          const k = Math.min(top - 1, Math.floor(pos));\n          const f = top > 0 ? pos - Math.max(k, 0) : 0;\n          const a = pal[Math.max(k, 0)];\n          const b = pal[Math.min(Math.max(k, 0) + 1, top)];\n          const lum = o.brightness * (0.55 + 0.45 * v) + boost * 0.45;\n          const mixW = boost * 0.18;\n          const cr = ((a[0] + (b[0] - a[0]) * f) * lum) * (1 - mixW) + 255 * mixW;\n          const cg = ((a[1] + (b[1] - a[1]) * f) * lum) * (1 - mixW) + 255 * mixW;\n          const cb = ((a[2] + (b[2] - a[2]) * f) * lum) * (1 - mixW) + 255 * mixW;\n          ctx.fillStyle = `rgb(${cr | 0},${cg | 0},${cb | 0})`;\n          ctx.fillRect(x + gp / 2, y + gp / 2, s - gp, s - gp);\n        }\n      }\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      draw(0);\n    };\n\n    const onMove = (e: PointerEvent) => {\n      const r = canvas.getBoundingClientRect();\n      mouse.x = e.clientX - r.left;\n      mouse.y = e.clientY - r.top;\n      mouse.target = 1;\n      if (!raf) {\n        mouse.on = 1;\n        draw(0);\n      }\n    };\n    const onLeave = () => {\n      mouse.target = 0;\n      if (!raf) {\n        mouse.on = 0;\n        draw(0);\n      }\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    area.addEventListener(\"pointermove\", onMove, { passive: true });\n    area.addEventListener(\"pointerleave\", onLeave);\n\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      io.disconnect();\n      redraw.current = undefined;\n      area.removeEventListener(\"pointermove\", onMove);\n      area.removeEventListener(\"pointerleave\", onLeave);\n    };\n  }, []);\n\n  return (\n    <canvas\n      ref={ref}\n      aria-hidden\n      className={className}\n      style={{ position: \"absolute\", inset: 0, width: \"100%\", height: \"100%\", pointerEvents: \"none\" }}\n    />\n  );\n}\n"}]}