{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dot-matrix-wave",
  "title": "DotMatrixWave",
  "description": "Field of dots breathing in a drifting noise wave, colored by currentColor. The RegionSelect dot-map aesthetic as a background surface.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/dot-matrix-wave/dot-matrix-wave.tsx",
      "content": "\"use client\";\n\n/*\n * DotMatrixWave: a field of dots breathing in a smooth noise wave,\n * the RegionSelect dot-map aesthetic, generalized into a background\n * surface. Color comes from currentColor at mount (same contract as\n * NeonLoader and AnimatedWash), so a text-primary ancestor or the\n * theme drives it with zero props; pass a colors array for a\n * left-to-right gradient fade instead, like the neon.com green-to-\n * blue type treatment. Static single frame under reduced motion.\n * The GLSL lives in dot-matrix-wave-shader.ts.\n */\n\nimport { useEffect, useMemo, useRef } from \"react\";\nimport type { ComponentProps } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nimport { DOT_FRAGMENT, DOT_VERTEX } from \"./dot-matrix-wave-shader\";\n\nexport type DotMatrixWaveProps = Omit<ComponentProps<\"canvas\">, \"children\"> & {\n  /** Wave drift speed multiplier; 0 freezes the field. */\n  speed?: number;\n  /** Dot pitch in CSS pixels. */\n  gap?: number;\n  /** 0-1 dot radius as a share of the pitch. */\n  dotSize?: number;\n  /** 0-1 how hard the wave swells the dots. */\n  amplitude?: number;\n  /** 0-1 minimum brightness of resting dots. */\n  floor?: number;\n  /**\n   * Gradient stops spread evenly left to right, up to 6. Omit to\n   * paint the whole field with currentColor.\n   */\n  colors?: string[];\n};\n\nconst MAX_STOPS = 6;\n\n/**\n * Seconds for a dial change to close ~63% of its gap — uniforms ease\n * toward their targets each frame, so a dragged slider glides\n * instead of snapping.\n */\nconst SMOOTH_TAU = 0.12;\n\nconst parseColor = (css: string): [number, number, number] => {\n  const probe = document.createElement(\"canvas\");\n  probe.width = 1;\n  probe.height = 1;\n  const context = probe.getContext(\"2d\");\n\n  if (!context) {\n    return [0, 0.9, 0.6];\n  }\n\n  context.fillStyle = css;\n  context.fillRect(0, 0, 1, 1);\n  const [r, g, b] = context.getImageData(0, 0, 1, 1).data;\n  return [(r ?? 0) / 255, (g ?? 0) / 255, (b ?? 0) / 255];\n};\n\nconst warnDev = (message: string) => {\n  if (typeof process !== \"undefined\" && process.env.NODE_ENV !== \"production\") {\n    console.warn(message);\n  }\n};\n\nconst compile = (\n  gl: WebGLRenderingContext,\n  type: number,\n  source: string\n): WebGLShader | null => {\n  const shader = gl.createShader(type);\n\n  if (!shader) {\n    return null;\n  }\n\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    warnDev(\n      `neon-ui: shader compile failed: ${gl.getShaderInfoLog(shader) ?? \"unknown\"}`\n    );\n    gl.deleteShader(shader);\n    return null;\n  }\n\n  return shader;\n};\n\nexport const DotMatrixWave = ({\n  amplitude = 0.8,\n  className,\n  colors,\n  dotSize = 0.35,\n  floor = 0.08,\n  gap = 14,\n  speed = 1,\n  ...props\n}: DotMatrixWaveProps) => {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  // Shader time, accumulated frame by frame so a speed change scales\n  // the flow from here instead of teleporting the whole field.\n  const phaseRef = useRef(0);\n  const lastFrameRef = useRef<number | null>(null);\n  // Smoothed dial values, persisting across prop-driven re-inits.\n  const smoothedRef = useRef<Record<string, number> | null>(null);\n  const stopsKey = useMemo(() => colors?.join(\"|\") ?? \"\", [colors]);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    const gl = canvas?.getContext(\"webgl\", {\n      alpha: true,\n      premultipliedAlpha: true,\n    });\n\n    if (!(canvas && gl)) {\n      return;\n    }\n\n    const vertex = compile(gl, gl.VERTEX_SHADER, DOT_VERTEX);\n    const fragment = compile(gl, gl.FRAGMENT_SHADER, DOT_FRAGMENT);\n    const program = gl.createProgram();\n\n    if (!(vertex && fragment && program)) {\n      if (vertex) {\n        gl.deleteShader(vertex);\n      }\n      if (fragment) {\n        gl.deleteShader(fragment);\n      }\n      return;\n    }\n\n    gl.attachShader(program, vertex);\n    gl.attachShader(program, fragment);\n    gl.linkProgram(program);\n\n    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n      warnDev(\n        `neon-ui: program link failed: ${gl.getProgramInfoLog(program) ?? \"unknown\"}`\n      );\n      gl.deleteProgram(program);\n      gl.deleteShader(vertex);\n      gl.deleteShader(fragment);\n      return;\n    }\n\n    // oxlint-disable-next-line react/react-compiler -- WebGL method, not a React hook\n    gl.useProgram(program);\n\n    const quad = gl.createBuffer();\n    gl.bindBuffer(gl.ARRAY_BUFFER, quad);\n    gl.bufferData(\n      gl.ARRAY_BUFFER,\n      new Float32Array([-1, -1, 3, -1, -1, 3]),\n      gl.STATIC_DRAW\n    );\n    const position = gl.getAttribLocation(program, \"a_position\");\n    gl.enableVertexAttribArray(position);\n    gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n\n    const uResolution = gl.getUniformLocation(program, \"u_resolution\");\n    const uTime = gl.getUniformLocation(program, \"u_time\");\n    const uGap = gl.getUniformLocation(program, \"u_gap\");\n    const uDot = gl.getUniformLocation(program, \"u_dot\");\n    const uAmplitude = gl.getUniformLocation(program, \"u_amplitude\");\n    const uFloor = gl.getUniformLocation(program, \"u_floor\");\n\n    const dpr = Math.min(window.devicePixelRatio || 1, 2);\n\n    // The gradient: explicit stops, or currentColor as a single stop.\n    const stops =\n      colors && colors.length > 0\n        ? colors.slice(0, MAX_STOPS).map(parseColor)\n        : [parseColor(getComputedStyle(canvas).color)];\n    const stopData = new Float32Array(MAX_STOPS * 3);\n\n    for (const [index, [r, g, b]] of stops.entries()) {\n      stopData[index * 3] = r;\n      stopData[index * 3 + 1] = g;\n      stopData[index * 3 + 2] = b;\n    }\n\n    gl.uniform1i(gl.getUniformLocation(program, \"u_stop_count\"), stops.length);\n    gl.uniform3fv(gl.getUniformLocation(program, \"u_stops\"), stopData);\n    const targets: Record<string, number> = {\n      amplitude,\n      dotSize,\n      floor,\n      gap,\n      speed,\n    };\n\n    smoothedRef.current ??= { ...targets };\n    const smoothed = smoothedRef.current;\n\n    /** Ease every dial toward its target and upload; k=1 snaps. */\n    const applyUniforms = (k: number) => {\n      for (const key of Object.keys(targets)) {\n        const current = smoothed[key] ?? targets[key] ?? 0;\n        smoothed[key] = current + ((targets[key] ?? 0) - current) * k;\n      }\n\n      gl.uniform1f(uGap, (smoothed.gap ?? gap) * dpr);\n      gl.uniform1f(uDot, smoothed.dotSize ?? dotSize);\n      gl.uniform1f(uAmplitude, smoothed.amplitude ?? amplitude);\n      gl.uniform1f(uFloor, smoothed.floor ?? floor);\n    };\n\n    let frame = 0;\n    let staticFrame = false;\n\n    const renderStatic = () => {\n      applyUniforms(1);\n      gl.uniform1f(uTime, 3);\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n    };\n\n    const resize = () => {\n      const width = Math.max(1, Math.round(canvas.clientWidth * dpr));\n      const height = Math.max(1, Math.round(canvas.clientHeight * dpr));\n\n      if (canvas.width !== width || canvas.height !== height) {\n        canvas.width = width;\n        canvas.height = height;\n        gl.viewport(0, 0, width, height);\n      }\n\n      gl.uniform2f(uResolution, canvas.width, canvas.height);\n    };\n\n    const observer = new ResizeObserver(() => {\n      resize();\n\n      if (staticFrame) {\n        renderStatic();\n      }\n    });\n    observer.observe(canvas);\n    resize();\n\n    const dispose = () => {\n      observer.disconnect();\n      gl.deleteBuffer(quad);\n      gl.deleteProgram(program);\n      gl.deleteShader(vertex);\n      gl.deleteShader(fragment);\n    };\n\n    const draw = (now: number) => {\n      const last = lastFrameRef.current ?? now;\n      lastFrameRef.current = now;\n      // Skip GL work while hidden: keep the loop alive, drop the cost.\n      if (!(canvas.checkVisibility?.() ?? true)) {\n        frame = requestAnimationFrame(draw);\n        return;\n      }\n\n      const dt = (now - last) / 1000;\n      applyUniforms(1 - Math.exp(-dt / SMOOTH_TAU));\n      phaseRef.current += dt * (smoothed.speed ?? speed);\n      gl.uniform1f(uTime, phaseRef.current);\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n      frame = requestAnimationFrame(draw);\n    };\n\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n\n    if (reduced.matches || speed === 0) {\n      staticFrame = true;\n      resize();\n      renderStatic();\n      return dispose;\n    }\n\n    frame = requestAnimationFrame(draw);\n\n    return () => {\n      cancelAnimationFrame(frame);\n      dispose();\n    };\n    // stopsKey covers the colors array contents.\n    // oxlint-disable-next-line react-hooks/exhaustive-deps\n  }, [amplitude, dotSize, floor, gap, speed, stopsKey]);\n\n  return (\n    <canvas\n      aria-hidden=\"true\"\n      className={cn(\"size-full\", className)}\n      data-slot=\"dot-matrix-wave\"\n      ref={canvasRef}\n      {...props}\n    />\n  );\n};\n",
      "type": "registry:component"
    },
    {
      "path": "src/components/dot-matrix-wave/dot-matrix-wave-shader.ts",
      "content": "/*\n * The visual shader for DotMatrixWave, kept separate from the React\n * mount.\n *\n * A field of dots breathing in a smooth noise wave: the dot-matrix\n * world map from RegionSelect, generalized into a background surface.\n * Each dot's size and brightness ride a drifting value-noise field, so\n * broad swells roll across the grid instead of dots blinking in\n * unison.\n *\n * Fragment shader uniforms:\n * - u_resolution (vec2): canvas resolution in pixels\n * - u_time (float): animation time in seconds (pre-multiplied by speed)\n * - u_gap (float): dot pitch in device pixels\n * - u_dot (float): 0-1 dot radius as a share of the pitch\n * - u_amplitude (float): 0-1 how hard the wave swells the dots\n * - u_floor (float): 0-1 minimum brightness of resting dots\n * - u_stop_count (int): number of live gradient stops (up to 6)\n * - u_stops[6] (vec3): gradient stops, spread evenly left to right;\n *   a single stop paints the whole field flat (currentColor default)\n */\n\nexport const DOT_VERTEX = `\nattribute vec2 a_position;\nvoid main() {\n  gl_Position = vec4(a_position, 0.0, 1.0);\n}\n`;\n\nexport const DOT_FRAGMENT = `\nprecision mediump float;\nuniform vec2 u_resolution;\nuniform float u_time;\nuniform float u_gap;\nuniform float u_dot;\nuniform float u_amplitude;\nuniform float u_floor;\nuniform int u_stop_count;\nuniform vec3 u_stops[6];\n\nfloat hash(vec2 p) {\n  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);\n}\n\nfloat vnoise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = smoothstep(0.0, 1.0, fract(p));\n  float a = hash(i);\n  float b = hash(i + vec2(1.0, 0.0));\n  float c = hash(i + vec2(0.0, 1.0));\n  float d = hash(i + vec2(1.0, 1.0));\n  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);\n}\n\n// Piecewise-linear gradient across evenly spread stops.\nvec3 gradientAt(float t) {\n  vec3 color = u_stops[0];\n  float span = max(float(u_stop_count - 1), 1.0);\n\n  for (int i = 1; i < 6; i++) {\n    if (i >= u_stop_count) {\n      break;\n    }\n\n    float a = float(i - 1) / span;\n    float b = float(i) / span;\n    color = mix(color, u_stops[i], smoothstep(a, b, t));\n  }\n\n  return color;\n}\n\nvoid main() {\n  vec2 cell = floor(gl_FragCoord.xy / u_gap);\n  vec2 local = fract(gl_FragCoord.xy / u_gap) - 0.5;\n\n  // Two octaves of drifting noise: a broad swell plus finer chop, each\n  // sliding in its own direction so the field never loops visibly.\n  float swell = vnoise(cell * 0.08 + vec2(u_time * 0.25, u_time * 0.1));\n  float chop = vnoise(cell * 0.3 - vec2(u_time * 0.15, u_time * 0.3));\n  float wave = swell * 0.7 + chop * 0.3;\n\n  // The wave swells both radius and brightness; resting dots keep a\n  // faint floor so the grid never fully disappears.\n  float energy = u_floor + (1.0 - u_floor) * pow(wave, 1.6) * u_amplitude +\n    (1.0 - u_amplitude) * (1.0 - u_floor) * 0.25;\n  float radius = u_dot * 0.5 * (0.55 + 0.45 * wave * u_amplitude +\n    0.45 * (1.0 - u_amplitude));\n\n  float d = length(local);\n  float aa = 1.0 / u_gap;\n  float disc = 1.0 - smoothstep(radius - aa, radius + aa, d);\n\n  // Each dot takes one flat tone from the gradient at its cell center.\n  vec3 color = gradientAt(((cell.x + 0.5) * u_gap) / u_resolution.x);\n\n  float alpha = disc * energy;\n  gl_FragColor = vec4(color * alpha, alpha);\n}\n`;\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}