{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "shader-caustics",
  "title": "Shader Caustics",
  "description": "A field of refracted light — the bright filaments light makes through a rippling surface.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/shader-caustics/index.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport {\n  continueRender,\n  delayRender,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\n// A field of refracted light — caustics. The web of bright filaments light\n// makes when it passes through a surface and lands on the floor beneath.\n// Written for the remocn registry as a custom WebGL shader (not a\n// paper-design wrapper): frame-driven, deterministic, no wall-clock time.\n//\n// The filaments are slightly denser and brighter toward the bottom of the\n// frame — the light lands on the base — which makes it a natural carrier for\n// foundation stories. `accent` / `accentAmount` tint the filaments themselves\n// (not the floor), so an undertone can arrive mid-video by animating\n// `accentAmount` from the master timeline and stay in the light afterwards.\n\nexport interface ShaderCausticsProps {\n  /** [floor color, filament light color]. */\n  colors?: [string, string];\n  /** Tint mixed into the bright filaments. */\n  accent?: string;\n  /** 0..1 — how much of the accent the filaments take. */\n  accentAmount?: number;\n  /** Spatial frequency of the light web. */\n  scale?: number;\n  /** Overall brightness of the filaments. */\n  intensity?: number;\n  /** Global time multiplier. */\n  speed?: number;\n  className?: string;\n}\n\nconst VERT = `\nattribute vec2 a_pos;\nvoid main() {\n  gl_Position = vec4(a_pos, 0.0, 1.0);\n}\n`;\n\nconst FRAG = `\nprecision highp float;\n\nuniform vec2 u_res;\nuniform float u_time;\nuniform float u_scale;\nuniform float u_intensity;\nuniform vec3 u_floor;\nuniform vec3 u_light;\nuniform vec3 u_accent;\nuniform float u_accentAmt;\n\nfloat hash(vec2 p) {\n  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nvoid main() {\n  vec2 uv = gl_FragCoord.xy / u_res;\n  float aspect = u_res.x / u_res.y;\n  vec2 p = vec2(uv.x * aspect, uv.y) * u_scale;\n\n  // Iterative trigonometric warp: each pass bends the sample point by the\n  // previous pass, and the reciprocal distance to the warped axes\n  // accumulates into the caustic web.\n  float t = u_time * 0.35;\n  vec2 i = p;\n  float c = 0.0;\n  for (int n = 0; n < 4; n++) {\n    float phase = t * (1.0 - 3.2 / float(n + 2));\n    i = p + vec2(\n      cos(phase - i.x) + sin(phase + i.y),\n      sin(phase - i.y) + cos(phase + i.x)\n    );\n    c += 1.0 / length(vec2(\n      p.x / (sin(i.x + phase) * 90.0),\n      p.y / (cos(i.y + phase) * 90.0)\n    ));\n  }\n  c /= 4.0;\n  c = 1.24 - pow(abs(c), 1.35);\n  // |c| collapses to zero along thin branching veins — that zero-crossing\n  // set IS the caustic web. Light the veins with an exponential falloff\n  // (sharp core + a soft halo) and leave everything else near-black.\n  float web = exp(-abs(c) * 0.5) + 0.22 * exp(-abs(c) * 0.12);\n  web = web / (1.0 + 0.6 * web);\n\n  // The light lands on the base: filaments read stronger low in the frame.\n  float ground = mix(1.25, 0.55, uv.y);\n  web *= ground * u_intensity;\n\n  // Vignette keeps the corners quiet so type always wins.\n  vec2 v = uv - 0.5;\n  float vig = 1.0 - dot(v, v) * 1.1;\n  web *= clamp(vig, 0.0, 1.0);\n\n  vec3 filament = mix(u_light, u_accent, clamp(u_accentAmt, 0.0, 1.0));\n  vec3 col = u_floor + filament * web;\n\n  // A whisper of living grain, time-derived so it stays deterministic.\n  col += (hash(gl_FragCoord.xy + floor(u_time * 9.0)) - 0.5) * 0.012;\n\n  gl_FragColor = vec4(col, 1.0);\n}\n`;\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n  const h = hex.replace(\"#\", \"\");\n  const v =\n    h.length === 3\n      ? h\n          .split(\"\")\n          .map((c) => c + c)\n          .join(\"\")\n      : h;\n  const n = parseInt(v, 16);\n  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n};\n\ntype GlState = {\n  gl: WebGLRenderingContext;\n  uniforms: Record<string, WebGLUniformLocation | null>;\n};\n\nexport function ShaderCaustics({\n  colors = [\"#0a0a0a\", \"#2e2e33\"],\n  accent = \"#7F57FF\",\n  accentAmount = 0,\n  scale = 5.2,\n  intensity = 1,\n  speed = 1,\n  className,\n}: ShaderCausticsProps) {\n  const frame = useCurrentFrame();\n  const { fps, width, height } = useVideoConfig();\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const stateRef = useRef<GlState | null>(null);\n  const [handle] = useState(() => delayRender(\"shader-caustics\"));\n  const continuedRef = useRef(false);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas || stateRef.current) return;\n    const gl = canvas.getContext(\"webgl\", {\n      antialias: false,\n      preserveDrawingBuffer: true,\n    });\n    if (!gl) return;\n\n    const compile = (type: number, src: string) => {\n      const shader = gl.createShader(type);\n      if (!shader) throw new Error(\"Failed to create WebGL shader\");\n      gl.shaderSource(shader, src);\n      gl.compileShader(shader);\n      return shader;\n    };\n    const program = gl.createProgram();\n    if (!program) return;\n    gl.attachShader(program, compile(gl.VERTEX_SHADER, VERT));\n    gl.attachShader(program, compile(gl.FRAGMENT_SHADER, FRAG));\n    gl.linkProgram(program);\n    // biome-ignore lint/correctness/useHookAtTopLevel: gl.useProgram is a WebGL method, not a React hook\n    gl.useProgram(program);\n\n    const buf = gl.createBuffer();\n    gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n    gl.bufferData(\n      gl.ARRAY_BUFFER,\n      new Float32Array([-1, -1, 3, -1, -1, 3]),\n      gl.STATIC_DRAW,\n    );\n    const loc = gl.getAttribLocation(program, \"a_pos\");\n    gl.enableVertexAttribArray(loc);\n    gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);\n\n    const uniforms: GlState[\"uniforms\"] = {};\n    for (const name of [\n      \"u_res\",\n      \"u_time\",\n      \"u_scale\",\n      \"u_intensity\",\n      \"u_floor\",\n      \"u_light\",\n      \"u_accent\",\n      \"u_accentAmt\",\n    ]) {\n      uniforms[name] = gl.getUniformLocation(program, name);\n    }\n    stateRef.current = { gl, uniforms };\n  }, []);\n\n  useEffect(() => {\n    const state = stateRef.current;\n    const canvas = canvasRef.current;\n    if (!state || !canvas) return;\n    const { gl, uniforms } = state;\n\n    gl.viewport(0, 0, canvas.width, canvas.height);\n    gl.uniform2f(uniforms.u_res, canvas.width, canvas.height);\n    gl.uniform1f(uniforms.u_time, (frame / fps) * speed);\n    gl.uniform1f(uniforms.u_scale, scale);\n    gl.uniform1f(uniforms.u_intensity, intensity);\n    gl.uniform3fv(uniforms.u_floor, hexToRgb(colors[0]));\n    gl.uniform3fv(uniforms.u_light, hexToRgb(colors[1]));\n    gl.uniform3fv(uniforms.u_accent, hexToRgb(accent));\n    gl.uniform1f(uniforms.u_accentAmt, accentAmount);\n    gl.drawArrays(gl.TRIANGLES, 0, 3);\n\n    if (!continuedRef.current) {\n      continuedRef.current = true;\n      requestAnimationFrame(() =>\n        requestAnimationFrame(() => continueRender(handle)),\n      );\n    }\n  }, [\n    frame,\n    fps,\n    speed,\n    scale,\n    intensity,\n    colors,\n    accent,\n    accentAmount,\n    handle,\n  ]);\n\n  return (\n    <div className={className} style={{ position: \"absolute\", inset: 0 }}>\n      <canvas\n        ref={canvasRef}\n        width={width}\n        height={height}\n        style={{ width: \"100%\", height: \"100%\", display: \"block\" }}\n      />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/shader-caustics.tsx"
    }
  ],
  "type": "registry:component"
}
