{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "orb",
	"title": "Orb",
	"type": "registry:ui",
	"description": "A 3D animated orb built with Three.js + Threlte, audio-reactive with GLSL shaders.",
	"dependencies": [
		"@threlte/core",
		"@threlte/extras",
		"mode-watcher",
		"three"
	],
	"devDependencies": [
		"@types/three"
	],
	"files": [
		{
			"content": "<script lang=\"ts\" module>\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport type { OrbAgentState } from \"./types.js\";\n\n\texport type OrbProps = Omit<HTMLAttributes<HTMLDivElement>, \"children\"> & {\n\t\t/**\n\t\t * Two hex colors sampled across the orb's gradient. Update reactively\n\t\t * to animate between palettes.\n\t\t * @default [\"#CADCFC\", \"#A0B9D1\"]\n\t\t */\n\t\tcolors?: [string, string];\n\t\t/**\n\t\t * Seed for the orb's internal noise, determining its overall shape.\n\t\t * Defaults to a stable per-instance random value so multiple orbs on\n\t\t * one page look distinct.\n\t\t */\n\t\tseed?: number;\n\t\t/**\n\t\t * Drives the orb's visual behavior to reflect the agent lifecycle.\n\t\t * Pass `null` to render the idle state.\n\t\t * @default null\n\t\t */\n\t\tagentState?: OrbAgentState;\n\t\t/**\n\t\t * `\"auto\"` uses the active microphone and output audio streams to\n\t\t * drive reactivity. `\"manual\"` reads `manualInput` / `manualOutput`\n\t\t * (or the `get*Volume` callbacks) instead.\n\t\t * @default \"auto\"\n\t\t */\n\t\tvolumeMode?: \"auto\" | \"manual\";\n\t\t/** Manual input volume in `[0, 1]`. Only read when `volumeMode=\"manual\"`. */\n\t\tmanualInput?: number;\n\t\t/** Manual output volume in `[0, 1]`. Only read when `volumeMode=\"manual\"`. */\n\t\tmanualOutput?: number;\n\t\t/**\n\t\t * Called every frame to sample input volume in `[0, 1]`. Takes\n\t\t * precedence over `manualInput` when both are provided.\n\t\t */\n\t\tgetInputVolume?: () => number;\n\t\t/**\n\t\t * Called every frame to sample output volume in `[0, 1]`. Takes\n\t\t * precedence over `manualOutput` when both are provided.\n\t\t */\n\t\tgetOutputVolume?: () => number;\n\t};\n</script>\n\n<script lang=\"ts\">\n\timport { Canvas } from \"@threlte/core\";\n\timport { ACESFilmicToneMapping, WebGLRenderer } from \"three\";\n\timport OrbScene from \"./orb-scene.svelte\";\n\timport { cn } from \"$UTILS$.js\";\n\n\tlet {\n\t\tcolors = [\"#CADCFC\", \"#A0B9D1\"],\n\t\tseed,\n\t\tagentState = null,\n\t\tvolumeMode = \"auto\",\n\t\tmanualInput,\n\t\tmanualOutput,\n\t\tgetInputVolume,\n\t\tgetOutputVolume,\n\t\tclass: className,\n\t\t...restProps\n\t}: OrbProps = $props();\n\n\t// Stable per-instance fallback seed if none is passed\n\tconst instanceSeed = Math.floor(Math.random() * 2 ** 32);\n\tconst resolvedSeed = $derived(seed ?? instanceSeed);\n</script>\n\n<div data-slot=\"orb\" class={cn(\"relative h-full w-full\", className)} {...restProps}>\n\t<Canvas\n\t\ttoneMapping={ACESFilmicToneMapping}\n\t\tcreateRenderer={(canvas) =>\n\t\t\tnew WebGLRenderer({\n\t\t\t\tcanvas,\n\t\t\t\talpha: true,\n\t\t\t\tantialias: true,\n\t\t\t\tpremultipliedAlpha: true,\n\t\t\t})}\n\t>\n\t\t<OrbScene\n\t\t\t{colors}\n\t\t\tseed={resolvedSeed}\n\t\t\t{agentState}\n\t\t\t{volumeMode}\n\t\t\t{manualInput}\n\t\t\t{manualOutput}\n\t\t\t{getInputVolume}\n\t\t\t{getOutputVolume}\n\t\t/>\n\t</Canvas>\n</div>\n",
			"type": "registry:ui",
			"target": "orb/orb.svelte"
		},
		{
			"content": "<script lang=\"ts\">\n\timport { T, useTask, useThrelte } from \"@threlte/core\";\n\timport { useTexture } from \"@threlte/extras\";\n\timport { onMount, untrack } from \"svelte\";\n\timport * as THREE from \"three\";\n\timport { mode } from \"mode-watcher\";\n\timport type { OrbAgentState } from \"./types.js\";\n\timport fragmentShader from \"./shaders/orb.frag.glsl?raw\";\n\timport vertexShader from \"./shaders/orb.vert.glsl?raw\";\n\n\ttype Props = {\n\t\tcolors: [string, string];\n\t\tseed: number;\n\t\tagentState: OrbAgentState;\n\t\tvolumeMode: \"auto\" | \"manual\";\n\t\tmanualInput?: number;\n\t\tmanualOutput?: number;\n\t\tgetInputVolume?: () => number;\n\t\tgetOutputVolume?: () => number;\n\t};\n\n\tlet {\n\t\tcolors,\n\t\tseed,\n\t\tagentState,\n\t\tvolumeMode,\n\t\tmanualInput,\n\t\tmanualOutput,\n\t\tgetInputVolume,\n\t\tgetOutputVolume,\n\t}: Props = $props();\n\n\tconst { canvas, renderer } = useThrelte();\n\n\tlet mesh = $state.raw<THREE.Mesh<THREE.CircleGeometry, THREE.ShaderMaterial>>();\n\n\tconst targetColor1 = new THREE.Color(colors[0]);\n\tconst targetColor2 = new THREE.Color(colors[1]);\n\tlet curIn = 0;\n\tlet curOut = 0;\n\tlet animSpeed = 0.1;\n\tconst offsets = makeOffsets(seed);\n\n\t$effect(() => {\n\t\ttargetColor1.set(colors[0]);\n\t\ttargetColor2.set(colors[1]);\n\t});\n\n\t$effect(() => {\n\t\tconst inverted = mode.current === \"dark\" ? 1 : 0;\n\t\tconst mat = mesh?.material;\n\t\tif (!mat) return;\n\t\tmat.uniforms.uInverted.value = inverted;\n\t});\n\n\tonMount(() => {\n\t\tconst onLost = (e: Event) => {\n\t\t\te.preventDefault();\n\t\t\tsetTimeout(() => renderer.forceContextRestore(), 1);\n\t\t};\n\t\tcanvas.addEventListener(\"webglcontextlost\", onLost, false);\n\t\treturn () => canvas.removeEventListener(\"webglcontextlost\", onLost, false);\n\t});\n\n\tconst texture = useTexture(\"https://sv11.ui.twango.dev/orbs/perlin-noise.png\", {\n\t\ttransform: (t) => {\n\t\t\tt.wrapS = THREE.RepeatWrapping;\n\t\t\tt.wrapT = THREE.RepeatWrapping;\n\t\t\tt.colorSpace = THREE.NoColorSpace;\n\t\t\treturn t;\n\t\t},\n\t});\n\n\tfunction makeUniforms(perlinTexture: THREE.Texture<HTMLImageElement>) {\n\t\treturn untrack(() => ({\n\t\t\tuColor1: new THREE.Uniform(new THREE.Color(colors[0])),\n\t\t\tuColor2: new THREE.Uniform(new THREE.Color(colors[1])),\n\t\t\tuOffsets: { value: offsets },\n\t\t\tuPerlinTexture: new THREE.Uniform(perlinTexture),\n\t\t\tuTime: new THREE.Uniform(0),\n\t\t\tuAnimation: new THREE.Uniform(0.1),\n\t\t\tuInverted: new THREE.Uniform(mode.current === \"dark\" ? 1 : 0),\n\t\t\tuInputVolume: new THREE.Uniform(0),\n\t\t\tuOutputVolume: new THREE.Uniform(0),\n\t\t\tuOpacity: new THREE.Uniform(0),\n\t\t}));\n\t}\n\n\tuseTask((delta) => {\n\t\tconst mat = mesh?.material;\n\t\tif (!mat) return;\n\t\tconst u = mat.uniforms;\n\t\tu.uTime.value += delta * 0.5;\n\n\t\tif (u.uOpacity.value < 1) {\n\t\t\tu.uOpacity.value = Math.min(1, u.uOpacity.value + delta * 2);\n\t\t}\n\n\t\tlet targetIn = 0;\n\t\tlet targetOut = 0.3;\n\t\tif (volumeMode === \"manual\") {\n\t\t\ttargetIn = clamp01(manualInput ?? getInputVolume?.() ?? 0);\n\t\t\ttargetOut = clamp01(manualOutput ?? getOutputVolume?.() ?? 0);\n\t\t} else {\n\t\t\tconst t = u.uTime.value * 2;\n\t\t\tif (agentState === null) {\n\t\t\t\ttargetIn = 0;\n\t\t\t\ttargetOut = 0.3;\n\t\t\t} else if (agentState === \"listening\") {\n\t\t\t\ttargetIn = clamp01(0.55 + Math.sin(t * 3.2) * 0.35);\n\t\t\t\ttargetOut = 0.45;\n\t\t\t} else if (agentState === \"talking\") {\n\t\t\t\ttargetIn = clamp01(0.65 + Math.sin(t * 4.8) * 0.22);\n\t\t\t\ttargetOut = clamp01(0.75 + Math.sin(t * 3.6) * 0.22);\n\t\t\t} else {\n\t\t\t\tconst base = 0.38 + 0.07 * Math.sin(t * 0.7);\n\t\t\t\tconst wander = 0.05 * Math.sin(t * 2.1) * Math.sin(t * 0.37 + 1.2);\n\t\t\t\ttargetIn = clamp01(base + wander);\n\t\t\t\ttargetOut = clamp01(0.48 + 0.12 * Math.sin(t * 1.05 + 0.6));\n\t\t\t}\n\t\t}\n\n\t\tcurIn += (targetIn - curIn) * 0.2;\n\t\tcurOut += (targetOut - curOut) * 0.2;\n\n\t\tconst targetSpeed = 0.1 + (1 - Math.pow(curOut - 1, 2)) * 0.9;\n\t\tanimSpeed += (targetSpeed - animSpeed) * 0.12;\n\n\t\tu.uAnimation.value += delta * animSpeed;\n\t\tu.uInputVolume.value = curIn;\n\t\tu.uOutputVolume.value = curOut;\n\t\tu.uColor1.value.lerp(targetColor1, 0.08);\n\t\tu.uColor2.value.lerp(targetColor2, 0.08);\n\t});\n\n\tfunction splitmix32(a: number) {\n\t\treturn function () {\n\t\t\ta |= 0;\n\t\t\ta = (a + 0x9e3779b9) | 0;\n\t\t\tlet t = a ^ (a >>> 16);\n\t\t\tt = Math.imul(t, 0x21f0aaad);\n\t\t\tt = t ^ (t >>> 15);\n\t\t\tt = Math.imul(t, 0x735a2d97);\n\t\t\treturn ((t = t ^ (t >>> 15)) >>> 0) / 4294967296;\n\t\t};\n\t}\n\n\tfunction makeOffsets(s: number): Float32Array {\n\t\tconst rng = splitmix32(s);\n\t\treturn new Float32Array(Array.from({ length: 7 }, () => rng() * Math.PI * 2));\n\t}\n\n\tfunction clamp01(n: number) {\n\t\tif (!Number.isFinite(n)) return 0;\n\t\treturn Math.min(1, Math.max(0, n));\n\t}\n</script>\n\n{#await $texture then perlinTexture}\n\t{#if perlinTexture}\n\t\t<T.Mesh bind:ref={mesh}>\n\t\t\t<T.CircleGeometry args={[3.5, 64]} />\n\t\t\t<T.ShaderMaterial\n\t\t\t\tuniforms={makeUniforms(perlinTexture)}\n\t\t\t\t{fragmentShader}\n\t\t\t\t{vertexShader}\n\t\t\t\ttransparent\n\t\t\t/>\n\t\t</T.Mesh>\n\t{/if}\n{/await}\n",
			"type": "registry:ui",
			"target": "orb/orb-scene.svelte"
		},
		{
			"content": "export type OrbAgentState = null | \"thinking\" | \"listening\" | \"talking\";\n",
			"type": "registry:ui",
			"target": "orb/types.ts"
		},
		{
			"content": "uniform 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",
			"type": "registry:file",
			"target": "orb/shaders/orb.frag.glsl"
		},
		{
			"content": "uniform 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",
			"type": "registry:file",
			"target": "orb/shaders/orb.vert.glsl"
		},
		{
			"content": "declare module \"*.glsl?raw\" {\n\tconst source: string;\n\texport default source;\n}\n",
			"type": "registry:file",
			"target": "orb/shaders/glsl.d.ts"
		},
		{
			"content": "import Root from \"./orb.svelte\";\n\nexport { Root, Root as Orb };\nexport type { OrbProps } from \"./orb.svelte\";\nexport type { OrbAgentState } from \"./types.js\";\n",
			"type": "registry:ui",
			"target": "orb/index.ts"
		}
	]
}