{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "light-rays",
  "title": "Light Rays",
  "description": "Soft beams of light falling from above, from a small WebGL2 shader.",
  "registryDependencies": [
    "@corsair-ui/utils",
    "@corsair-ui/use-in-view"
  ],
  "files": [
    {
      "path": "registry/default/ui/light-rays.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useMemo, useRef, type ComponentProps, type Ref } from \"react\";\n\nimport { useInView } from \"@/registry/default/hooks/use-in-view\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nfunction mergeRefs<T>(...refs: (Ref<T> | undefined)[]) {\n  return (node: T | null) => {\n    for (const ref of refs) {\n      if (typeof ref === \"function\") ref(node);\n      else if (ref) ref.current = node;\n    }\n  };\n}\n\nconst VERTEX = `#version 300 es\nin vec2 a_position;\nvoid main() { gl_Position = vec4(a_position, 0.0, 1.0); }`;\n\n// Beams fanning down from a point above the top edge. Their pattern is\n// smooth 1D noise over the angle, in two layers drifting against each other,\n// with a haze around the source and a fade with distance.\nconst FRAGMENT = `#version 300 es\nprecision highp float;\nuniform vec2 u_resolution;\nuniform float u_time;\nuniform vec2 u_origin;\nuniform vec3 u_color1;\nuniform vec3 u_color2;\nuniform float u_intensity;\nuniform float u_density;\nuniform float u_reach;\nuniform float u_spread;\nout vec4 fragColor;\n\nfloat hash(float n) { return fract(sin(n * 127.1) * 43758.5453123); }\nfloat noise(float x) {\n  float i = floor(x);\n  float f = fract(x);\n  return mix(hash(i), hash(i + 1.0), f * f * (3.0 - 2.0 * f));\n}\n\nvoid main() {\n  vec2 p = vec2(gl_FragCoord.x, u_resolution.y - gl_FragCoord.y);\n  vec2 d = p - u_origin;\n  float dist = length(d);\n  float across = atan(d.x, d.y) / u_spread;\n  float beams = 0.65 * noise(across * u_density + u_time * 0.35)\n    + 0.35 * noise(across * u_density * 2.3 - u_time * 0.5 + 17.0);\n  beams = smoothstep(0.2, 1.0, beams);\n  float diagonal = length(u_resolution);\n  float fade = 1.0 - smoothstep(0.0, max(u_reach, 0.05) * diagonal, dist);\n  fade *= fade;\n  float fan = 1.0 - smoothstep(0.7, 1.0, abs(across));\n  float haze = 0.3 * exp(-dist / (0.2 * diagonal));\n  float strength = clamp((beams * fan + haze) * fade * u_intensity, 0.0, 1.0);\n  vec3 color = mix(u_color1, u_color2, clamp(across * 0.5 + 0.5, 0.0, 1.0));\n  fragColor = vec4(color * strength, strength);\n}`;\n\n/** Any CSS colour, theme variables included, as 0–1 RGB. */\nfunction resolveColor(color: string, probe: HTMLElement): [number, number, number] {\n  probe.style.color = color;\n  const canvas = document.createElement(\"canvas\");\n  canvas.width = canvas.height = 1;\n  const context = canvas.getContext(\"2d\");\n  if (!context) return [1, 1, 1];\n  context.fillStyle = getComputedStyle(probe).color;\n  context.fillRect(0, 0, 1, 1);\n  const [r = 255, g = 255, b = 255] = context.getImageData(0, 0, 1, 1).data;\n  return [r / 255, g / 255, b / 255];\n}\n\nfunction compile(gl: WebGL2RenderingContext) {\n  const program = gl.createProgram();\n  for (const [type, source] of [\n    [gl.VERTEX_SHADER, VERTEX],\n    [gl.FRAGMENT_SHADER, FRAGMENT],\n  ] as const) {\n    const shader = gl.createShader(type);\n    if (!shader) return null;\n    gl.shaderSource(shader, source);\n    gl.compileShader(shader);\n    gl.attachShader(program, shader);\n    gl.deleteShader(shader);\n  }\n  gl.linkProgram(program);\n  return gl.getProgramParameter(program, gl.LINK_STATUS) ? program : null;\n}\n\ninterface LightRaysSettings {\n  intensity: number;\n  density: number;\n  reach: number;\n  origin: number;\n  spread: number;\n  speed: number;\n}\n\ninterface LightRaysProps\n  extends Omit<ComponentProps<\"div\">, \"children\">, Partial<LightRaysSettings> {\n  /** One colour for every beam, or two to shade the fan from left to right. Theme variables work. */\n  colors?: string | [string, string];\n  /**\n   * Rendering scale against CSS pixels. Soft light loses nothing at half\n   * resolution and costs a quarter of the work.\n   */\n  resolution?: number;\n}\n\n/**\n * Soft beams of light falling from above, drawn by a small WebGL2 shader on\n * a transparent canvas, so it sits on whatever background its container\n * has. Put it inside a positioned element; it fills it. It pauses off\n * screen and in background tabs, draws a single still frame with\n * `prefers-reduced-motion`, and falls back to a soft CSS glow where WebGL2\n * is unavailable.\n *\n * - `intensity`: brightness of the beams, from 0 to 1.\n * - `density`: how many beams fit in the fan; higher is finer.\n * - `reach`: how far down the light reaches, as a share of the diagonal.\n * - `origin`: where the source sits across the top, from 0 (left) to 1 (right).\n * - `spread`: how wide the fan opens, in degrees either side of straight down.\n * - `speed`: pace of the drift; 0 holds it still.\n *\n * @example\n * <section className=\"relative isolate overflow-hidden bg-neutral-950\">\n *   <LightRays colors=\"#6fb3c2\" />\n *   <h2 className=\"relative\">Chart your own course</h2>\n * </section>\n */\nfunction LightRays({\n  colors = \"#ffffff\",\n  intensity = 0.6,\n  density = 12,\n  reach = 0.8,\n  origin = 0.5,\n  spread = 60,\n  speed = 1,\n  resolution = 0.5,\n  className,\n  style,\n  ref,\n  ...props\n}: LightRaysProps) {\n  const canvas = useRef<HTMLCanvasElement>(null);\n  const fallback = useRef<HTMLDivElement>(null);\n  const [observe, inView] = useInView<HTMLDivElement>({ rootMargin: \"100px\" });\n  const mergedRef = useMemo(() => mergeRefs(ref, observe), [ref, observe]);\n  const [first = \"#ffffff\", second = first] = Array.isArray(colors) ? colors : [colors];\n\n  // The render loop reads the latest props from here, without restarting WebGL.\n  const settings = useRef<LightRaysSettings>({ intensity, density, reach, origin, spread, speed });\n  const visible = useRef(false);\n  const wake = useRef<() => void>(() => {});\n\n  useEffect(() => {\n    const element = canvas.current;\n    const gl = element?.getContext(\"webgl2\", {\n      alpha: true,\n      premultipliedAlpha: true,\n      antialias: false,\n    });\n    if (!element || !gl) return;\n    const program = compile(gl);\n    if (!program) return;\n    const glow = fallback.current;\n    if (glow) glow.hidden = true;\n\n    const buffer = gl.createBuffer();\n    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n    // One triangle that covers the whole canvas.\n    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n    const position = gl.getAttribLocation(program, \"a_position\");\n    gl.enableVertexAttribArray(position);\n    gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n    gl.useProgram(program);\n    const at = (name: string) => gl.getUniformLocation(program, name);\n    const u = {\n      resolution: at(\"u_resolution\"),\n      time: at(\"u_time\"),\n      origin: at(\"u_origin\"),\n      color1: at(\"u_color1\"),\n      color2: at(\"u_color2\"),\n      intensity: at(\"u_intensity\"),\n      density: at(\"u_density\"),\n      reach: at(\"u_reach\"),\n      spread: at(\"u_spread\"),\n    };\n\n    const probe = document.createElement(\"span\");\n    probe.style.display = \"none\";\n    element.after(probe);\n    const paint = () => {\n      gl.uniform3fv(u.color1, resolveColor(first, probe));\n      gl.uniform3fv(u.color2, resolveColor(second, probe));\n    };\n\n    let time = 0;\n    const draw = () => {\n      const s = settings.current;\n      gl.uniform2f(u.resolution, element.width, element.height);\n      gl.uniform1f(u.time, time);\n      // The source sits a little above the top edge, so the beams enter already spread.\n      gl.uniform2f(u.origin, s.origin * element.width, -0.25 * element.height);\n      gl.uniform1f(u.intensity, Math.min(Math.max(s.intensity, 0), 1));\n      gl.uniform1f(u.density, Math.max(s.density, 1));\n      gl.uniform1f(u.reach, s.reach);\n      gl.uniform1f(u.spread, (Math.min(Math.max(s.spread, 5), 89) * Math.PI) / 180);\n      gl.clearColor(0, 0, 0, 0);\n      gl.clear(gl.COLOR_BUFFER_BIT);\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n    };\n\n    const resize = () => {\n      const scale =\n        Math.min(window.devicePixelRatio || 1, 2) * Math.min(Math.max(resolution, 0.1), 2);\n      element.width = Math.max(1, Math.round(element.clientWidth * scale));\n      element.height = Math.max(1, Math.round(element.clientHeight * scale));\n      gl.viewport(0, 0, element.width, element.height);\n      draw();\n    };\n\n    const still = window.matchMedia?.(\"(prefers-reduced-motion: reduce)\");\n    let frame = 0;\n    let last = 0;\n    const loop = (now: number) => {\n      frame = 0;\n      const moving = visible.current && !still?.matches && settings.current.speed !== 0;\n      if (moving && last) time += ((now - last) / 1000) * settings.current.speed;\n      last = moving ? now : 0;\n      draw();\n      if (moving) frame = requestAnimationFrame(loop);\n    };\n    wake.current = () => {\n      if (!frame) frame = requestAnimationFrame(loop);\n    };\n\n    paint();\n    resize();\n    const resizer = new ResizeObserver(resize);\n    resizer.observe(element);\n    // Theme switches change what colour variables resolve to.\n    const theme = new MutationObserver(() => {\n      paint();\n      draw();\n    });\n    theme.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\", \"style\", \"data-theme\"],\n    });\n    wake.current();\n\n    return () => {\n      cancelAnimationFrame(frame);\n      wake.current = () => {};\n      resizer.disconnect();\n      theme.disconnect();\n      probe.remove();\n      gl.deleteBuffer(buffer);\n      gl.deleteProgram(program);\n      if (glow) glow.hidden = false;\n    };\n  }, [first, second, resolution]);\n\n  // Hand the latest props to the loop, and restart it when it is back on screen.\n  useEffect(() => {\n    settings.current = { intensity, density, reach, origin, spread, speed };\n    visible.current = inView;\n    wake.current();\n  }, [intensity, density, reach, origin, spread, speed, inView]);\n\n  return (\n    <div\n      ref={mergedRef}\n      aria-hidden=\"true\"\n      data-slot=\"light-rays\"\n      className={cn(\"pointer-events-none absolute inset-0 -z-10 overflow-hidden\", className)}\n      style={style}\n      {...props}\n    >\n      {/* Stands in for the beams where WebGL2 is unavailable. */}\n      <div\n        ref={fallback}\n        data-slot=\"light-rays-fallback\"\n        className=\"absolute inset-x-0 top-0 h-3/4 opacity-40\"\n        style={{\n          background: `radial-gradient(ellipse 60% 80% at ${origin * 100}% 0%, ${first}, transparent 70%)`,\n        }}\n      />\n      <canvas ref={canvas} className=\"block size-full\" />\n    </div>\n  );\n}\n\nexport { LightRays, type LightRaysProps };\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "background"
  ],
  "type": "registry:ui"
}