{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "light-rays",
  "title": "Light Rays",
  "description": "Animated light rays effect using WebGL shaders.",
  "dependencies": [
    "three"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/spell-ui/light-rays.tsx",
      "content": "\"use client\";\n\nimport { useRef, useEffect, useMemo, useState, type CSSProperties } from \"react\";\nimport * as THREE from \"three\";\n\ninterface AnimationConfig {\n  animate: boolean;\n  speed: number;\n}\n\ninterface SingleColorConfig {\n  mode: \"single\";\n  color: string;\n}\n\ninterface MultiColorConfig {\n  mode: \"multi\";\n  color1: string;\n  color2: string;\n}\n\ninterface RandomColorConfig {\n  mode: \"random\";\n}\n\ntype RaysColorConfig = SingleColorConfig | MultiColorConfig | RandomColorConfig;\n\ninterface RaysProps {\n  intensity?: number;\n  rays?: number;\n  reach?: number;\n  position?: number;\n  radius?: string;\n  backgroundColor?: string;\n  animation?: AnimationConfig;\n  raysColor?: RaysColorConfig;\n  style?: CSSProperties;\n  className?: string;\n}\n\nconst RAY_Y_POSITION_1 = -0.4;\nconst RAY_Y_POSITION_2 = -0.5;\n\nexport default function Rays({\n  intensity = 13,\n  rays = 32,\n  reach = 16,\n  position = 50,\n  radius = \"0px\",\n  backgroundColor = \"#000\",\n  animation = { animate: true, speed: 10 },\n  raysColor = { mode: \"single\", color: \"#639AFF\" },\n  style,\n  className,\n}: RaysProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const meshRef = useRef<THREE.Mesh | null>(null);\n  const frameIdRef = useRef<number | undefined>(undefined);\n  const animationRef = useRef<AnimationConfig>(animation);\n\n  const [isMounted, setIsMounted] = useState(false);\n\n  useEffect(() => {\n    setIsMounted(true);\n    return () => setIsMounted(false);\n  }, []);\n\n  useEffect(() => {\n    animationRef.current = animation;\n  }, [animation]);\n\n  const [randomColor1RGB, randomColor2RGB] = useMemo(() => {\n    if (raysColor.mode === \"random\") {\n      const h = Math.random() * 360;\n      const s = 60 + Math.random() * 40;\n      return [hslToRgb(h, s, 50), hslToRgb(h, s, 65)];\n    }\n    return [\n      [1, 1, 1],\n      [1, 1, 1],\n    ] as [[number, number, number], [number, number, number]];\n  }, [raysColor.mode]);\n\n  const [color1RGB, color2RGB] = useMemo((): [\n    [number, number, number],\n    [number, number, number],\n  ] => {\n    if (raysColor.mode === \"random\") {\n      return [randomColor1RGB, randomColor2RGB] as [\n        [number, number, number],\n        [number, number, number],\n      ];\n    }\n\n    let color1 = \"#fff\";\n    let color2 = \"#fff\";\n\n    if (raysColor.mode === \"single\") {\n      color1 = raysColor.color;\n      color2 = raysColor.color;\n    } else if (raysColor.mode === \"multi\") {\n      color1 = raysColor.color1;\n      color2 = raysColor.color2;\n    }\n\n    return [colorToRGB(color1), colorToRGB(color2)];\n  }, [raysColor, randomColor1RGB, randomColor2RGB]);\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container || !isMounted) return;\n\n    const scene = new THREE.Scene();\n    const camera = new THREE.PerspectiveCamera(\n      75,\n      container.clientWidth / container.clientHeight,\n      0.1,\n      1000\n    );\n    camera.position.z = 5;\n\n    const renderer = new THREE.WebGLRenderer({\n      preserveDrawingBuffer: true,\n      premultipliedAlpha: true,\n      alpha: true,\n      antialias: true,\n      precision: \"highp\",\n      powerPreference: \"high-performance\",\n    });\n\n    renderer.setSize(container.clientWidth, container.clientHeight);\n    renderer.setPixelRatio(1);\n    container.appendChild(renderer.domElement);\n\n    const geometry = new THREE.PlaneGeometry(1024, 1024);\n    const material = new THREE.ShaderMaterial({\n      fragmentShader: FRAGMENT_SHADER,\n      vertexShader: VERTEX_SHADER,\n      uniforms: {\n        u_colors: {\n          value: [\n            new THREE.Vector4(color1RGB[0], color1RGB[1], color1RGB[2], 1),\n            new THREE.Vector4(color2RGB[0], color2RGB[1], color2RGB[2], 1),\n          ],\n        },\n        u_intensity: { value: mapRange(intensity, 0, 100, 0, 0.5) },\n        u_rays: { value: mapRange(rays, 0, 100, 0, 0.3) },\n        u_reach: { value: mapRange(reach, 0, 100, 0, 0.5) },\n        u_time: { value: Math.random() * 10000 },\n        u_mouse: { value: [0, 0] },\n        u_resolution: {\n          value: [container.clientWidth, container.clientHeight],\n        },\n        u_rayPos1: {\n          value: [\n            (position / 100) * container.clientWidth,\n            RAY_Y_POSITION_1 * container.clientHeight,\n          ],\n        },\n        u_rayPos2: {\n          value: [\n            (position / 100 + 0.02) * container.clientWidth,\n            RAY_Y_POSITION_2 * container.clientHeight,\n          ],\n        },\n      },\n      wireframe: false,\n      dithering: false,\n      side: THREE.DoubleSide,\n    });\n\n    const mesh = new THREE.Mesh(geometry, material);\n    scene.add(mesh);\n\n    meshRef.current = mesh;\n\n    let lastTime = 0;\n    const animate = (time: number) => {\n      const anim = animationRef.current;\n      if (!anim.animate) {\n        lastTime = time;\n      }\n\n      const delta = time - lastTime;\n      lastTime = time;\n\n      if (mesh.material instanceof THREE.ShaderMaterial) {\n        if (anim.animate) {\n          mesh.material.uniforms.u_time.value +=\n            (delta * anim.speed) / 1000 / 10;\n        }\n      }\n\n      renderer.render(scene, camera);\n      frameIdRef.current = requestAnimationFrame(animate);\n    };\n\n    frameIdRef.current = requestAnimationFrame(animate);\n\n    return () => {\n      if (frameIdRef.current !== undefined) {\n        cancelAnimationFrame(frameIdRef.current);\n      }\n      renderer.dispose();\n      geometry.dispose();\n      material.dispose();\n      if (container.contains(renderer.domElement)) {\n        container.removeChild(renderer.domElement);\n      }\n    };\n  }, [isMounted]);\n\n  useEffect(() => {\n    if (meshRef.current?.material instanceof THREE.ShaderMaterial) {\n      const material = meshRef.current.material;\n      const container = containerRef.current;\n      if (!container) return;\n\n      material.uniforms.u_colors.value = [\n        new THREE.Vector4(color1RGB[0], color1RGB[1], color1RGB[2], 1),\n        new THREE.Vector4(color2RGB[0], color2RGB[1], color2RGB[2], 1),\n      ];\n      material.uniforms.u_intensity.value = mapRange(intensity, 0, 100, 0, 0.5);\n      material.uniforms.u_rays.value = mapRange(rays, 0, 100, 0, 0.3);\n      material.uniforms.u_reach.value = mapRange(reach, 0, 100, 0, 0.5);\n      material.uniforms.u_rayPos1.value = [\n        (position / 100) * container.clientWidth,\n        RAY_Y_POSITION_1 * container.clientHeight,\n      ];\n      material.uniforms.u_rayPos2.value = [\n        (position / 100 + 0.02) * container.clientWidth,\n        RAY_Y_POSITION_2 * container.clientHeight,\n      ];\n    }\n  }, [intensity, rays, reach, position, color1RGB, color2RGB]);\n\n  return (\n    <div\n      ref={containerRef}\n      className={className}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        zIndex: -1,\n        borderRadius: radius,\n        overflow: \"hidden\",\n        backgroundColor,\n        ...style,\n      }}\n    />\n  );\n}\n\nfunction colorToRGB(hex: string): [number, number, number] {\n  let r = 1,\n    g = 1,\n    b = 1;\n\n  if (hex.startsWith(\"rgba(\")) {\n    const parts = hex.slice(5, -1).split(\",\");\n    r = parseInt(parts[0]) / 255;\n    g = parseInt(parts[1]) / 255;\n    b = parseInt(parts[2]) / 255;\n  } else if (hex.startsWith(\"rgb(\")) {\n    const parts = hex.slice(4, -1).split(\",\");\n    r = parseInt(parts[0]) / 255;\n    g = parseInt(parts[1]) / 255;\n    b = parseInt(parts[2]) / 255;\n  } else if (hex.startsWith(\"#\")) {\n    const c = hex.slice(1);\n    if (c.length === 3) {\n      r = parseInt(c[0] + c[0], 16) / 255;\n      g = parseInt(c[1] + c[1], 16) / 255;\n      b = parseInt(c[2] + c[2], 16) / 255;\n    } else if (c.length >= 6) {\n      r = parseInt(c.slice(0, 2), 16) / 255;\n      g = parseInt(c.slice(2, 4), 16) / 255;\n      b = parseInt(c.slice(4, 6), 16) / 255;\n    }\n  }\n\n  return [r, g, b];\n}\n\nfunction hslToRgb(h: number, s: number, l: number): [number, number, number] {\n  s /= 100;\n  l /= 100;\n  const c = (1 - Math.abs(2 * l - 1)) * s;\n  const x = c * (1 - Math.abs(((h / 60) % 2) - 1));\n  const m = l - c / 2;\n  let r = 0,\n    g = 0,\n    b = 0;\n\n  if (h >= 0 && h < 60) {\n    r = c;\n    g = x;\n    b = 0;\n  } else if (h >= 60 && h < 120) {\n    r = x;\n    g = c;\n    b = 0;\n  } else if (h >= 120 && h < 180) {\n    r = 0;\n    g = c;\n    b = x;\n  } else if (h >= 180 && h < 240) {\n    r = 0;\n    g = x;\n    b = c;\n  } else if (h >= 240 && h < 300) {\n    r = x;\n    g = 0;\n    b = c;\n  } else if (h >= 300 && h < 360) {\n    r = c;\n    g = 0;\n    b = x;\n  }\n\n  return [r + m, g + m, b + m];\n}\n\nfunction mapRange(\n  value: number,\n  fromLow: number,\n  fromHigh: number,\n  toLow: number,\n  toHigh: number\n): number {\n  const percentage = (value - fromLow) / (fromHigh - fromLow);\n  return toLow + percentage * (toHigh - toLow);\n}\n\nconst VERTEX_SHADER = `\nvoid main() {\n  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nconst FRAGMENT_SHADER = `\nuniform vec2 u_resolution;\nuniform vec2 u_mouse;\nuniform float u_time;\nuniform vec4 u_colors[2];\nuniform float u_intensity;\nuniform float u_rays;\nuniform float u_reach;\nuniform vec2 u_rayPos1;\nuniform vec2 u_rayPos2;\n\nfloat rayStrength(vec2 raySource, vec2 rayRefDirection, vec2 coord, float seedA, float seedB, float speed) {\n    vec2 sourceToCoord = coord - raySource;\n    float cosAngle = dot(normalize(sourceToCoord), rayRefDirection);\n    float diagonal = length(u_resolution);\n\n    return clamp(\n        (.45 + 0.15 * sin(cosAngle * seedA + u_time * speed)) +\n        (0.3 + 0.2 * cos(-cosAngle * seedB + u_time * speed)),\n        u_reach, 1.0) *\n        clamp((diagonal - length(sourceToCoord)) / diagonal, u_reach, 1.0);\n}\n\nvoid main() {\n    vec2 uv = gl_FragCoord.xy / u_resolution.xy;\n    uv.y = 1.0 - uv.y;\n    vec2 coord = vec2(gl_FragCoord.x, u_resolution.y - gl_FragCoord.y);\n    float speed = u_rays * 10.0;\n\n    vec2 rayPos1 = u_rayPos1;\n    vec2 rayRefDir1 = normalize(vec2(1.0, -0.116));\n    float raySeedA1 = 36.2214 * speed;\n    float raySeedB1 = 21.11349 * speed;\n    float raySpeed1 = 1.5 * speed;\n\n    vec2 rayPos2 = u_rayPos2;\n    vec2 rayRefDir2 = normalize(vec2(1.0, 0.241));\n    float raySeedA2 = 22.39910 * speed;\n    float raySeedB2 = 18.0234 * speed;\n    float raySpeed2 = 1.1 * speed;\n\n    float strength1 = rayStrength(rayPos1, rayRefDir1, coord, raySeedA1, raySeedB1, raySpeed1);\n    float strength2 = rayStrength(rayPos2, rayRefDir2, coord, raySeedA2, raySeedB2, raySpeed2);\n\n    float brightness = 1.0 * u_reach - (coord.y / u_resolution.y);\n    float attenuation = clamp(brightness + (0.5 + u_intensity), 0.0, 1.0);\n\n    float alpha1 = strength1 * attenuation * u_colors[0].a;\n    float alpha2 = strength2 * attenuation * u_colors[1].a;\n\n    vec3 premultColor1 = u_colors[0].rgb * alpha1;\n    vec3 premultColor2 = u_colors[1].rgb * alpha2;\n\n    vec3 blendedColor = premultColor1 + premultColor2;\n    float blendedAlpha = alpha1 + alpha2 * (1.0 - alpha1);\n\n    vec3 finalRGB = blendedColor / max(blendedAlpha, 0.0001);\n\n    gl_FragColor = vec4(finalRGB * blendedAlpha, blendedAlpha);\n}\n`;\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}