{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "fluid-orb",
  "title": "Fluid Orb",
  "description": "An animated WebGL orb with drifting fluid shading. Supports size and color props.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "components/ui/fluid-orb.tsx",
      "content": "'use client'\n\nimport React, { useEffect, useRef } from 'react'\n\nimport { cn } from '@/lib/utils'\n\nexport type FluidOrbProps = React.ComponentProps<'div'> & {\n  size?: number\n  color?: 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 = `\n#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\n\nuniform vec2 u_resolution;\nuniform float u_time;\nuniform vec3 u_color;\n\nfloat hash(vec2 p) {\n  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nfloat noise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  vec2 u = f * f * (3.0 - 2.0 * f);\n  return mix(\n    mix(hash(i + vec2(0.0, 0.0)), hash(i + vec2(1.0, 0.0)), u.x),\n    mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x),\n    u.y\n  );\n}\n\nfloat fbm(vec2 p) {\n  float v = 0.0;\n  float a = 0.6;\n  for (int i = 0; i < 3; i++) {\n    v += a * noise(p);\n    p *= 2.0;\n    a *= 0.5;\n  }\n  return v;\n}\n\nvoid main() {\n  vec2 uv = gl_FragCoord.xy / u_resolution.xy;\n  float t = u_time * 0.22;\n\n  vec2 drift = vec2(\n    sin(t) + 0.6 * sin(t * 1.7 + 1.3),\n    cos(t * 0.8) + 0.6 * cos(t * 1.3 + 2.1)\n  );\n\n  vec2 p = vec2(uv.x * 1.8, uv.y * 1.0) + drift * 0.7;\n\n  vec2 q = vec2(fbm(p + drift), fbm(p + vec2(3.2, 1.5) - drift));\n  float f = fbm(p + 1.2 * q);\n\n  float g = clamp(1.0 - uv.y, 0.0, 1.0);\n  float anchor = smoothstep(0.0, 0.3, uv.y);\n  float shade = clamp(g + (f - 0.5) * 0.8 * anchor, 0.0, 1.0);\n\n  vec3 white = vec3(0.99, 1.0, 1.0);\n  vec3 light = mix(white, u_color, 0.5);\n  vec3 dark = u_color;\n\n  vec3 col = white;\n  col = mix(col, light, smoothstep(0.28, 0.52, shade));\n  col = mix(col, dark, smoothstep(0.58, 0.88, shade));\n\n  float edge = smoothstep(0.5, 0.49, distance(uv, vec2(0.5)));\n\n  gl_FragColor = vec4(col * edge, edge);\n}\n`\n\nfunction hexToRgb(hex: string): [number, number, number] {\n  let h = hex.replace('#', '').trim()\n  if (h.length === 3) {\n    h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2]\n  }\n  const n = parseInt(h, 16)\n  if (h.length !== 6 || Number.isNaN(n)) return [0.1, 0.45, 0.95]\n  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255]\n}\n\nfunction compile(gl: WebGLRenderingContext, type: number, src: string) {\n  const shader = gl.createShader(type)\n  if (!shader) return null\n  gl.shaderSource(shader, src)\n  gl.compileShader(shader)\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    console.error(gl.getShaderInfoLog(shader))\n    gl.deleteShader(shader)\n    return null\n  }\n  return shader\n}\n\nconst FluidOrb = ({\n  size = 240,\n  color = '#1A73F2',\n  className,\n  style,\n  ...props\n}: FluidOrbProps) => {\n  const canvasRef = useRef<HTMLCanvasElement>(null)\n\n  useEffect(() => {\n    const canvas = canvasRef.current\n    if (!canvas) return\n\n    const gl = canvas.getContext('webgl', { antialias: true, alpha: true })\n    if (!gl) return\n\n    const program = gl.createProgram()\n    const vert = compile(gl, gl.VERTEX_SHADER, VERT)\n    const frag = compile(gl, gl.FRAGMENT_SHADER, FRAG)\n    if (!program || !vert || !frag) return\n\n    gl.attachShader(program, vert)\n    gl.attachShader(program, frag)\n    gl.linkProgram(program)\n    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n      console.error(gl.getProgramInfoLog(program))\n      return\n    }\n    gl.useProgram(program)\n\n    const buffer = gl.createBuffer()\n    gl.bindBuffer(gl.ARRAY_BUFFER, buffer)\n    gl.bufferData(\n      gl.ARRAY_BUFFER,\n      new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),\n      gl.STATIC_DRAW,\n    )\n    const aPos = gl.getAttribLocation(program, 'a_pos')\n    gl.enableVertexAttribArray(aPos)\n    gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0)\n\n    const uResolution = gl.getUniformLocation(program, 'u_resolution')\n    const uTime = gl.getUniformLocation(program, 'u_time')\n    gl.uniform3f(gl.getUniformLocation(program, 'u_color'), ...hexToRgb(color))\n\n    const dpr = Math.min(window.devicePixelRatio || 1, 2)\n    const px = Math.round(size * dpr)\n    canvas.width = px\n    canvas.height = px\n    gl.viewport(0, 0, px, px)\n    gl.uniform2f(uResolution, px, px)\n\n    const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches\n    const start = performance.now()\n    let raf = 0\n\n    const render = (now: number) => {\n      gl.uniform1f(uTime, reduce ? 0 : (now - start) / 1000)\n      gl.drawArrays(gl.TRIANGLES, 0, 6)\n      if (!reduce) raf = requestAnimationFrame(render)\n    }\n    render(start)\n\n    return () => {\n      cancelAnimationFrame(raf)\n      gl.deleteProgram(program)\n      gl.deleteShader(vert)\n      gl.deleteShader(frag)\n      gl.deleteBuffer(buffer)\n    }\n  }, [size, color])\n\n  return (\n    <div\n      data-slot=\"fluid-orb\"\n      className={cn('relative overflow-hidden rounded-full', className)}\n      style={{\n        width: size,\n        height: size,\n        ...style,\n      }}\n      {...props}\n    >\n      <canvas ref={canvasRef} className=\"h-full w-full\" />\n    </div>\n  )\n}\n\nexport default FluidOrb\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}