{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"bayer-waves","type":"registry:ui","title":"Bayer Waves","description":"Flowing waves rendered with 8x8 ordered Bayer dithering in two colors; the cursor sends ripples through them. WebGL.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/bayer-waves.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nexport interface BayerWavesProps {\n  /** Ink color of the lit dots. */\n  color?: string;\n  /** Background color between the dots. */\n  background?: string;\n  /** Size of one dither pixel in px. */\n  pixelSize?: number;\n  /** Wave speed multiplier; 0 freezes the motion. */\n  speed?: number;\n  /** Zoom; higher means broader waves. */\n  scale?: number;\n  /** Cursor sends ripples through the waves. */\n  interactive?: boolean;\n  className?: string;\n}\n\nconst VERT = \"attribute vec2 p;void main(){gl_Position=vec4(p,0.,1.);}\";\n\nconst FRAG = `\nprecision highp float;\nuniform vec2 uRes;\nuniform float uTime;\nuniform vec2 uMouse;\nuniform float uActive;\nuniform vec3 uInk;\nuniform vec3 uBg;\nuniform float uPixel;\nuniform float uScale;\n\nfloat hash(vec2 p) {\n  p = fract(p * vec2(123.34, 456.21));\n  p += dot(p, p + 45.32);\n  return fract(p.x * p.y);\n}\nfloat noise(vec2 p) {\n  vec2 i = floor(p), f = fract(p);\n  vec2 u = f * f * (3.0 - 2.0 * f);\n  return mix(mix(hash(i), hash(i + vec2(1, 0)), u.x), mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), u.x), u.y);\n}\nfloat fbm(vec2 p) {\n  float v = 0.0, a = 0.5;\n  mat2 m = mat2(1.6, 1.2, -1.2, 1.6);\n  for (int i = 0; i < 4; i++) { v += a * noise(p); p = m * p; a *= 0.5; }\n  return v;\n}\n\n// Ordered (Bayer) threshold, built recursively from the 2x2 matrix.\nfloat bayer2(vec2 a) { a = floor(a); return fract(a.x * 0.5 + a.y * a.y * 0.75); }\nfloat bayer4(vec2 a) { return bayer2(0.5 * a) * 0.25 + bayer2(a); }\nfloat bayer8(vec2 a) { return bayer4(0.5 * a) * 0.25 + bayer2(a); }\n\nvoid main() {\n  vec2 cell = floor(gl_FragCoord.xy / uPixel);\n  vec2 c = (cell + 0.5) * uPixel;\n  vec2 p = (c - 0.5 * uRes) / uRes.y * 3.0 / uScale;\n  float t = uTime * 0.5;\n\n  float n = fbm(p * 0.55 + vec2(t * 0.12, -t * 0.08));\n  float v = 0.5 + 0.5 * sin(p.y * 2.6 + p.x * 0.6 + n * 3.2 - t * 1.3);\n  v = pow(v, 1.8) * (0.35 + 0.75 * n);\n\n  vec2 m = (uMouse - 0.5) * uRes / uRes.y * 3.0 / uScale;\n  float d = length(p - m);\n  v += uActive * exp(-d * d * 0.9) * (0.3 + 0.3 * sin(d * 9.0 - t * 9.0));\n\n  vec2 uv = c / uRes - 0.5;\n  v *= 1.05 - dot(uv, uv) * 1.3;\n\n  float on = step(bayer8(cell) + 0.01, clamp(v, 0.0, 1.0));\n  gl_FragColor = vec4(mix(uBg, uInk, on), 1.0);\n}\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) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n}\n\n/** Compiles a full-screen-triangle program and returns a cached uniform lookup, or null. */\nfunction fullscreen(gl: WebGLRenderingContext, frag: string) {\n  const prog = gl.createProgram();\n  if (!prog) return null;\n  for (const [type, src] of [\n    [gl.VERTEX_SHADER, VERT],\n    [gl.FRAGMENT_SHADER, frag],\n  ] as const) {\n    const sh = gl.createShader(type);\n    if (!sh) return null;\n    gl.shaderSource(sh, src);\n    gl.compileShader(sh);\n    if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) console.warn(gl.getShaderInfoLog(sh));\n    gl.attachShader(prog, sh);\n  }\n  gl.bindAttribLocation(prog, 0, \"p\");\n  gl.linkProgram(prog);\n  if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) return null;\n  gl.useProgram(prog);\n  gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());\n  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n  gl.enableVertexAttribArray(0);\n  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);\n  const cache = new Map<string, WebGLUniformLocation | null>();\n  return (name: string) => {\n    if (!cache.has(name)) cache.set(name, gl.getUniformLocation(prog, name));\n    return cache.get(name) ?? null;\n  };\n}\n\nexport function BayerWaves({\n  color = \"#c6ff3d\",\n  background = \"#0a0a0c\",\n  pixelSize = 3,\n  speed = 1,\n  scale = 1,\n  interactive = true,\n  className,\n}: BayerWavesProps) {\n  const host = useRef<HTMLDivElement>(null);\n  const opts = useRef({ color, background, pixelSize, speed, scale, interactive });\n  const redraw = useRef<(() => void) | null>(null);\n\n  useEffect(() => {\n    opts.current = { color, background, pixelSize, speed, scale, interactive };\n    redraw.current?.();\n  }, [color, background, pixelSize, speed, scale, interactive]);\n\n  useEffect(() => {\n    const el = host.current!;\n    const canvas = document.createElement(\"canvas\");\n    canvas.style.cssText = \"position:absolute;inset:0;width:100%;height:100%;display:block\";\n    el.appendChild(canvas);\n    const gl = canvas.getContext(\"webgl\", { antialias: false, powerPreference: \"low-power\" });\n    const u = gl && fullscreen(gl, FRAG);\n    if (!gl || !u) {\n      canvas.remove();\n      return;\n    }\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    const pointer = { x: 0.5, y: 0.5, active: 0, tx: 0.5, ty: 0.5, ta: 0 };\n    let raf = 0;\n    let visible = true;\n    let time = 5;\n    let last = 0;\n    let dpr = 1;\n\n    const draw = () => {\n      const o = opts.current;\n      gl.uniform2f(u(\"uRes\"), canvas.width, canvas.height);\n      gl.uniform1f(u(\"uTime\"), time);\n      gl.uniform2f(u(\"uMouse\"), pointer.x, pointer.y);\n      gl.uniform1f(u(\"uActive\"), o.interactive ? pointer.active : 0);\n      gl.uniform3fv(u(\"uInk\"), rgb(o.color));\n      gl.uniform3fv(u(\"uBg\"), rgb(o.background));\n      gl.uniform1f(u(\"uPixel\"), Math.max(Math.round(o.pixelSize * dpr), 1));\n      gl.uniform1f(u(\"uScale\"), Math.max(o.scale, 0.05));\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n    };\n    const loop = (now: number) => {\n      time += (Math.min(now - last, 50) / 1000) * opts.current.speed;\n      last = now;\n      pointer.x += (pointer.tx - pointer.x) * 0.1;\n      pointer.y += (pointer.ty - pointer.y) * 0.1;\n      pointer.active += (pointer.ta - pointer.active) * 0.05;\n      draw();\n      raf = requestAnimationFrame(loop);\n    };\n    const play = () => {\n      if (raf || !visible || reduced) return;\n      last = performance.now();\n      raf = requestAnimationFrame(loop);\n    };\n    const resize = () => {\n      dpr = Math.min(window.devicePixelRatio || 1, 2);\n      canvas.width = Math.max(1, Math.round(el.clientWidth * dpr));\n      canvas.height = Math.max(1, Math.round(el.clientHeight * dpr));\n      gl.viewport(0, 0, canvas.width, canvas.height);\n      draw();\n    };\n    const onMove = (e: PointerEvent) => {\n      const r = el.getBoundingClientRect();\n      pointer.tx = (e.clientX - r.left) / r.width;\n      pointer.ty = 1 - (e.clientY - r.top) / r.height;\n      if (!pointer.ta) {\n        pointer.x = pointer.tx;\n        pointer.y = pointer.ty;\n      }\n      pointer.ta = 1;\n    };\n    const onLeave = () => {\n      pointer.ta = 0;\n    };\n\n    const target = el.parentElement ?? el;\n    target.addEventListener(\"pointermove\", onMove, { passive: true });\n    target.addEventListener(\"pointerleave\", onLeave);\n    const ro = new ResizeObserver(resize);\n    ro.observe(el);\n    const io = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting;\n      if (visible) play();\n      else {\n        cancelAnimationFrame(raf);\n        raf = 0;\n      }\n    });\n    io.observe(el);\n    redraw.current = draw;\n\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      io.disconnect();\n      target.removeEventListener(\"pointermove\", onMove);\n      target.removeEventListener(\"pointerleave\", onLeave);\n      redraw.current = null;\n      gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n      canvas.remove();\n    };\n  }, []);\n\n  return (\n    <div\n      ref={host}\n      aria-hidden\n      className={className}\n      style={{ position: \"absolute\", inset: 0, overflow: \"hidden\", pointerEvents: \"none\" }}\n    />\n  );\n}\n"}]}