{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "orb",
  "type": "registry:ui",
  "dependencies": [
    "@react-three/drei",
    "@react-three/fiber",
    "three",
    "@types/three"
  ],
  "files": [
    {
      "path": "components/ui/orb.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useMemo, useRef } from \"react\"\nimport { useTexture } from \"@react-three/drei\"\nimport { Canvas, useFrame, useThree } from \"@react-three/fiber\"\nimport * as THREE from \"three\"\n\nexport type AgentState = null | \"thinking\" | \"listening\" | \"talking\"\n\ntype OrbProps = {\n  colors?: [string, string]\n  colorsRef?: React.RefObject<[string, string]>\n  resizeDebounce?: number\n  seed?: number\n  agentState?: AgentState\n  volumeMode?: \"auto\" | \"manual\"\n  manualInput?: number\n  manualOutput?: number\n  inputVolumeRef?: React.RefObject<number>\n  outputVolumeRef?: React.RefObject<number>\n  getInputVolume?: () => number\n  getOutputVolume?: () => number\n  className?: string\n}\n\nexport function Orb({\n  colors = [\"#CADCFC\", \"#A0B9D1\"],\n  colorsRef,\n  resizeDebounce = 100,\n  seed,\n  agentState = null,\n  volumeMode = \"auto\",\n  manualInput,\n  manualOutput,\n  inputVolumeRef,\n  outputVolumeRef,\n  getInputVolume,\n  getOutputVolume,\n  className,\n}: OrbProps) {\n  return (\n    <div className={className ?? \"relative h-full w-full\"}>\n      <Canvas\n        resize={{ debounce: resizeDebounce }}\n        gl={{\n          alpha: true,\n          antialias: true,\n          premultipliedAlpha: true,\n        }}\n      >\n        <Scene\n          colors={colors}\n          colorsRef={colorsRef}\n          seed={seed}\n          agentState={agentState}\n          volumeMode={volumeMode}\n          manualInput={manualInput}\n          manualOutput={manualOutput}\n          inputVolumeRef={inputVolumeRef}\n          outputVolumeRef={outputVolumeRef}\n          getInputVolume={getInputVolume}\n          getOutputVolume={getOutputVolume}\n        />\n      </Canvas>\n    </div>\n  )\n}\n\nfunction Scene({\n  colors,\n  colorsRef,\n  seed,\n  agentState,\n  volumeMode,\n  manualInput,\n  manualOutput,\n  inputVolumeRef,\n  outputVolumeRef,\n  getInputVolume,\n  getOutputVolume,\n}: {\n  colors: [string, string]\n  colorsRef?: React.RefObject<[string, string]>\n  seed?: number\n  agentState: AgentState\n  volumeMode: \"auto\" | \"manual\"\n  manualInput?: number\n  manualOutput?: number\n  inputVolumeRef?: React.RefObject<number>\n  outputVolumeRef?: React.RefObject<number>\n  getInputVolume?: () => number\n  getOutputVolume?: () => number\n}) {\n  const { gl } = useThree()\n  const circleRef =\n    useRef<THREE.Mesh<THREE.CircleGeometry, THREE.ShaderMaterial>>(null)\n  const initialColorsRef = useRef<[string, string]>(colors)\n  const targetColor1Ref = useRef(new THREE.Color(colors[0]))\n  const targetColor2Ref = useRef(new THREE.Color(colors[1]))\n  const animSpeedRef = useRef(0.1)\n  const perlinNoiseTexture = useTexture(\n    \"https://storage.googleapis.com/eleven-public-cdn/images/perlin-noise.png\"\n  )\n\n  const agentRef = useRef<AgentState>(agentState)\n  const modeRef = useRef<\"auto\" | \"manual\">(volumeMode)\n  const manualInRef = useRef<number>(manualInput ?? 0)\n  const manualOutRef = useRef<number>(manualOutput ?? 0)\n  const curInRef = useRef(0)\n  const curOutRef = useRef(0)\n\n  useEffect(() => {\n    agentRef.current = agentState\n  }, [agentState])\n\n  useEffect(() => {\n    modeRef.current = volumeMode\n  }, [volumeMode])\n\n  useEffect(() => {\n    manualInRef.current = clamp01(\n      manualInput ?? inputVolumeRef?.current ?? getInputVolume?.() ?? 0\n    )\n  }, [manualInput, inputVolumeRef, getInputVolume])\n\n  useEffect(() => {\n    manualOutRef.current = clamp01(\n      manualOutput ?? outputVolumeRef?.current ?? getOutputVolume?.() ?? 0\n    )\n  }, [manualOutput, outputVolumeRef, getOutputVolume])\n\n  const random = useMemo(\n    () => splitmix32(seed ?? Math.floor(Math.random() * 2 ** 32)),\n    [seed]\n  )\n  const offsets = useMemo(\n    () =>\n      new Float32Array(Array.from({ length: 7 }, () => random() * Math.PI * 2)),\n    [random]\n  )\n\n  useEffect(() => {\n    targetColor1Ref.current = new THREE.Color(colors[0])\n    targetColor2Ref.current = new THREE.Color(colors[1])\n  }, [colors])\n\n  useEffect(() => {\n    const apply = () => {\n      if (!circleRef.current) return\n      const isDark = document.documentElement.classList.contains(\"dark\")\n      circleRef.current.material.uniforms.uInverted.value = isDark ? 1 : 0\n    }\n\n    apply()\n\n    const observer = new MutationObserver(apply)\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    })\n    return () => observer.disconnect()\n  }, [])\n\n  useFrame((_, delta: number) => {\n    const mat = circleRef.current?.material\n    if (!mat) return\n    const live = colorsRef?.current\n    if (live) {\n      if (live[0]) targetColor1Ref.current.set(live[0])\n      if (live[1]) targetColor2Ref.current.set(live[1])\n    }\n    const u = mat.uniforms\n    u.uTime.value += delta * 0.5\n\n    if (u.uOpacity.value < 1) {\n      u.uOpacity.value = Math.min(1, u.uOpacity.value + delta * 2)\n    }\n\n    let targetIn = 0\n    let targetOut = 0.3\n    if (modeRef.current === \"manual\") {\n      targetIn = clamp01(\n        manualInput ?? inputVolumeRef?.current ?? getInputVolume?.() ?? 0\n      )\n      targetOut = clamp01(\n        manualOutput ?? outputVolumeRef?.current ?? getOutputVolume?.() ?? 0\n      )\n    } else {\n      const t = u.uTime.value * 2\n      if (agentRef.current === null) {\n        targetIn = 0\n        targetOut = 0.3\n      } else if (agentRef.current === \"listening\") {\n        targetIn = clamp01(0.55 + Math.sin(t * 3.2) * 0.35)\n        targetOut = 0.45\n      } else if (agentRef.current === \"talking\") {\n        targetIn = clamp01(0.65 + Math.sin(t * 4.8) * 0.22)\n        targetOut = clamp01(0.75 + Math.sin(t * 3.6) * 0.22)\n      } else {\n        const base = 0.38 + 0.07 * Math.sin(t * 0.7)\n        const wander = 0.05 * Math.sin(t * 2.1) * Math.sin(t * 0.37 + 1.2)\n        targetIn = clamp01(base + wander)\n        targetOut = clamp01(0.48 + 0.12 * Math.sin(t * 1.05 + 0.6))\n      }\n    }\n\n    curInRef.current += (targetIn - curInRef.current) * 0.2\n    curOutRef.current += (targetOut - curOutRef.current) * 0.2\n\n    const targetSpeed = 0.1 + (1 - Math.pow(curOutRef.current - 1, 2)) * 0.9\n    animSpeedRef.current += (targetSpeed - animSpeedRef.current) * 0.12\n\n    u.uAnimation.value += delta * animSpeedRef.current\n    u.uInputVolume.value = curInRef.current\n    u.uOutputVolume.value = curOutRef.current\n    u.uColor1.value.lerp(targetColor1Ref.current, 0.08)\n    u.uColor2.value.lerp(targetColor2Ref.current, 0.08)\n  })\n\n  useEffect(() => {\n    const canvas = gl.domElement\n    const onContextLost = (event: Event) => {\n      event.preventDefault()\n      setTimeout(() => {\n        gl.forceContextRestore()\n      }, 1)\n    }\n    canvas.addEventListener(\"webglcontextlost\", onContextLost, false)\n    return () =>\n      canvas.removeEventListener(\"webglcontextlost\", onContextLost, false)\n  }, [gl])\n\n  const uniforms = useMemo(() => {\n    perlinNoiseTexture.wrapS = THREE.RepeatWrapping\n    perlinNoiseTexture.wrapT = THREE.RepeatWrapping\n    const isDark =\n      typeof document !== \"undefined\" &&\n      document.documentElement.classList.contains(\"dark\")\n    return {\n      uColor1: new THREE.Uniform(new THREE.Color(initialColorsRef.current[0])),\n      uColor2: new THREE.Uniform(new THREE.Color(initialColorsRef.current[1])),\n      uOffsets: { value: offsets },\n      uPerlinTexture: new THREE.Uniform(perlinNoiseTexture),\n      uTime: new THREE.Uniform(0),\n      uAnimation: new THREE.Uniform(0.1),\n      uInverted: new THREE.Uniform(isDark ? 1 : 0),\n      uInputVolume: new THREE.Uniform(0),\n      uOutputVolume: new THREE.Uniform(0),\n      uOpacity: new THREE.Uniform(0),\n    }\n  }, [perlinNoiseTexture, offsets])\n\n  return (\n    <mesh ref={circleRef}>\n      <circleGeometry args={[3.5, 64]} />\n      <shaderMaterial\n        uniforms={uniforms}\n        fragmentShader={fragmentShader}\n        vertexShader={vertexShader}\n        transparent={true}\n      />\n    </mesh>\n  )\n}\n\nfunction splitmix32(a: number) {\n  return function () {\n    a |= 0\n    a = (a + 0x9e3779b9) | 0\n    let t = a ^ (a >>> 16)\n    t = Math.imul(t, 0x21f0aaad)\n    t = t ^ (t >>> 15)\n    t = Math.imul(t, 0x735a2d97)\n    return ((t = t ^ (t >>> 15)) >>> 0) / 4294967296\n  }\n}\n\nfunction clamp01(n: number) {\n  if (!Number.isFinite(n)) return 0\n  return Math.min(1, Math.max(0, n))\n}\nconst vertexShader = /* glsl */ `\nuniform float uTime;\nuniform sampler2D uPerlinTexture;\nvarying vec2 vUv;\n\nvoid main() {\n  vUv = uv;\n  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`\n\nconst fragmentShader = /* glsl */ `\nuniform float uTime;\nuniform float uAnimation;\nuniform float uInverted;\nuniform float uOffsets[7];\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform float uInputVolume;\nuniform float uOutputVolume;\nuniform float uOpacity;\nuniform sampler2D uPerlinTexture;\nvarying vec2 vUv;\n\nconst float PI = 3.14159265358979323846;\n\n// Draw a single oval with soft edges and calculate its gradient color\nbool drawOval(vec2 polarUv, vec2 polarCenter, float a, float b, bool reverseGradient, float softness, out vec4 color) {\n    vec2 p = polarUv - polarCenter;\n    float oval = (p.x * p.x) / (a * a) + (p.y * p.y) / (b * b);\n\n    float edge = smoothstep(1.0, 1.0 - softness, oval);\n\n    if (edge > 0.0) {\n        float gradient = reverseGradient ? (1.0 - (p.x / a + 1.0) / 2.0) : ((p.x / a + 1.0) / 2.0);\n        // Flatten gradient toward middle value for more uniform appearance\n        gradient = mix(0.5, gradient, 0.1);\n        color = vec4(vec3(gradient), 0.85 * edge);\n        return true;\n    }\n    return false;\n}\n\n// Map grayscale value to a 4-color ramp (color1, color2, color3, color4)\nvec3 colorRamp(float grayscale, vec3 color1, vec3 color2, vec3 color3, vec3 color4) {\n    if (grayscale < 0.33) {\n        return mix(color1, color2, grayscale * 3.0);\n    } else if (grayscale < 0.66) {\n        return mix(color2, color3, (grayscale - 0.33) * 3.0);\n    } else {\n        return mix(color3, color4, (grayscale - 0.66) * 3.0);\n    }\n}\n\nvec2 hash2(vec2 p) {\n    return fract(sin(vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)))) * 43758.5453);\n}\n\n// 2D noise for the ring\nfloat noise2D(vec2 p) {\n    vec2 i = floor(p);\n    vec2 f = fract(p);\n    \n    vec2 u = f * f * (3.0 - 2.0 * f);\n    float n = mix(\n        mix(dot(hash2(i + vec2(0.0, 0.0)), f - vec2(0.0, 0.0)),\n            dot(hash2(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0)), u.x),\n        mix(dot(hash2(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0)),\n            dot(hash2(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0)), u.x),\n        u.y\n    );\n\n    return 0.5 + 0.5 * n;\n}\n\nfloat sharpRing(vec3 decomposed, float time) {\n    float ringStart = 1.0;\n    float ringWidth = 0.3;\n    float noiseScale = 5.0;\n\n    float noise = mix(\n        noise2D(vec2(decomposed.x, time) * noiseScale),\n        noise2D(vec2(decomposed.y, time) * noiseScale),\n        decomposed.z\n    );\n\n    noise = (noise - 0.5) * 2.5;\n\n    return ringStart + noise * ringWidth * 1.5;\n}\n\nfloat smoothRing(vec3 decomposed, float time) {\n    float ringStart = 0.9;\n    float ringWidth = 0.2;\n    float noiseScale = 6.0;\n\n    float noise = mix(\n        noise2D(vec2(decomposed.x, time) * noiseScale),\n        noise2D(vec2(decomposed.y, time) * noiseScale),\n        decomposed.z\n    );\n\n    noise = (noise - 0.5) * 5.0;\n\n    return ringStart + noise * ringWidth;\n}\n\nfloat flow(vec3 decomposed, float time) {\n    return mix(\n        texture(uPerlinTexture, vec2(time, decomposed.x / 2.0)).r,\n        texture(uPerlinTexture, vec2(time, decomposed.y / 2.0)).r,\n        decomposed.z\n    );\n}\n\nvoid main() {\n    // Normalize vUv to be centered around (0.0, 0.0)\n    vec2 uv = vUv * 2.0 - 1.0;\n\n    // Convert uv to polar coordinates\n    float radius = length(uv);\n    float theta = atan(uv.y, uv.x);\n    if (theta < 0.0) theta += 2.0 * PI; // Normalize theta to [0, 2*PI]\n\n    // Decomposed angle is used for sampling noise textures without seams:\n    // float noise = mix(sample(decomposed.x), sample(decomposed.y), decomposed.z);\n    vec3 decomposed = vec3(\n        // angle in the range [0, 1]\n        theta / (2.0 * PI),\n        // angle offset by 180 degrees in the range [1, 2]\n        mod(theta / (2.0 * PI) + 0.5, 1.0) + 1.0,\n        // mixing factor between two noises\n        abs(theta / PI - 1.0)\n    );\n\n    // Add noise to the angle for a flow-like distortion (reduced for flatter look)\n    float noise = flow(decomposed, radius * 0.03 - uAnimation * 0.2) - 0.5;\n    theta += noise * mix(0.08, 0.25, uOutputVolume);\n\n    // Initialize the base color to white\n    vec4 color = vec4(1.0, 1.0, 1.0, 1.0);\n\n    // Original parameters for the ovals in polar coordinates\n    float originalCenters[7] = float[7](0.0, 0.5 * PI, 1.0 * PI, 1.5 * PI, 2.0 * PI, 2.5 * PI, 3.0 * PI);\n\n    // Parameters for the animated centers in polar coordinates\n    float centers[7];\n    for (int i = 0; i < 7; i++) {\n        centers[i] = originalCenters[i] + 0.5 * sin(uTime / 20.0 + uOffsets[i]);\n    }\n\n    float a, b;\n    vec4 ovalColor;\n\n    // Check if the pixel is inside any of the ovals\n    for (int i = 0; i < 7; i++) {\n        float noise = texture(uPerlinTexture, vec2(mod(centers[i] + uTime * 0.05, 1.0), 0.5)).r;\n        a = 0.5 + noise * 0.3; // Increased for more coverage\n        b = noise * mix(3.5, 2.5, uInputVolume); // Increased height for fuller appearance\n        bool reverseGradient = (i % 2 == 1); // Reverse gradient for every second oval\n\n        // Calculate the distance in polar coordinates\n        float distTheta = min(\n            abs(theta - centers[i]),\n            min(\n                abs(theta + 2.0 * PI - centers[i]),\n                abs(theta - 2.0 * PI - centers[i])\n            )\n        );\n        float distRadius = radius;\n\n        float softness = 0.6; // Increased softness for flatter, less pronounced edges\n\n        // Check if the pixel is inside the oval in polar coordinates\n        if (drawOval(vec2(distTheta, distRadius), vec2(0.0, 0.0), a, b, reverseGradient, softness, ovalColor)) {\n            // Blend the oval color with the existing color\n            color.rgb = mix(color.rgb, ovalColor.rgb, ovalColor.a);\n            color.a = max(color.a, ovalColor.a); // Max alpha\n        }\n    }\n    \n    // Calculate both noisy rings\n    float ringRadius1 = sharpRing(decomposed, uTime * 0.1);\n    float ringRadius2 = smoothRing(decomposed, uTime * 0.1);\n    \n    // Adjust rings based on input volume (reduced for flatter appearance)\n    float inputRadius1 = radius + uInputVolume * 0.2;\n    float inputRadius2 = radius + uInputVolume * 0.15;\n    float opacity1 = mix(0.2, 0.6, uInputVolume);\n    float opacity2 = mix(0.15, 0.45, uInputVolume);\n\n    // Blend both rings\n    float ringAlpha1 = (inputRadius2 >= ringRadius1) ? opacity1 : 0.0;\n    float ringAlpha2 = smoothstep(ringRadius2 - 0.05, ringRadius2 + 0.05, inputRadius1) * opacity2;\n    \n    float totalRingAlpha = max(ringAlpha1, ringAlpha2);\n    \n    // Apply screen blend mode for combined rings\n    vec3 ringColor = vec3(1.0); // White ring color\n    color.rgb = 1.0 - (1.0 - color.rgb) * (1.0 - ringColor * totalRingAlpha);\n\n    // Define colours to ramp against greyscale (could increase the amount of colours in the ramp)\n    vec3 color1 = vec3(0.0, 0.0, 0.0); // Black\n    vec3 color2 = uColor1; // Darker Color\n    vec3 color3 = uColor2; // Lighter Color\n    vec3 color4 = vec3(1.0, 1.0, 1.0); // White\n\n    // Convert grayscale color to the color ramp\n    float luminance = mix(color.r, 1.0 - color.r, uInverted);\n    color.rgb = colorRamp(luminance, color1, color2, color3, color4); // Apply the color ramp\n\n    // Apply fade-in opacity\n    color.a *= uOpacity;\n\n    gl_FragColor = color;\n}\n`\n",
      "type": "registry:ui"
    }
  ]
}