{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"fork-flash","type":"registry:ui","title":"Fork Flash","description":"A crackling procedural lightning bolt with forking branches, strobing strikes and storm clouds lit from within. WebGL.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/fork-flash.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nexport interface ForkFlashProps {\n  /** Bolt hue in degrees (0–360). */\n  hue?: number;\n  /** Animation speed multiplier. */\n  speed?: number;\n  /** Bolt and flash brightness. */\n  intensity?: number;\n  /** Bolt scale; larger zooms in. */\n  size?: number;\n  /** Horizontal position of the bolt, -1 (left) to 1 (right). */\n  xOffset?: number;\n  /** Seconds between strikes at speed 1. */\n  interval?: number;\n  className?: string;\n}\n\nconst FRAG = `precision highp float;\nuniform vec2 uRes;\nuniform float uTime;\nuniform float uHue;\nuniform float uIntensity;\nuniform float uSize;\nuniform float uX;\nuniform float uInterval;\n\nfloat hash(float n){ return fract(sin(n * 78.233) * 43758.5453); }\nfloat hash2(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }\nfloat noise(vec2 p){\n  vec2 i = floor(p), f = fract(p);\n  f = f * f * (3.0 - 2.0 * f);\n  return mix(mix(hash2(i), hash2(i + vec2(1, 0)), f.x), mix(hash2(i + vec2(0, 1)), hash2(i + vec2(1, 1)), f.x), f.y);\n}\nfloat fbm(vec2 p){\n  float v = 0.0, a = 0.5;\n  for (int i = 0; i < 6; i++) { v += a * noise(p); p = p * 2.03 + 17.1; a *= 0.5; }\n  return v;\n}\nvec3 hsv(float h, float s, float v){\n  vec3 k = clamp(abs(mod(h * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);\n  return v * mix(vec3(1.0), k, s);\n}\n\n// Jagged horizontal offset of a bolt at height y.\nfloat path(float y, float seed, float t){\n  return (fbm(vec2(y * 1.8, seed + t * 0.08)) - 0.5) * 0.9 + (fbm(vec2(y * 7.0, seed * 1.3 + t * 0.4)) - 0.5) * 0.12;\n}\n\nvoid main(){\n  vec2 p = (gl_FragCoord.xy - 0.5 * uRes) / uRes.y;\n  vec2 q = p / uSize;\n  q.x -= uX * 0.5 * uRes.x / uRes.y / uSize;\n  float t = uTime;\n\n  float cyc = floor(t / uInterval);\n  float lt = t - cyc * uInterval;          // time since the last strike\n  float seed = hash(cyc + 1.0) * 60.0;\n  float flash = exp(-lt * 3.5) * (0.6 + 0.4 * step(0.0, sin(lt * 42.0))) + exp(-max(lt - 0.18, 0.0) * 9.0) * step(0.18, lt) * 0.6;\n  float env = 0.5 + 1.5 * flash;\n\n  // Main bolt.\n  float x0 = path(q.y, seed, t);\n  float d = abs(q.x - x0);\n  float glow = 0.003 / (d + 0.0015);\n  float core = exp(-d * d / 0.000004);\n\n  // Branches forking off the main bolt.\n  for (int i = 0; i < 5; i++) {\n    float fi = float(i);\n    float by = 0.45 - hash(seed + fi * 7.1) * 0.8;\n    float len = 0.12 + hash(seed + fi * 3.3) * 0.35;\n    float k = by - q.y;\n    if (k > 0.0 && k < len) {\n      float side = hash(seed + fi * 1.7) > 0.5 ? 1.0 : -1.0;\n      float bx = path(by, seed, t) + side * k * (0.35 + hash(seed + fi * 5.9) * 0.9)\n               + (fbm(vec2(q.y * 5.0, seed + fi * 13.0 + t * 0.3)) - 0.5) * 0.5 * k + (noise(vec2(q.y * 40.0, seed + fi)) - 0.5) * 0.012;\n      float bd = abs(q.x - bx);\n      float fade = 1.0 - k / len;\n      glow += 0.0009 / (bd + 0.0012) * fade * fade;\n      core += exp(-bd * bd / 0.0000025) * fade;\n    }\n  }\n\n  vec3 tint = hsv(uHue / 360.0, 0.75, 1.0);\n  vec3 col = tint * glow * env + vec3(1.0) * min(core, 1.0) * env;\n  // Clouds lit from within on each strike.\n  float cloud = fbm(p * 2.2 + vec2(t * 0.03, 0.0)) * smoothstep(-0.2, 0.55, p.y);\n  float xs = (fbm(vec2(q.y * 1.8, seed + t * 0.08)) - 0.5) * 0.9; // smooth path for the glow\n  float near = exp(-abs(q.x - xs) * 2.5);\n  col += tint * (flash * 0.9 + 0.06) * cloud * cloud * (0.35 + 0.65 * near);\n  col += tint * (flash * 0.28 + 0.03) * near + tint * flash * 0.05;\n  col *= uIntensity;\n  col += (hash2(gl_FragCoord.xy) - 0.5) / 255.0;\n  col = clamp(col, 0.0, 1.0);\n  gl_FragColor = vec4(col, max(col.r, max(col.g, col.b)));\n}`;\n\ntype SetUniform = (name: string, v: number | number[], size?: number) => void;\n\n/** Full-screen-triangle WebGL runner: sizes to its host, pauses offscreen, one static frame under reduced motion. */\nfunction mountShader(host: HTMLElement, frag: string, frame: (set: SetUniform, dt: number) => void) {\n  const canvas = document.createElement(\"canvas\");\n  canvas.style.cssText = \"position:absolute;inset:0;width:100%;height:100%;display:block\";\n  const gl = canvas.getContext(\"webgl\", { antialias: false, premultipliedAlpha: true });\n  if (!gl) return null;\n  const prog = gl.createProgram()!;\n  const stages: [number, string][] = [\n    [gl.VERTEX_SHADER, \"attribute vec2 p;void main(){gl_Position=vec4(p,0.,1.);}\"],\n    [gl.FRAGMENT_SHADER, frag],\n  ];\n  for (const [type, src] of stages) {\n    const s = gl.createShader(type)!;\n    gl.shaderSource(s, src);\n    gl.compileShader(s);\n    if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) console.warn(gl.getShaderInfoLog(s));\n    gl.attachShader(prog, s);\n  }\n  gl.bindAttribLocation(prog, 0, \"p\");\n  gl.linkProgram(prog);\n  if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {\n    gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n    return null;\n  }\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  host.appendChild(canvas);\n\n  const locs = new Map<string, WebGLUniformLocation | null>();\n  const setters = [gl.uniform1fv, gl.uniform2fv, gl.uniform3fv, gl.uniform4fv];\n  const set: SetUniform = (name, v, size) => {\n    if (!locs.has(name)) locs.set(name, gl.getUniformLocation(prog, name));\n    const arr = typeof v === \"number\" ? [v] : v;\n    setters[(size ?? arr.length) - 1].call(gl, locs.get(name)!, arr);\n  };\n\n  const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n  let raf = 0;\n  let last = 0;\n  const render = (dt: number) => {\n    set(\"uRes\", [canvas.width, canvas.height]);\n    frame(set, dt);\n    gl.drawArrays(gl.TRIANGLES, 0, 3);\n  };\n  const loop = (now: number) => {\n    render(last ? Math.min((now - last) / 1000, 0.1) : 0);\n    last = now;\n    raf = requestAnimationFrame(loop);\n  };\n  const resize = () => {\n    const dpr = Math.min(window.devicePixelRatio || 1, 2);\n    canvas.width = Math.max(1, Math.round(host.clientWidth * dpr));\n    canvas.height = Math.max(1, Math.round(host.clientHeight * dpr));\n    gl.viewport(0, 0, canvas.width, canvas.height);\n    render(0);\n  };\n  const ro = new ResizeObserver(resize);\n  ro.observe(host);\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(host);\n\n  return {\n    redraw: () => {\n      if (!raf) render(0);\n    },\n    destroy: () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      io.disconnect();\n      gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n      canvas.remove();\n    },\n  };\n}\n\nexport function ForkFlash({\n  hue = 230,\n  speed = 1,\n  intensity = 1,\n  size = 1,\n  xOffset = 0,\n  interval = 2.4,\n  className,\n}: ForkFlashProps) {\n  const host = useRef<HTMLDivElement>(null);\n  const opts = useRef({ hue, speed, intensity, size, xOffset, interval });\n  const redraw = useRef<() => void>(undefined);\n\n  useEffect(() => {\n    opts.current = { hue, speed, intensity, size, xOffset, interval };\n    redraw.current?.();\n  }, [hue, speed, intensity, size, xOffset, interval]);\n\n  useEffect(() => {\n    // Start just after a strike so a static (reduced-motion) frame shows a lit bolt.\n    let t = 0.06;\n    const shader = mountShader(host.current!, FRAG, (set, dt) => {\n      const o = opts.current;\n      t += dt * o.speed;\n      set(\"uTime\", t);\n      set(\"uHue\", o.hue);\n      set(\"uIntensity\", o.intensity);\n      set(\"uSize\", Math.max(o.size, 0.1));\n      set(\"uX\", o.xOffset);\n      set(\"uInterval\", Math.max(o.interval, 0.3));\n    });\n    if (!shader) return;\n    redraw.current = shader.redraw;\n    return () => {\n      redraw.current = undefined;\n      shader.destroy();\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"}]}