{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"god-rays","type":"registry:ui","title":"God Rays","description":"Soft god rays streaming from an edge or corner, swaying gently and leaning toward the cursor. WebGL.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/god-rays.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nexport type GodRaysOrigin =\n  | \"top-center\"\n  | \"top-left\"\n  | \"top-right\"\n  | \"left\"\n  | \"right\"\n  | \"bottom-center\"\n  | \"center\";\n\nexport interface GodRaysProps {\n  /** Where the rays come from. */\n  origin?: GodRaysOrigin;\n  color?: string;\n  /** Animation speed multiplier. */\n  speed?: number;\n  /** Cone half-width in radians (ignored for \"center\"). */\n  spread?: number;\n  /** How far the rays reach, in container heights. */\n  length?: number;\n  intensity?: number;\n  /** 0–1, how much the beam leans toward the cursor. */\n  followMouse?: number;\n  className?: string;\n}\n\n// Origin (uv, y up) and main direction for each preset. Sources sit just offscreen.\nconst ORIGINS: Record<GodRaysOrigin, [number, number, number, number]> = {\n  \"top-center\": [0.5, 1.12, 0, -1],\n  \"top-left\": [-0.04, 1.08, 0.75, -0.66],\n  \"top-right\": [1.04, 1.08, -0.75, -0.66],\n  left: [-0.08, 0.5, 1, 0],\n  right: [1.08, 0.5, -1, 0],\n  \"bottom-center\": [0.5, -0.12, 0, 1],\n  center: [0.5, 0.5, 0, 1],\n};\n\nconst FRAG = `precision highp float;\nuniform vec2 uRes;\nuniform float uTime;\nuniform vec3 uColor;\nuniform vec4 uOrigin;\nuniform float uSpread;\nuniform float uLength;\nuniform float uIntensity;\nuniform float uFollow;\nuniform vec2 uMouse;\n\nfloat hash(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(hash(i), hash(i + vec2(1, 0)), f.x), mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y);\n}\n// Integer angular frequencies keep the pattern seamless all the way around.\nfloat streak(float a, float k, float ph, float sharp){ return pow(0.5 + 0.5 * sin(a * k + ph), sharp); }\n\nvoid main(){\n  float asp = uRes.x / uRes.y;\n  vec2 p = gl_FragCoord.xy / uRes.y;\n  vec2 o = uOrigin.xy * vec2(asp, 1.0);\n  vec2 m = uMouse * vec2(asp, 1.0);\n  float t = uTime;\n\n  vec2 d = p - o;\n  float dist = length(d);\n  vec2 nd = d / max(dist, 1e-4);\n\n  // Main direction leans toward the cursor and sways gently.\n  vec2 dir = normalize(mix(uOrigin.zw, normalize(m - o + 1e-4), uFollow * 0.35));\n  float sway = sin(t * 0.37) * 0.05 + sin(t * 0.21 + 1.7) * 0.035;\n  float a = atan(nd.y, nd.x) + sway;\n\n  float s = streak(a, 7.0, -t * 0.31, 2.0) * 0.35\n          + streak(a, 13.0, t * 0.53, 4.0) * 0.55\n          + streak(a, 23.0, -t * 0.47 + 1.3, 6.0) * 0.45\n          + streak(a, 37.0, t * 0.71 + 4.0, 9.0) * 0.35\n          + streak(a, 61.0, -t * 0.9 + 2.2, 12.0) * 0.2;\n  s *= 0.55 + 0.9 * noise(nd * 2.5 + vec2(t * 0.07, -t * 0.05));\n\n  float cosA = clamp(dot(nd, dir), -1.0, 1.0);\n  float ang = acos(cosA);\n  float cone = exp(-ang * ang / (uSpread * uSpread));\n\n  float fall = exp(-dist / uLength);\n  float shimmer = 0.8 + 0.2 * noise(vec2(dist * 5.0 - t * 1.2, a * 9.0));\n  float I = s * cone * fall * shimmer;\n  I += exp(-dist * dist * 9.0) * 0.45 * cone; // bloom near the source\n  I *= uIntensity;\n\n  vec3 col = uColor * I + vec3(pow(I, 3.0)) * 0.35;\n  col += (hash(gl_FragCoord.xy) - 0.5) / 255.0; // dither against banding\n  col = clamp(col, 0.0, 1.0);\n  gl_FragColor = vec4(col, max(col.r, max(col.g, col.b)));\n}`;\n\nfunction hexToRgb(hex: string) {\n  const n = parseInt(hex.replace(\"#\", \"\").padEnd(6, \"0\").slice(0, 6), 16);\n  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\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 GodRays({\n  origin = \"top-center\",\n  color = \"#d9f0ff\",\n  speed = 1,\n  spread = 0.6,\n  length = 1.2,\n  intensity = 1,\n  followMouse = 0.4,\n  className,\n}: GodRaysProps) {\n  const host = useRef<HTMLDivElement>(null);\n  const opts = useRef({ origin, color: hexToRgb(color), speed, spread, length, intensity, followMouse });\n  const redraw = useRef<() => void>(undefined);\n\n  useEffect(() => {\n    opts.current = { origin, color: hexToRgb(color), speed, spread, length, intensity, followMouse };\n    redraw.current?.();\n  }, [origin, color, speed, spread, length, intensity, followMouse]);\n\n  useEffect(() => {\n    const el = host.current!;\n    const area = el.parentElement ?? el;\n    const mouse = { x: 0.5, y: 0.5, tx: 0.5, ty: 0.5 };\n    let t = 4;\n    const shader = mountShader(el, FRAG, (set, dt) => {\n      const o = opts.current;\n      t += dt * o.speed;\n      const k = 1 - Math.exp(-dt * 3);\n      mouse.x += (mouse.tx - mouse.x) * k;\n      mouse.y += (mouse.ty - mouse.y) * k;\n      set(\"uTime\", t);\n      set(\"uColor\", o.color);\n      set(\"uOrigin\", ORIGINS[o.origin] ?? ORIGINS[\"top-center\"]);\n      set(\"uSpread\", o.origin === \"center\" ? 100 : Math.max(o.spread, 0.01));\n      set(\"uLength\", Math.max(o.length, 0.05));\n      set(\"uIntensity\", o.intensity);\n      set(\"uFollow\", o.origin === \"center\" ? 0 : o.followMouse);\n      set(\"uMouse\", [mouse.x, mouse.y]);\n    });\n    if (!shader) return;\n    redraw.current = shader.redraw;\n\n    const onMove = (e: PointerEvent) => {\n      const r = el.getBoundingClientRect();\n      mouse.tx = (e.clientX - r.left) / r.width;\n      mouse.ty = 1 - (e.clientY - r.top) / r.height;\n    };\n    const onLeave = () => {\n      mouse.tx = 0.5;\n      mouse.ty = 0.5;\n    };\n    area.addEventListener(\"pointermove\", onMove, { passive: true });\n    area.addEventListener(\"pointerleave\", onLeave);\n    return () => {\n      redraw.current = undefined;\n      area.removeEventListener(\"pointermove\", onMove);\n      area.removeEventListener(\"pointerleave\", onLeave);\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"}]}