{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"dot-wave","type":"registry:ui","title":"Dot Wave","description":"A perspective field of dots rolling in layered sine waves, fading into the horizon. Canvas 2D.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/dot-wave.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nexport interface DotWaveProps {\n  /** Color of the nearest dots. */\n  color?: string;\n  /** Color the dots blend toward on the horizon. */\n  farColor?: string;\n  /** Animation speed multiplier; 0 freezes the wave. */\n  speed?: number;\n  /** Wave height. */\n  amplitude?: number;\n  /** Wave length multiplier; higher means tighter ripples. */\n  scale?: number;\n  /** Distance between dots; lower is denser. */\n  spacing?: number;\n  /** Radius of the nearest dots in px. */\n  dotSize?: 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 Z_NEAR = 0.55;\nconst Z_FAR = 7;\nconst CAM_H = 0.5;\n\nexport function DotWave({\n  color = \"#5eead4\",\n  farColor = \"#818cf8\",\n  speed = 1,\n  amplitude = 1,\n  scale = 1,\n  spacing = 1,\n  dotSize = 2.4,\n  className,\n}: DotWaveProps) {\n  const ref = useRef<HTMLCanvasElement>(null);\n  const opts = useRef({ a: rgb(color), b: rgb(farColor), speed, amplitude, scale, spacing, dotSize });\n  const redraw = useRef<() => void>(undefined);\n\n  useEffect(() => {\n    opts.current = { a: rgb(color), b: rgb(farColor), speed, amplitude, scale, spacing, dotSize };\n    redraw.current?.();\n  }, [color, farColor, speed, amplitude, scale, spacing, dotSize]);\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    let w = 0;\n    let h = 0;\n    let t = 3;\n    let raf = 0;\n    let last = 0;\n\n    const wave = (x: number, z: number, k: number) =>\n      Math.sin(x * 1.1 * k + t * 1.2) * 0.5 +\n      Math.sin(z * 1.4 * k - t * 1.5) * 0.6 +\n      Math.sin((x * 0.7 + z) * 0.9 * k + t * 0.8) * 0.45;\n\n    const draw = (dt: number) => {\n      const o = opts.current;\n      t += dt * o.speed;\n      ctx.clearRect(0, 0, w, h);\n      const f = h * 0.95;\n      const horizon = h * 0.28;\n      const step = 0.11 * Math.max(o.spacing, 0.3);\n      const amp = 0.13 * o.amplitude;\n      const k = o.scale;\n      const rows = Math.ceil((Z_FAR - Z_NEAR) / step);\n\n      // Far rows first so near dots paint on top.\n      for (let r = rows; r >= 0; r--) {\n        const z = Z_NEAR + r * step;\n        const depth = (z - Z_NEAR) / (Z_FAR - Z_NEAR);\n        const alpha = Math.pow(1 - depth, 1.4) * Math.min(1, (z - Z_NEAR) / 0.3 + 0.35);\n        if (alpha < 0.01) continue;\n        const pz = f / z;\n        const rad = Math.max(0.45, (o.dotSize * Z_NEAR) / z);\n        const halfX = (w * 0.5 + rad) / pz;\n        const c0 = Math.ceil(-halfX / step);\n        const c1 = Math.floor(halfX / step);\n        const mix = Math.pow(depth, 0.7);\n        const cr = Math.round(o.a[0] + (o.b[0] - o.a[0]) * mix);\n        const cg = Math.round(o.a[1] + (o.b[1] - o.a[1]) * mix);\n        const cb = Math.round(o.a[2] + (o.b[2] - o.a[2]) * mix);\n\n        // Two paths per row: regular dots and brighter crest dots.\n        ctx.beginPath();\n        let crest = false;\n        for (let c = c0; c <= c1; c++) {\n          const x = c * step;\n          const v = wave(x, z, k);\n          if (v > 0.75) {\n            crest = true;\n            continue;\n          }\n          const sx = w * 0.5 + x * pz;\n          const sy = horizon + (CAM_H - v * amp) * pz;\n          ctx.moveTo(sx + rad, sy);\n          ctx.arc(sx, sy, rad, 0, Math.PI * 2);\n        }\n        ctx.fillStyle = `rgba(${cr},${cg},${cb},${alpha * 0.8})`;\n        ctx.fill();\n\n        if (crest) {\n          ctx.beginPath();\n          for (let c = c0; c <= c1; c++) {\n            const x = c * step;\n            const v = wave(x, z, k);\n            if (v <= 0.75) continue;\n            const sx = w * 0.5 + x * pz;\n            const sy = horizon + (CAM_H - v * amp) * pz;\n            ctx.moveTo(sx + rad * 1.15, sy);\n            ctx.arc(sx, sy, rad * 1.15, 0, Math.PI * 2);\n          }\n          ctx.fillStyle = `rgba(${(cr + 255) >> 1},${(cg + 255) >> 1},${(cb + 255) >> 1},${alpha})`;\n          ctx.fill();\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 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  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: \"linear-gradient(to bottom, transparent 20%, #000 45%)\",\n        WebkitMaskImage: \"linear-gradient(to bottom, transparent 20%, #000 45%)\",\n      }}\n    />\n  );\n}\n"}]}