skecher-ui
uiai-orbsource: ok · hit
AI Orb
An interactive Three.js orb for AI assistants, voice interfaces, and ambient status surfaces.
npx shadcn add "https://skecher-ui.com/r/ai-orb.json"free
Where it goes
stats
Visual style
3d
Interaction
animated-counter
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · heavythreewebglthree
Files1 · source cached 10/8/2026, 2:57:41 PM
components/ui-components/ai-orb.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
export type AiOrbProps = {
/** Accessible description for the rendered plasma visualization. */
ariaLabel?: string;
/** Surface color behind the plasma strands. Accepts any valid CSS color. */
backgroundColor?: string;
className?: string;
/** Controls the brightness of the plasma field. */
intensity?: number;
/** Enables subtle pointer-following offset and rotation on fine pointers. */
interactive?: boolean;
/** Multiplies the pointer-following depth effect. */
pointerStrength?: number;
/** Primary plasma strand color. Accepts any valid Three.js color string. */
primaryColor?: string;
/** Secondary plasma strand color. Accepts any valid Three.js color string. */
secondaryColor?: string;
/** Internal animation speed. Set to 0 for a still plasma field. */
speed?: number;
/** Thickness of the ray-marched plasma strands. */
strandWidth?: number;
/** Number of strand turns packed into the orb. */
twist?: number;
};
type ThreeModule = typeof import("three");
type PlasmaUniforms = {
uBend1: { value: number };
uBend2: { value: number };
uColor1: { value: import("three").Color };
uColor2: { value: import("three").Color };
uDir2: { value: number };
uFocalLength: { value: number };
uIntensity: { value: number };
uLineThickness: { value: number };
uOffset: { value: import("three").Vector2 };
uResolution: { value: import("three").Vector2 };
uRotation: { value: number };
uSpeed1: { value: number };
uSpeed2: { value: number };
uTime: { value: number };
uTwist: { value: number };
};
type OrbRuntime = {
render: () => void;
renderer: import("three").WebGLRenderer;
uniforms: PlasmaUniforms;
};
type OrbSettings = Required<
Pick<
AiOrbProps,
| "intensity"
| "interactive"
| "pointerStrength"
| "primaryColor"
| "secondaryColor"
| "speed"
| "strandWidth"
| "twist"
>
>;
const DEFAULT_BACKGROUND_COLOR = "#090a0f";
const DEFAULT_SETTINGS: OrbSettings = {
intensity: 1.25,
interactive: true,
pointerStrength: 1,
primaryColor: "#A855F7",
secondaryColor: "#06B6D4",
speed: 0.45,
strandWidth: 0.5,
twist: 5,
};
const VERTEX_SHADER = `
void main() {
gl_Position = vec4(position.xy, 0.0, 1.0);
}
`;
// The plasma is raymarched as a double-helix confined inside an analytic
// sphere (rather than an unbounded tunnel), so the strands curl around a
// contained "ball" of light instead of streaking off in one direction.
const FRAGMENT_SHADER = `
precision highp float;
uniform float uTime;
uniform vec2 uResolution;
uniform vec2 uOffset;
uniform float uRotation;
uniform float uFocalLength;
uniform float uSpeed1;
uniform float uSpeed2;
uniform float uDir2;
uniform float uBend1;
uniform float uBend2;
uniform float uIntensity;
uniform float uLineThickness;
uniform float uTwist;
uniform vec3 uColor1;
uniform vec3 uColor2;
const float sphereRadius = 1.2;
const float pi = 9.14159;
const float pi2 = 6.28318;
const float halfPi = 1.5708;
#define MAX_STEPS 22
void renderPlasma(out vec4 outputColor, in vec2 coordinate) {
float time = uTime * pi;
vec3 rayOrigin = vec3(0.0, 0.0, -3.4);
vec3 rayDirection = normalize(
vec3((coordinate - 0.5 * uResolution) / uResolution.y, uFocalLength)
);
// Analytic ray/sphere intersection. Anything outside the sphere is
// simply not part of the volume we march through.
float b = dot(rayOrigin, rayDirection);
float c = dot(rayOrigin, rayOrigin) - sphereRadius * sphereRadius;
float h = b * b - c;
if (h < 0.0) discard;
h = sqrt(h);
float tNear = max(-b - h, 0.0);
float tFar = -b + h;
if (tFar < 0.0) discard;
float depth = tNear;
float stepDistance = 1.0;
vec2 fieldDistance = vec2(1.0);
vec3 position = rayOrigin;
float waveTime1 = time * 0.9;
float waveTime2 = time * 0.9;
float strandTime1 = time * uSpeed1;
float strandTime2 = time * uSpeed2 * uDir2;
for (int stepIndex = 0; stepIndex < MAX_STEPS; ++stepIndex) {
position = rayOrigin + rayDirection * depth;
// Phase driving the twist of the two strands as they wind through
// the ball; multiplying by twistFrequency packs several loops into
// the sphere's diameter instead of one long straight run.
float twistX = position.x * uTwist;
float wobble1 = uBend1 + sin(waveTime1 + twistX * 0.8) * 0.1;
float wobble2 = uBend2 + cos(waveTime2 + twistX * 1.1) * 0.1;
vec2 sineOffset = sin(vec2(twistX, twistX + halfPi) + strandTime1) * wobble1;
vec2 cosineOffset = cos(vec2(twistX, twistX + halfPi) + strandTime2) * wobble2;
vec2 yz = position.yz;
fieldDistance.x = length(yz - sineOffset) - uLineThickness;
fieldDistance.y = length(yz - cosineOffset) - uLineThickness;
float currentDistance = min(fieldDistance.x, fieldDistance.y);
stepDistance = min(stepDistance, currentDistance);
if (stepDistance < 0.001 || depth > tFar) break;
depth += max(stepDistance, 0.02) * 0.7;
}
float rootDepth = sqrt(max(depth, 0.0));
vec3 raw = max(
cos(depth * pi2) - stepDistance * rootDepth - vec3(fieldDistance, 0.0),
0.0
);
raw.gb += 0.1;
float maximumChannel = max(raw.r, max(raw.g, raw.b));
if (maximumChannel < 0.15) discard;
raw = raw * 0.4 + raw.brg * 0.6 + raw * raw;
float luminance = dot(raw, vec3(0.299, 0.587, 0.114));
float strandWeight1 = max(0.0, 1.0 - fieldDistance.x * 2.0);
float strandWeight2 = max(0.0, 1.0 - fieldDistance.y * 2.0);
float totalWeight = strandWeight1 + strandWeight2 + 0.001;
vec3 plasmaColor =
(uColor1 * strandWeight1 + uColor2 * strandWeight2) /
totalWeight * luminance * 3.5 * uIntensity;
// Soft falloff as we approach the sphere's surface so the plasma
// dissolves into the container's glow instead of ending with a hard rim.
float edgeFade = 1.0 - smoothstep(sphereRadius * 0.72, sphereRadius, length(position));
plasmaColor *= edgeFade;
outputColor = vec4(plasmaColor, edgeFade);
}
void main() {
vec2 coordinate = gl_FragCoord.xy + uOffset;
coordinate -= 0.5 * uResolution;
float cosine = cos(uRotation);
float sine = sin(uRotation);
coordinate = mat2(cosine, -sine, sine, cosine) * coordinate;
coordinate += 0.5 * uResolution;
vec4 color;
renderPlasma(color, coordinate);
gl_FragColor = color;
}
`;
export function AiOrb({
ariaLabel = "Animated AI plasma field",
backgroundColor = DEFAULT_BACKGROUND_COLOR,
className,
intensity = DEFAULT_SETTINGS.intensity,
interactive = DEFAULT_SETTINGS.interactive,
pointerStrength = DEFAULT_SETTINGS.pointerStrength,
primaryColor = DEFAULT_SETTINGS.primaryColor,
secondaryColor = DEFAULT_SETTINGS.secondaryColor,
speed = DEFAULT_SETTINGS.speed,
strandWidth = DEFAULT_SETTINGS.strandWidth,
twist = DEFAULT_SETTINGS.twist,
}: AiOrbProps) {
const containerRef = React.useRef<HTMLDivElement>(null);
const runtimeRef = React.useRef<OrbRuntime | null>(null);
const settingsRef = React.useRef<OrbSettings>(DEFAULT_SETTINGS);
const [renderState, setRenderState] = React.useState<"loading" | "ready" | "fallback">("loading");
settingsRef.current = {
intensity: Math.max(0.25, Math.min(5.5, intensity)),
interactive,
pointerStrength: Math.max(0, Math.min(2, pointerStrength)),
primaryColor,
secondaryColor,
speed: Math.max(0, Math.min(2, speed)),
strandWidth: Math.max(0.2, Math.min(1, strandWidth)),
twist: Math.max(1, Math.min(10, twist)),
};
React.useEffect(() => {
const runtime = runtimeRef.current;
if (!runtime) return;
runtime.uniforms.uIntensity.value = Math.max(0.25, Math.min(2.5, intensity));
runtime.uniforms.uColor1.value.set(primaryColor);
runtime.uniforms.uColor2.value.set(secondaryColor);
runtime.uniforms.uLineThickness.value = Math.max(0.2, Math.min(1, strandWidth));
runtime.uniforms.uTwist.value = Math.max(1, Math.min(10, twist));
runtime.render();
}, [intensity, primaryColor, secondaryColor, strandWidth, twist]);
React.useEffect(() => {
const container = containerRef.current;
if (!container) return;
let cancelled = false;
let frameId = 0;
let resizeObserver: ResizeObserver | null = null;
let intersectionObserver: IntersectionObserver | null = null;
let removePointerListeners = () => {};
let disposeRuntime = () => {};
const setup = async () => {
try {
const THREE: ThreeModule = await import("three");
if (cancelled) return;
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const renderer = new THREE.WebGLRenderer({
alpha: true,
antialias: false,
depth: false,
powerPreference: "high-performance",
premultipliedAlpha: false,
stencil: false,
});
renderer.setClearColor(0x000000, 0);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.NoToneMapping;
renderer.domElement.setAttribute("aria-hidden", "true");
renderer.domElement.className = "absolute inset-0 size-full";
container.appendChild(renderer.domElement);
const settings = settingsRef.current;
const uniforms: PlasmaUniforms = {
uBend1: { value: 1 },
uBend2: { value: 0.5 },
uColor1: { value: new THREE.Color(settings.primaryColor) },
uColor2: { value: new THREE.Color(settings.secondaryColor) },
uDir2: { value: 1 },
uFocalLength: { value: 1.5 },
uIntensity: { value: settings.intensity },
uLineThickness: { value: settings.strandWidth },
uOffset: { value: new THREE.Vector2() },
uResolution: { value: new THREE.Vector2(1, 1) },
uRotation: { value: 0 },
uSpeed1: { value: 0.05 },
uSpeed2: { value: 0.05 },
uTime: { value: 0 },
uTwist: { value: settings.twist },
};
const geometry = new THREE.BufferGeometry();
geometry.setAttribute(
"position",
new THREE.BufferAttribute(new Float32Array([-1, -1, 0, 3, -1, 0, -1, 3, 0]), 3),
);
const material = new THREE.ShaderMaterial({
depthTest: false,
depthWrite: false,
fragmentShader: FRAGMENT_SHADER,
transparent: true,
uniforms,
vertexShader: VERTEX_SHADER,
});
const plasma = new THREE.Mesh(geometry, material);
plasma.frustumCulled = false;
scene.add(plasma);
const pointerTarget = new THREE.Vector2();
const pointerCurrent = new THREE.Vector2();
let isVisible = true;
let lastTime = performance.now();
const render = () => renderer.render(scene, camera);
const resize = () => {
const { width, height } = container.getBoundingClientRect();
const safeWidth = Math.max(1, Math.round(width));
const safeHeight = Math.max(1, Math.round(height));
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
renderer.setSize(safeWidth, safeHeight, false);
renderer.getDrawingBufferSize(uniforms.uResolution.value);
render();
};
const finePointer = window.matchMedia("(hover: hover) and (pointer: fine)");
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
const applyPointerDepth = () => {
const resolution = uniforms.uResolution.value;
const strength = settingsRef.current.pointerStrength;
uniforms.uOffset.value.set(
pointerCurrent.x * resolution.x * 0.075 * strength,
-pointerCurrent.y * resolution.y * 0.075 * strength,
);
uniforms.uRotation.value = pointerCurrent.x * 0.1 * strength;
};
const resetPointerDepth = () => {
pointerTarget.set(0, 0);
pointerCurrent.set(0, 0);
applyPointerDepth();
render();
};
const handlePointerMove = (event: PointerEvent) => {
if (!settingsRef.current.interactive || !finePointer.matches || reducedMotion.matches) {
return;
}
const bounds = container.getBoundingClientRect();
pointerTarget.set(
((event.clientX - bounds.left) / Math.max(bounds.width, 1) - 0.5) * 2,
((event.clientY - bounds.top) / Math.max(bounds.height, 1) - 0.5) * 2,
);
};
const handlePointerLeave = () => pointerTarget.set(0, 0);
reducedMotion.addEventListener("change", resetPointerDepth);
container.addEventListener("pointermove", handlePointerMove, { passive: true });
container.addEventListener("pointerleave", handlePointerLeave);
removePointerListeners = () => {
reducedMotion.removeEventListener("change", resetPointerDepth);
container.removeEventListener("pointermove", handlePointerMove);
container.removeEventListener("pointerleave", handlePointerLeave);
};
resizeObserver = new ResizeObserver(resize);
resizeObserver.observe(container);
intersectionObserver = new IntersectionObserver(([entry]) => {
isVisible = entry?.isIntersecting ?? true;
});
intersectionObserver.observe(container);
const animate = (now: number) => {
if (cancelled) return;
const delta = Math.min((now - lastTime) / 1000, 0.05);
lastTime = now;
const currentSettings = settingsRef.current;
if (!currentSettings.interactive) pointerTarget.set(0, 0);
const shouldAnimate =
isVisible &&
!document.hidden &&
!reducedMotion.matches &&
(currentSettings.speed > 0 || currentSettings.interactive);
if (shouldAnimate) {
uniforms.uTime.value += delta * currentSettings.speed;
pointerCurrent.lerp(pointerTarget, 1 - Math.exp(-delta * 7.5));
applyPointerDepth();
render();
}
frameId = window.requestAnimationFrame(animate);
};
runtimeRef.current = { render, renderer, uniforms };
resize();
render();
setRenderState("ready");
frameId = window.requestAnimationFrame(animate);
disposeRuntime = () => {
window.cancelAnimationFrame(frameId);
runtimeRef.current = null;
geometry.dispose();
material.dispose();
renderer.dispose();
renderer.domElement.remove();
};
} catch {
if (!cancelled) setRenderState("fallback");
}
};
void setup();
return () => {
cancelled = true;
window.cancelAnimationFrame(frameId);
resizeObserver?.disconnect();
intersectionObserver?.disconnect();
removePointerListeners();
disposeRuntime();
};
}, []);
return (
<div
aria-label={ariaLabel}
className={cn(
"relative isolate aspect-square w-[min(82vw,20rem)] overflow-hidden rounded-full shadow-[inset_0_1px_0_rgb(255_255_255/0.06),inset_0_-24px_48px_rgb(0_0_0/0.3)] [contain:layout_paint]",
className,
)}
data-render-state={renderState}
ref={containerRef}
role="img"
style={{
background: `radial-gradient(circle at 50% 42%, color-mix(in srgb, ${backgroundColor} 78%, white) 0%, ${backgroundColor} 58%, color-mix(in srgb, ${backgroundColor} 72%, black) 100%)`,
}}
>
<div
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-[12%] rounded-full blur-3xl transition-opacity duration-200 ease-[cubic-bezier(0.23,1,0.32,1)]",
renderState === "ready" ? "opacity-0" : "opacity-70",
)}
style={{
background: `radial-gradient(circle, ${primaryColor}, ${secondaryColor} 52%, transparent 72%)`,
}}
/>
</div>
);
}