{
  "slug": "ai-orb",
  "name": "AI Orb",
  "description": "Luminescent AI generation sphere with native WebGL Bayer 8x8 dithered plasma and animated letter wave typography.",
  "summary": "A generative AI orb that combines chromatic sphere glows, rotating inset shadows and a WebGL plasma core with an ordered-dither overlay. Features pointer-driven warping, six color variants, and a staggered letter-wave label for generation states, loaders and assistant interfaces.",
  "category": "ai-stuff",
  "tags": [
    "ai",
    "orb",
    "dither",
    "webgl",
    "shader",
    "loader",
    "bayer",
    "motion",
    "plasma"
  ],
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "motion"
  ],
  "registryDependencies": [
    "dither"
  ],
  "version": "1.0.0",
  "createdDate": "2026-09-22",
  "updatedDate": "2026-09-22",
  "interactive": true,
  "supportsColor": true,
  "highlights": [
    "Native WebGL 8x8 Bayer matrix dithered procedural plasma shader with zero three.js overhead",
    "Interactive pointer displacement warping fluid noise with smooth spherical vignette falloff",
    "Volumetric 3D chromatic inset box shadows with continuous harmonic counter-rotations",
    "Sinusoidal staggered letter wave typography with customizable labels ('Generating', 'Thinking', etc.)",
    "Quantized color stepping (colorNum), pixelSize control, and six color variants (chromatic, neon, cyberpunk, emerald, amber, monochrome)"
  ],
  "anatomy": [
    "<div> (Outer Halo Container: relative circular wrapper with ambient radial glow)",
    "<canvas> (WebGL Canvas: plasma noise shader with Bayer 8x8 dithering)",
    "<Dither> (Animated dither gradient layered over the core)",
    "<motion.div> (Primary Chromatic Shell: clockwise rotating inset shadow layer)",
    "<motion.div> (Secondary Rim Ring: counter-clockwise rotating highlight border)",
    "<div> (Specular Highlight: radial gradient reflection on upper hemisphere)",
    "<motion.span[]> (Letter Wave: staggered animated label text)"
  ],
  "physics": {
    "engine": "Bayer 8x8 Dither Shader + Harmonic CSS Box-Shadow Rotation",
    "description": "Perlin fbm noise passed through an 8x8 Bayer ordered dithering kernel, overlaid with counter-rotating chromatic inset shadows. Rotation and wave durations are divided by the speed prop.",
    "parameters": [
      {
        "label": "Dither Matrix",
        "value": "8x8 Bayer Ordered Kernel"
      },
      {
        "label": "Noise Function",
        "value": "4-Octave Fractional Brownian Motion (fbm)"
      },
      {
        "label": "Primary Rotation Speed",
        "value": "3.5s / speed, linear infinite (4.375s at the default speed 0.8)"
      },
      {
        "label": "Secondary Rotation Speed",
        "value": "7s / speed, reverse linear infinite (8.75s at default speed)"
      },
      {
        "label": "Letter Wave Frequency",
        "value": "2.2s / speed, ease-in-out with 100ms stagger (2.75s at default speed)"
      }
    ]
  },
  "accessibility": {
    "aria": "No ARIA attributes are set. The letter-wave label is plain visible text; wrap the orb in an element with role=\"status\" if generation updates should be announced.",
    "reducedMotion": "With prefers-reduced-motion, the ring rotation and the letter wave stop and the label renders as static text."
  },
  "guidelines": {
    "recommended": [
      "Primary AI generation indicator during LLM inference or agentic orchestration",
      "Centered focal point in conversational AI sidebars, chat modals, or synthesis screens",
      "Interactive retro-cyberpunk hero element in developer tools and creative platforms"
    ],
    "bestPractices": [
      "Provide a concise descriptive text prop like 'Thinking' or 'Synthesizing' for clear user feedback",
      "Use matching color props to align the orb with your application's primary brand accents",
      "Ensure parent container provides enough clearance for the outer 15% radial glow aura"
    ]
  },
  "props": [
    {
      "name": "size",
      "type": "number",
      "defaultValue": "190",
      "description": "Width and height of the orb in pixels."
    },
    {
      "name": "text",
      "type": "string",
      "defaultValue": "'Generating'",
      "description": "Text displayed in the center with staggered letter wave animation."
    },
    {
      "name": "color",
      "type": "string",
      "defaultValue": "undefined",
      "description": "Primary accent color for the plasma waves, outer aura, and inner chromatic shadows."
    },
    {
      "name": "variant",
      "type": "'chromatic' | 'neon' | 'cyberpunk' | 'emerald' | 'amber' | 'monochrome'",
      "defaultValue": "'chromatic'",
      "description": "Color palette theme for the dual-tone plasma and volumetric shadows."
    },
    {
      "name": "dither",
      "type": "boolean",
      "defaultValue": "true",
      "description": "Enables retro 8x8 Bayer ordered matrix dithering across the plasma."
    },
    {
      "name": "pixelSize",
      "type": "number",
      "defaultValue": "2",
      "description": "Resolution scale factor for the dither pixels (1-8)."
    },
    {
      "name": "colorNum",
      "type": "number",
      "defaultValue": "5",
      "description": "Number of quantized color bands rendered by the dither kernel."
    },
    {
      "name": "speed",
      "type": "number",
      "defaultValue": "0.8",
      "description": "Playback speed multiplier for plasma turbulence and shadow rotation."
    },
    {
      "name": "interactive",
      "type": "boolean",
      "defaultValue": "true",
      "description": "Enables pointer position tracking to distort plasma waves on hover."
    },
    {
      "name": "showText",
      "type": "boolean",
      "defaultValue": "true",
      "description": "Controls whether the centered letter wave typography is visible."
    },
    {
      "name": "className",
      "type": "string",
      "defaultValue": "undefined",
      "description": "Additional CSS classes applied to the root container."
    }
  ],
  "usage": "import { AiOrb } from \"@/components/ui/ai-orb\";\n\nexport function AiOrbDemo() {\n  return <AiOrb text=\"Thinking\" variant=\"chromatic\" size={190} />;\n}",
  "files": [
    {
      "name": "ai-orb.tsx",
      "path": "registry/ui/ai-orb.tsx",
      "code": "\"use client\";\n\nimport React, { useEffect, useMemo, useRef } from \"react\";\nimport { motion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\nimport { Dither } from \"./dither\";\n\nexport interface AiOrbProps {\n  className?: string;\n  color?: string;\n  colorNum?: number;\n  dither?: boolean;\n  interactive?: boolean;\n  pixelSize?: number;\n  showText?: boolean;\n  size?: number;\n  speed?: number;\n  text?: string;\n  variant?:\n    | \"chromatic\"\n    | \"neon\"\n    | \"cyberpunk\"\n    | \"emerald\"\n    | \"amber\"\n    | \"monochrome\";\n}\n\nconst VERTEX_SHADER = `\nattribute vec2 position;\nvarying vec2 vUv;\nvoid main() {\n  vUv = (position + 1.0) * 0.5;\n  gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst FRAGMENT_SHADER = `\nprecision highp float;\nuniform vec2 u_resolution;\nuniform float u_time;\nuniform vec2 u_mouse;\nuniform vec3 u_color;\nuniform vec3 u_bg_color;\nuniform float u_pixel_size;\nuniform float u_color_num;\nuniform float u_speed;\nuniform float u_dither_active;\n\nconst float bayerMatrix8x8[64] = float[64](\n  0.0/64.0, 48.0/64.0, 12.0/64.0, 60.0/64.0,  3.0/64.0, 51.0/64.0, 15.0/64.0, 63.0/64.0,\n  32.0/64.0,16.0/64.0, 44.0/64.0, 28.0/64.0, 35.0/64.0,19.0/64.0, 47.0/64.0, 31.0/64.0,\n  8.0/64.0, 56.0/64.0,  4.0/64.0, 52.0/64.0, 11.0/64.0,59.0/64.0,  7.0/64.0, 55.0/64.0,\n  40.0/64.0,24.0/64.0, 36.0/64.0, 20.0/64.0, 43.0/64.0,27.0/64.0, 39.0/64.0, 23.0/64.0,\n  2.0/64.0, 50.0/64.0, 14.0/64.0, 62.0/64.0,  1.0/64.0,49.0/64.0, 13.0/64.0, 61.0/64.0,\n  34.0/64.0,18.0/64.0, 46.0/64.0, 30.0/64.0, 33.0/64.0,17.0/64.0, 45.0/64.0, 29.0/64.0,\n  10.0/64.0,58.0/64.0,  6.0/64.0, 54.0/64.0,  9.0/64.0,57.0/64.0,  5.0/64.0, 53.0/64.0,\n  42.0/64.0,26.0/64.0, 38.0/64.0, 22.0/64.0, 41.0/64.0,25.0/64.0, 37.0/64.0, 21.0/64.0\n);\n\nvec4 mod289(vec4 x) { return x - floor(x * (1.0/289.0)) * 289.0; }\nvec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }\nvec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }\nvec2 fade(vec2 t) { return t*t*t*(t*(t*6.0-15.0)+10.0); }\n\nfloat cnoise(vec2 P) {\n  vec4 Pi = floor(P.xyxy) + vec4(0.0,0.0,1.0,1.0);\n  vec4 Pf = fract(P.xyxy) - vec4(0.0,0.0,1.0,1.0);\n  Pi = mod289(Pi);\n  vec4 ix = Pi.xzxz;\n  vec4 iy = Pi.yyww;\n  vec4 fx = Pf.xzxz;\n  vec4 fy = Pf.yyww;\n  vec4 i = permute(permute(ix) + iy);\n  vec4 gx = fract(i * (1.0/41.0)) * 2.0 - 1.0;\n  vec4 gy = abs(gx) - 0.5;\n  vec4 tx = floor(gx + 0.5);\n  gx = gx - tx;\n  vec2 g00 = vec2(gx.x, gy.x);\n  vec2 g10 = vec2(gx.y, gy.y);\n  vec2 g01 = vec2(gx.z, gy.z);\n  vec2 g11 = vec2(gx.w, gy.w);\n  vec4 norm = taylorInvSqrt(vec4(dot(g00,g00), dot(g01,g01), dot(g10,g10), dot(g11,g11)));\n  g00 *= norm.x; g01 *= norm.y; g10 *= norm.z; g11 *= norm.w;\n  float n00 = dot(g00, vec2(fx.x, fy.x));\n  float n10 = dot(g10, vec2(fx.y, fy.y));\n  float n01 = dot(g01, vec2(fx.z, fy.z));\n  float n11 = dot(g11, vec2(fx.w, fy.w));\n  vec2 fade_xy = fade(Pf.xy);\n  vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);\n  return 2.3 * mix(n_x.x, n_x.y, fade_xy.y);\n}\n\nfloat fbm(vec2 p) {\n  float v = 0.0;\n  float a = 0.5;\n  mat2 rot = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5));\n  for (int i = 0; i < 4; i++) {\n    v += a * abs(cnoise(p));\n    p = rot * p * 2.0 + vec2(100.0);\n    a *= 0.5;\n  }\n  return v;\n}\n\nvec3 dither(vec2 coord, vec3 color) {\n  if (u_dither_active < 0.5) return color;\n  vec2 scaledCoord = floor(coord / u_pixel_size);\n  int x = int(mod(scaledCoord.x, 8.0));\n  int y = int(mod(scaledCoord.y, 8.0));\n  int index = y * 8 + x;\n  float threshold = 0.0;\n  for (int i = 0; i < 64; i++) {\n    if (i == index) {\n      threshold = bayerMatrix8x8[i] - 0.25;\n      break;\n    }\n  }\n  float stepVal = 1.0 / max(1.0, u_color_num - 1.0);\n  color += threshold * stepVal;\n  float luminance = dot(color, vec3(0.2126, 0.7152, 0.0722));\n  float bias = mix(0.15, 0.0, smoothstep(0.4, 0.8, luminance));\n  color = clamp(color - bias, 0.0, 1.0);\n  return floor(color * (u_color_num - 1.0) + 0.5) / (u_color_num - 1.0);\n}\n\nvoid main() {\n  vec2 uv = gl_FragCoord.xy / u_resolution.xy;\n  vec2 centered = uv - 0.5;\n  centered.x *= u_resolution.x / u_resolution.y;\n\n  float len = length(centered);\n  if (len > 0.5) {\n    gl_FragColor = vec4(0.0);\n    return;\n  }\n\n  float t = u_time * u_speed;\n  vec2 p = centered * 3.4;\n\n  vec2 m = (u_mouse / u_resolution - 0.5) * vec2(1.0, -1.0);\n  m.x *= u_resolution.x / u_resolution.y;\n  float mDist = length(centered - m);\n  float mEffect = 1.0 - smoothstep(0.0, 0.45, mDist);\n\n  float n1 = fbm(p + vec2(t * 0.35, -t * 0.25));\n  float n2 = fbm(p - vec2(t * 0.15, t * 0.2) + n1);\n  float n = n2 - mEffect * 0.35;\n\n  float sphericalVignette = smoothstep(0.5, 0.2, len);\n  vec3 col = mix(u_bg_color, u_color, clamp(n, 0.0, 1.0));\n  col = mix(col, vec3(1.0), pow(clamp(n * 1.25, 0.0, 1.0), 3.0) * 0.65);\n\n  col = dither(gl_FragCoord.xy, col);\n  float alpha = sphericalVignette * 0.92;\n\n  gl_FragColor = vec4(col, alpha);\n}\n`;\n\nconst REDUCED_MOTION_QUERY = \"(prefers-reduced-motion: reduce)\";\n\nfunction subscribeReducedMotion(onChange: () => void) {\n  const media = window.matchMedia(REDUCED_MOTION_QUERY);\n  media.addEventListener(\"change\", onChange);\n  return () => media.removeEventListener(\"change\", onChange);\n}\n\nfunction getReducedMotion() {\n  return window.matchMedia(REDUCED_MOTION_QUERY).matches;\n}\n\nfunction hexToRgb(hex: string): [number, number, number] {\n  let clean = hex.replace(\"#\", \"\");\n  if (clean.length === 3) {\n    clean = clean\n      .split(\"\")\n      .map((c) => c + c)\n      .join(\"\");\n  }\n  const intVal = parseInt(clean, 16);\n  if (isNaN(intVal)) return [0.68, 0.37, 1.0];\n  const r = ((intVal >> 16) & 255) / 255;\n  const g = ((intVal >> 8) & 255) / 255;\n  const b = (intVal & 255) / 255;\n  return [r, g, b];\n}\n\nconst VARIANT_PRESETS: Record<\n  NonNullable<AiOrbProps[\"variant\"]>,\n  { primary: string; secondary: string; shadowA: string; shadowB: string }\n> = {\n  chromatic: {\n    primary: \"#ad5fff\",\n    secondary: \"#471eec\",\n    shadowA: \"#ad5fff\",\n    shadowB: \"#471eec\",\n  },\n  neon: {\n    primary: \"#3B82F6\",\n    secondary: \"#10B981\",\n    shadowA: \"#3B82F6\",\n    shadowB: \"#10B981\",\n  },\n  cyberpunk: {\n    primary: \"#F43F5E\",\n    secondary: \"#06B6D4\",\n    shadowA: \"#F43F5E\",\n    shadowB: \"#06B6D4\",\n  },\n  emerald: {\n    primary: \"#10B981\",\n    secondary: \"#047857\",\n    shadowA: \"#34D399\",\n    shadowB: \"#064E3B\",\n  },\n  amber: {\n    primary: \"#F59E0B\",\n    secondary: \"#B45309\",\n    shadowA: \"#FBBF24\",\n    shadowB: \"#78350F\",\n  },\n  monochrome: {\n    primary: \"#FFFFFF\",\n    secondary: \"#52525B\",\n    shadowA: \"#E4E4E7\",\n    shadowB: \"#27272A\",\n  },\n};\n\nexport function AiOrb({\n  className,\n  color,\n  colorNum = 5,\n  dither = true,\n  interactive = true,\n  pixelSize = 2,\n  showText = true,\n  size = 190,\n  speed = 0.8,\n  text = \"Generating\",\n  variant = \"chromatic\",\n}: AiOrbProps) {\n  const canvasHostRef = useRef<HTMLDivElement>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const mousePos = useRef({ x: -1000, y: -1000 });\n\n  const preset = VARIANT_PRESETS[variant] ?? VARIANT_PRESETS.chromatic;\n  const primaryColor = color ?? preset.primary;\n  const secondaryColor = preset.secondary;\n\n  const rgbPrimary = useMemo(() => hexToRgb(primaryColor), [primaryColor]);\n  const rgbSecondary = useMemo(\n    () => hexToRgb(secondaryColor),\n    [secondaryColor],\n  );\n\n  const letters = useMemo(() => text.split(\"\"), [text]);\n  const reduceMotion = React.useSyncExternalStore(\n    subscribeReducedMotion,\n    getReducedMotion,\n    () => false,\n  );\n\n  useEffect(() => {\n    const host = canvasHostRef.current;\n    if (!host) return;\n\n    const canvas = document.createElement(\"canvas\");\n    canvas.className = \"relative block h-full w-full\";\n    host.appendChild(canvas);\n\n    let gl: WebGLRenderingContext | null = null;\n    const releaseCanvas = () => {\n      gl?.getExtension(\"WEBGL_lose_context\")?.loseContext();\n      canvas.remove();\n    };\n    try {\n      gl = canvas.getContext(\"webgl\", {\n        alpha: true,\n        antialias: false,\n        depth: false,\n        preserveDrawingBuffer: false,\n      });\n    } catch {\n      return releaseCanvas;\n    }\n\n    if (!gl) {\n      return releaseCanvas;\n    }\n\n    const compileShader = (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        gl!.deleteShader(shader);\n        return null;\n      }\n      return shader;\n    };\n\n    const vs = compileShader(gl.VERTEX_SHADER, VERTEX_SHADER);\n    const fs = compileShader(gl.FRAGMENT_SHADER, FRAGMENT_SHADER);\n\n    if (!vs || !fs) {\n      return releaseCanvas;\n    }\n\n    const program = gl.createProgram();\n    if (!program) return releaseCanvas;\n    gl.attachShader(program, vs);\n    gl.attachShader(program, fs);\n    gl.linkProgram(program);\n\n    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n      gl.deleteProgram(program);\n      return releaseCanvas;\n    }\n\n    gl.useProgram(program);\n\n    const quadBuffer = gl.createBuffer();\n    gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);\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\n    const positionLoc = gl.getAttribLocation(program, \"position\");\n    gl.enableVertexAttribArray(positionLoc);\n    gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);\n\n    const uResolution = gl.getUniformLocation(program, \"u_resolution\");\n    const uTime = gl.getUniformLocation(program, \"u_time\");\n    const uMouse = gl.getUniformLocation(program, \"u_mouse\");\n    const uColor = gl.getUniformLocation(program, \"u_color\");\n    const uBgColor = gl.getUniformLocation(program, \"u_bg_color\");\n    const uPixelSize = gl.getUniformLocation(program, \"u_pixel_size\");\n    const uColorNum = gl.getUniformLocation(program, \"u_color_num\");\n    const uSpeed = gl.getUniformLocation(program, \"u_speed\");\n    const uDitherActive = gl.getUniformLocation(program, \"u_dither_active\");\n\n    let animationFrameId = 0;\n    const startTime = performance.now();\n\n    const render = () => {\n      if (!gl || !canvas) return;\n\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      const displayWidth = Math.floor(canvas.clientWidth * dpr);\n      const displayHeight = Math.floor(canvas.clientHeight * dpr);\n\n      if (canvas.width !== displayWidth || canvas.height !== displayHeight) {\n        canvas.width = displayWidth;\n        canvas.height = displayHeight;\n        gl.viewport(0, 0, displayWidth, displayHeight);\n      }\n\n      gl.clearColor(0, 0, 0, 0);\n      gl.clear(gl.COLOR_BUFFER_BIT);\n\n      const elapsed = (performance.now() - startTime) * 0.001;\n\n      gl.uniform2f(uResolution, canvas.width, canvas.height);\n      gl.uniform1f(uTime, elapsed);\n      gl.uniform2f(uMouse, mousePos.current.x * dpr, mousePos.current.y * dpr);\n      gl.uniform3f(uColor, rgbPrimary[0], rgbPrimary[1], rgbPrimary[2]);\n      gl.uniform3f(uBgColor, rgbSecondary[0], rgbSecondary[1], rgbSecondary[2]);\n      gl.uniform1f(uPixelSize, pixelSize);\n      gl.uniform1f(uColorNum, colorNum);\n      gl.uniform1f(uSpeed, speed);\n      gl.uniform1f(uDitherActive, dither ? 1.0 : 0.0);\n\n      gl.drawArrays(gl.TRIANGLES, 0, 6);\n\n      if (!reduceMotion) {\n        animationFrameId = requestAnimationFrame(render);\n      }\n    };\n\n    animationFrameId = requestAnimationFrame(render);\n\n    const resizeObserver = reduceMotion ? new ResizeObserver(render) : null;\n    resizeObserver?.observe(host);\n\n    return () => {\n      cancelAnimationFrame(animationFrameId);\n      resizeObserver?.disconnect();\n      if (gl) {\n        gl.deleteBuffer(quadBuffer);\n        gl.deleteProgram(program);\n        gl.deleteShader(vs);\n        gl.deleteShader(fs);\n      }\n      releaseCanvas();\n    };\n  }, [\n    colorNum,\n    dither,\n    pixelSize,\n    reduceMotion,\n    rgbPrimary,\n    rgbSecondary,\n    speed,\n  ]);\n\n  const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (!interactive) return;\n    const rect = containerRef.current?.getBoundingClientRect();\n    if (!rect) return;\n    mousePos.current = {\n      x: e.clientX - rect.left,\n      y: e.clientY - rect.top,\n    };\n  };\n\n  const handlePointerLeave = () => {\n    mousePos.current = { x: -1000, y: -1000 };\n  };\n\n  return (\n    <div\n      ref={containerRef}\n      onPointerMove={handlePointerMove}\n      onPointerLeave={handlePointerLeave}\n      data-slot=\"ai-orb\"\n      className={cn(\n        \"relative flex items-center justify-center rounded-full select-none cursor-pointer overflow-visible transition-transform duration-300 hover:scale-[1.03]\",\n        className,\n      )}\n      style={{\n        width: size,\n        height: size,\n      }}\n    >\n      <div\n        className=\"absolute inset-[-15%] rounded-full blur-2xl opacity-40 pointer-events-none transition-colors duration-500 will-change-transform\"\n        style={{\n          background: `radial-gradient(circle, ${primaryColor} 0%, ${secondaryColor} 60%, transparent 75%)`,\n        }}\n      />\n\n      <div className=\"absolute inset-0 rounded-full overflow-hidden z-0\">\n        <div\n          className=\"absolute inset-0 rounded-full\"\n          style={{\n            background: `radial-gradient(circle at 35% 35%, ${primaryColor} 0%, ${secondaryColor} 55%, #050508 100%)`,\n          }}\n        />\n        <div ref={canvasHostRef} className=\"relative h-full w-full\" />\n      </div>\n\n      <motion.div\n        className=\"absolute inset-0 rounded-full pointer-events-none z-10\"\n        animate={{ rotate: reduceMotion ? 0 : 360 }}\n        transition={\n          reduceMotion\n            ? { duration: 0 }\n            : { repeat: Infinity, duration: 3.5 / speed, ease: \"linear\" }\n        }\n        style={{\n          boxShadow: `\n            0 10px 22px 0 rgba(255, 255, 255, 0.85) inset,\n            0 24px 34px 0 ${primaryColor} inset,\n            0 64px 64px 0 ${secondaryColor} inset,\n            0 0 20px 2px ${primaryColor}66\n          `,\n        }}\n      />\n\n      <motion.div\n        className=\"absolute inset-0.75 rounded-full pointer-events-none z-10 border border-white/20\"\n        animate={{ rotate: reduceMotion ? 0 : -360 }}\n        transition={\n          reduceMotion\n            ? { duration: 0 }\n            : { repeat: Infinity, duration: 7 / speed, ease: \"linear\" }\n        }\n        style={{\n          boxShadow: `\n            0 -8px 18px 0 rgba(255, 255, 255, 0.4) inset,\n            0 -16px 28px 0 ${secondaryColor} inset\n          `,\n        }}\n      />\n\n      <div className=\"absolute inset-0 rounded-full overflow-hidden pointer-events-none z-15 mix-blend-screen opacity-55\">\n        <Dither\n          color1={primaryColor}\n          color2={secondaryColor}\n          color3=\"#000000\"\n          grainAmount={0.16}\n          grainScale={2.0}\n          grainAnimated={true}\n          warpStrength={1.2}\n          timeSpeed={speed * 0.25}\n          zoom={1.1}\n        />\n      </div>\n\n      <div className=\"absolute inset-0 rounded-full pointer-events-none z-20 bg-[radial-gradient(circle_at_35%_30%,rgba(255,255,255,0.35)_0%,transparent_50%)]\" />\n\n      {showText && (\n        <div className=\"relative z-30 flex items-center justify-center gap-0.5 px-3 pointer-events-none select-none\">\n          {letters.map((char, index) => (\n            <motion.span\n              key={`${char}-${index}`}\n              initial={{ opacity: 0.35, y: 0, scale: 1 }}\n              animate={\n                reduceMotion\n                  ? { opacity: 0.85, scale: 1, y: 0 }\n                  : {\n                      opacity: [0.35, 1, 0.65, 0.35],\n                      scale: [1, 1.18, 1, 1],\n                      y: [0, -2.5, 0, 0],\n                    }\n              }\n              transition={\n                reduceMotion\n                  ? { duration: 0 }\n                  : {\n                      duration: 2.2 / speed,\n                      repeat: Infinity,\n                      ease: \"easeInOut\",\n                      delay: index * 0.1,\n                    }\n              }\n              className=\"inline-block font-sans font-light tracking-wide text-white text-sm drop-shadow-[0_0_8px_rgba(255,255,255,0.9)]\"\n            >\n              {char}\n            </motion.span>\n          ))}\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport default AiOrb;\n",
      "content": "\"use client\";\n\nimport React, { useEffect, useMemo, useRef } from \"react\";\nimport { motion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\nimport { Dither } from \"./dither\";\n\nexport interface AiOrbProps {\n  className?: string;\n  color?: string;\n  colorNum?: number;\n  dither?: boolean;\n  interactive?: boolean;\n  pixelSize?: number;\n  showText?: boolean;\n  size?: number;\n  speed?: number;\n  text?: string;\n  variant?:\n    | \"chromatic\"\n    | \"neon\"\n    | \"cyberpunk\"\n    | \"emerald\"\n    | \"amber\"\n    | \"monochrome\";\n}\n\nconst VERTEX_SHADER = `\nattribute vec2 position;\nvarying vec2 vUv;\nvoid main() {\n  vUv = (position + 1.0) * 0.5;\n  gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst FRAGMENT_SHADER = `\nprecision highp float;\nuniform vec2 u_resolution;\nuniform float u_time;\nuniform vec2 u_mouse;\nuniform vec3 u_color;\nuniform vec3 u_bg_color;\nuniform float u_pixel_size;\nuniform float u_color_num;\nuniform float u_speed;\nuniform float u_dither_active;\n\nconst float bayerMatrix8x8[64] = float[64](\n  0.0/64.0, 48.0/64.0, 12.0/64.0, 60.0/64.0,  3.0/64.0, 51.0/64.0, 15.0/64.0, 63.0/64.0,\n  32.0/64.0,16.0/64.0, 44.0/64.0, 28.0/64.0, 35.0/64.0,19.0/64.0, 47.0/64.0, 31.0/64.0,\n  8.0/64.0, 56.0/64.0,  4.0/64.0, 52.0/64.0, 11.0/64.0,59.0/64.0,  7.0/64.0, 55.0/64.0,\n  40.0/64.0,24.0/64.0, 36.0/64.0, 20.0/64.0, 43.0/64.0,27.0/64.0, 39.0/64.0, 23.0/64.0,\n  2.0/64.0, 50.0/64.0, 14.0/64.0, 62.0/64.0,  1.0/64.0,49.0/64.0, 13.0/64.0, 61.0/64.0,\n  34.0/64.0,18.0/64.0, 46.0/64.0, 30.0/64.0, 33.0/64.0,17.0/64.0, 45.0/64.0, 29.0/64.0,\n  10.0/64.0,58.0/64.0,  6.0/64.0, 54.0/64.0,  9.0/64.0,57.0/64.0,  5.0/64.0, 53.0/64.0,\n  42.0/64.0,26.0/64.0, 38.0/64.0, 22.0/64.0, 41.0/64.0,25.0/64.0, 37.0/64.0, 21.0/64.0\n);\n\nvec4 mod289(vec4 x) { return x - floor(x * (1.0/289.0)) * 289.0; }\nvec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }\nvec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }\nvec2 fade(vec2 t) { return t*t*t*(t*(t*6.0-15.0)+10.0); }\n\nfloat cnoise(vec2 P) {\n  vec4 Pi = floor(P.xyxy) + vec4(0.0,0.0,1.0,1.0);\n  vec4 Pf = fract(P.xyxy) - vec4(0.0,0.0,1.0,1.0);\n  Pi = mod289(Pi);\n  vec4 ix = Pi.xzxz;\n  vec4 iy = Pi.yyww;\n  vec4 fx = Pf.xzxz;\n  vec4 fy = Pf.yyww;\n  vec4 i = permute(permute(ix) + iy);\n  vec4 gx = fract(i * (1.0/41.0)) * 2.0 - 1.0;\n  vec4 gy = abs(gx) - 0.5;\n  vec4 tx = floor(gx + 0.5);\n  gx = gx - tx;\n  vec2 g00 = vec2(gx.x, gy.x);\n  vec2 g10 = vec2(gx.y, gy.y);\n  vec2 g01 = vec2(gx.z, gy.z);\n  vec2 g11 = vec2(gx.w, gy.w);\n  vec4 norm = taylorInvSqrt(vec4(dot(g00,g00), dot(g01,g01), dot(g10,g10), dot(g11,g11)));\n  g00 *= norm.x; g01 *= norm.y; g10 *= norm.z; g11 *= norm.w;\n  float n00 = dot(g00, vec2(fx.x, fy.x));\n  float n10 = dot(g10, vec2(fx.y, fy.y));\n  float n01 = dot(g01, vec2(fx.z, fy.z));\n  float n11 = dot(g11, vec2(fx.w, fy.w));\n  vec2 fade_xy = fade(Pf.xy);\n  vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);\n  return 2.3 * mix(n_x.x, n_x.y, fade_xy.y);\n}\n\nfloat fbm(vec2 p) {\n  float v = 0.0;\n  float a = 0.5;\n  mat2 rot = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5));\n  for (int i = 0; i < 4; i++) {\n    v += a * abs(cnoise(p));\n    p = rot * p * 2.0 + vec2(100.0);\n    a *= 0.5;\n  }\n  return v;\n}\n\nvec3 dither(vec2 coord, vec3 color) {\n  if (u_dither_active < 0.5) return color;\n  vec2 scaledCoord = floor(coord / u_pixel_size);\n  int x = int(mod(scaledCoord.x, 8.0));\n  int y = int(mod(scaledCoord.y, 8.0));\n  int index = y * 8 + x;\n  float threshold = 0.0;\n  for (int i = 0; i < 64; i++) {\n    if (i == index) {\n      threshold = bayerMatrix8x8[i] - 0.25;\n      break;\n    }\n  }\n  float stepVal = 1.0 / max(1.0, u_color_num - 1.0);\n  color += threshold * stepVal;\n  float luminance = dot(color, vec3(0.2126, 0.7152, 0.0722));\n  float bias = mix(0.15, 0.0, smoothstep(0.4, 0.8, luminance));\n  color = clamp(color - bias, 0.0, 1.0);\n  return floor(color * (u_color_num - 1.0) + 0.5) / (u_color_num - 1.0);\n}\n\nvoid main() {\n  vec2 uv = gl_FragCoord.xy / u_resolution.xy;\n  vec2 centered = uv - 0.5;\n  centered.x *= u_resolution.x / u_resolution.y;\n\n  float len = length(centered);\n  if (len > 0.5) {\n    gl_FragColor = vec4(0.0);\n    return;\n  }\n\n  float t = u_time * u_speed;\n  vec2 p = centered * 3.4;\n\n  vec2 m = (u_mouse / u_resolution - 0.5) * vec2(1.0, -1.0);\n  m.x *= u_resolution.x / u_resolution.y;\n  float mDist = length(centered - m);\n  float mEffect = 1.0 - smoothstep(0.0, 0.45, mDist);\n\n  float n1 = fbm(p + vec2(t * 0.35, -t * 0.25));\n  float n2 = fbm(p - vec2(t * 0.15, t * 0.2) + n1);\n  float n = n2 - mEffect * 0.35;\n\n  float sphericalVignette = smoothstep(0.5, 0.2, len);\n  vec3 col = mix(u_bg_color, u_color, clamp(n, 0.0, 1.0));\n  col = mix(col, vec3(1.0), pow(clamp(n * 1.25, 0.0, 1.0), 3.0) * 0.65);\n\n  col = dither(gl_FragCoord.xy, col);\n  float alpha = sphericalVignette * 0.92;\n\n  gl_FragColor = vec4(col, alpha);\n}\n`;\n\nconst REDUCED_MOTION_QUERY = \"(prefers-reduced-motion: reduce)\";\n\nfunction subscribeReducedMotion(onChange: () => void) {\n  const media = window.matchMedia(REDUCED_MOTION_QUERY);\n  media.addEventListener(\"change\", onChange);\n  return () => media.removeEventListener(\"change\", onChange);\n}\n\nfunction getReducedMotion() {\n  return window.matchMedia(REDUCED_MOTION_QUERY).matches;\n}\n\nfunction hexToRgb(hex: string): [number, number, number] {\n  let clean = hex.replace(\"#\", \"\");\n  if (clean.length === 3) {\n    clean = clean\n      .split(\"\")\n      .map((c) => c + c)\n      .join(\"\");\n  }\n  const intVal = parseInt(clean, 16);\n  if (isNaN(intVal)) return [0.68, 0.37, 1.0];\n  const r = ((intVal >> 16) & 255) / 255;\n  const g = ((intVal >> 8) & 255) / 255;\n  const b = (intVal & 255) / 255;\n  return [r, g, b];\n}\n\nconst VARIANT_PRESETS: Record<\n  NonNullable<AiOrbProps[\"variant\"]>,\n  { primary: string; secondary: string; shadowA: string; shadowB: string }\n> = {\n  chromatic: {\n    primary: \"#ad5fff\",\n    secondary: \"#471eec\",\n    shadowA: \"#ad5fff\",\n    shadowB: \"#471eec\",\n  },\n  neon: {\n    primary: \"#3B82F6\",\n    secondary: \"#10B981\",\n    shadowA: \"#3B82F6\",\n    shadowB: \"#10B981\",\n  },\n  cyberpunk: {\n    primary: \"#F43F5E\",\n    secondary: \"#06B6D4\",\n    shadowA: \"#F43F5E\",\n    shadowB: \"#06B6D4\",\n  },\n  emerald: {\n    primary: \"#10B981\",\n    secondary: \"#047857\",\n    shadowA: \"#34D399\",\n    shadowB: \"#064E3B\",\n  },\n  amber: {\n    primary: \"#F59E0B\",\n    secondary: \"#B45309\",\n    shadowA: \"#FBBF24\",\n    shadowB: \"#78350F\",\n  },\n  monochrome: {\n    primary: \"#FFFFFF\",\n    secondary: \"#52525B\",\n    shadowA: \"#E4E4E7\",\n    shadowB: \"#27272A\",\n  },\n};\n\nexport function AiOrb({\n  className,\n  color,\n  colorNum = 5,\n  dither = true,\n  interactive = true,\n  pixelSize = 2,\n  showText = true,\n  size = 190,\n  speed = 0.8,\n  text = \"Generating\",\n  variant = \"chromatic\",\n}: AiOrbProps) {\n  const canvasHostRef = useRef<HTMLDivElement>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const mousePos = useRef({ x: -1000, y: -1000 });\n\n  const preset = VARIANT_PRESETS[variant] ?? VARIANT_PRESETS.chromatic;\n  const primaryColor = color ?? preset.primary;\n  const secondaryColor = preset.secondary;\n\n  const rgbPrimary = useMemo(() => hexToRgb(primaryColor), [primaryColor]);\n  const rgbSecondary = useMemo(\n    () => hexToRgb(secondaryColor),\n    [secondaryColor],\n  );\n\n  const letters = useMemo(() => text.split(\"\"), [text]);\n  const reduceMotion = React.useSyncExternalStore(\n    subscribeReducedMotion,\n    getReducedMotion,\n    () => false,\n  );\n\n  useEffect(() => {\n    const host = canvasHostRef.current;\n    if (!host) return;\n\n    const canvas = document.createElement(\"canvas\");\n    canvas.className = \"relative block h-full w-full\";\n    host.appendChild(canvas);\n\n    let gl: WebGLRenderingContext | null = null;\n    const releaseCanvas = () => {\n      gl?.getExtension(\"WEBGL_lose_context\")?.loseContext();\n      canvas.remove();\n    };\n    try {\n      gl = canvas.getContext(\"webgl\", {\n        alpha: true,\n        antialias: false,\n        depth: false,\n        preserveDrawingBuffer: false,\n      });\n    } catch {\n      return releaseCanvas;\n    }\n\n    if (!gl) {\n      return releaseCanvas;\n    }\n\n    const compileShader = (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        gl!.deleteShader(shader);\n        return null;\n      }\n      return shader;\n    };\n\n    const vs = compileShader(gl.VERTEX_SHADER, VERTEX_SHADER);\n    const fs = compileShader(gl.FRAGMENT_SHADER, FRAGMENT_SHADER);\n\n    if (!vs || !fs) {\n      return releaseCanvas;\n    }\n\n    const program = gl.createProgram();\n    if (!program) return releaseCanvas;\n    gl.attachShader(program, vs);\n    gl.attachShader(program, fs);\n    gl.linkProgram(program);\n\n    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n      gl.deleteProgram(program);\n      return releaseCanvas;\n    }\n\n    gl.useProgram(program);\n\n    const quadBuffer = gl.createBuffer();\n    gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);\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\n    const positionLoc = gl.getAttribLocation(program, \"position\");\n    gl.enableVertexAttribArray(positionLoc);\n    gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);\n\n    const uResolution = gl.getUniformLocation(program, \"u_resolution\");\n    const uTime = gl.getUniformLocation(program, \"u_time\");\n    const uMouse = gl.getUniformLocation(program, \"u_mouse\");\n    const uColor = gl.getUniformLocation(program, \"u_color\");\n    const uBgColor = gl.getUniformLocation(program, \"u_bg_color\");\n    const uPixelSize = gl.getUniformLocation(program, \"u_pixel_size\");\n    const uColorNum = gl.getUniformLocation(program, \"u_color_num\");\n    const uSpeed = gl.getUniformLocation(program, \"u_speed\");\n    const uDitherActive = gl.getUniformLocation(program, \"u_dither_active\");\n\n    let animationFrameId = 0;\n    const startTime = performance.now();\n\n    const render = () => {\n      if (!gl || !canvas) return;\n\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      const displayWidth = Math.floor(canvas.clientWidth * dpr);\n      const displayHeight = Math.floor(canvas.clientHeight * dpr);\n\n      if (canvas.width !== displayWidth || canvas.height !== displayHeight) {\n        canvas.width = displayWidth;\n        canvas.height = displayHeight;\n        gl.viewport(0, 0, displayWidth, displayHeight);\n      }\n\n      gl.clearColor(0, 0, 0, 0);\n      gl.clear(gl.COLOR_BUFFER_BIT);\n\n      const elapsed = (performance.now() - startTime) * 0.001;\n\n      gl.uniform2f(uResolution, canvas.width, canvas.height);\n      gl.uniform1f(uTime, elapsed);\n      gl.uniform2f(uMouse, mousePos.current.x * dpr, mousePos.current.y * dpr);\n      gl.uniform3f(uColor, rgbPrimary[0], rgbPrimary[1], rgbPrimary[2]);\n      gl.uniform3f(uBgColor, rgbSecondary[0], rgbSecondary[1], rgbSecondary[2]);\n      gl.uniform1f(uPixelSize, pixelSize);\n      gl.uniform1f(uColorNum, colorNum);\n      gl.uniform1f(uSpeed, speed);\n      gl.uniform1f(uDitherActive, dither ? 1.0 : 0.0);\n\n      gl.drawArrays(gl.TRIANGLES, 0, 6);\n\n      if (!reduceMotion) {\n        animationFrameId = requestAnimationFrame(render);\n      }\n    };\n\n    animationFrameId = requestAnimationFrame(render);\n\n    const resizeObserver = reduceMotion ? new ResizeObserver(render) : null;\n    resizeObserver?.observe(host);\n\n    return () => {\n      cancelAnimationFrame(animationFrameId);\n      resizeObserver?.disconnect();\n      if (gl) {\n        gl.deleteBuffer(quadBuffer);\n        gl.deleteProgram(program);\n        gl.deleteShader(vs);\n        gl.deleteShader(fs);\n      }\n      releaseCanvas();\n    };\n  }, [\n    colorNum,\n    dither,\n    pixelSize,\n    reduceMotion,\n    rgbPrimary,\n    rgbSecondary,\n    speed,\n  ]);\n\n  const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (!interactive) return;\n    const rect = containerRef.current?.getBoundingClientRect();\n    if (!rect) return;\n    mousePos.current = {\n      x: e.clientX - rect.left,\n      y: e.clientY - rect.top,\n    };\n  };\n\n  const handlePointerLeave = () => {\n    mousePos.current = { x: -1000, y: -1000 };\n  };\n\n  return (\n    <div\n      ref={containerRef}\n      onPointerMove={handlePointerMove}\n      onPointerLeave={handlePointerLeave}\n      data-slot=\"ai-orb\"\n      className={cn(\n        \"relative flex items-center justify-center rounded-full select-none cursor-pointer overflow-visible transition-transform duration-300 hover:scale-[1.03]\",\n        className,\n      )}\n      style={{\n        width: size,\n        height: size,\n      }}\n    >\n      <div\n        className=\"absolute inset-[-15%] rounded-full blur-2xl opacity-40 pointer-events-none transition-colors duration-500 will-change-transform\"\n        style={{\n          background: `radial-gradient(circle, ${primaryColor} 0%, ${secondaryColor} 60%, transparent 75%)`,\n        }}\n      />\n\n      <div className=\"absolute inset-0 rounded-full overflow-hidden z-0\">\n        <div\n          className=\"absolute inset-0 rounded-full\"\n          style={{\n            background: `radial-gradient(circle at 35% 35%, ${primaryColor} 0%, ${secondaryColor} 55%, #050508 100%)`,\n          }}\n        />\n        <div ref={canvasHostRef} className=\"relative h-full w-full\" />\n      </div>\n\n      <motion.div\n        className=\"absolute inset-0 rounded-full pointer-events-none z-10\"\n        animate={{ rotate: reduceMotion ? 0 : 360 }}\n        transition={\n          reduceMotion\n            ? { duration: 0 }\n            : { repeat: Infinity, duration: 3.5 / speed, ease: \"linear\" }\n        }\n        style={{\n          boxShadow: `\n            0 10px 22px 0 rgba(255, 255, 255, 0.85) inset,\n            0 24px 34px 0 ${primaryColor} inset,\n            0 64px 64px 0 ${secondaryColor} inset,\n            0 0 20px 2px ${primaryColor}66\n          `,\n        }}\n      />\n\n      <motion.div\n        className=\"absolute inset-0.75 rounded-full pointer-events-none z-10 border border-white/20\"\n        animate={{ rotate: reduceMotion ? 0 : -360 }}\n        transition={\n          reduceMotion\n            ? { duration: 0 }\n            : { repeat: Infinity, duration: 7 / speed, ease: \"linear\" }\n        }\n        style={{\n          boxShadow: `\n            0 -8px 18px 0 rgba(255, 255, 255, 0.4) inset,\n            0 -16px 28px 0 ${secondaryColor} inset\n          `,\n        }}\n      />\n\n      <div className=\"absolute inset-0 rounded-full overflow-hidden pointer-events-none z-15 mix-blend-screen opacity-55\">\n        <Dither\n          color1={primaryColor}\n          color2={secondaryColor}\n          color3=\"#000000\"\n          grainAmount={0.16}\n          grainScale={2.0}\n          grainAnimated={true}\n          warpStrength={1.2}\n          timeSpeed={speed * 0.25}\n          zoom={1.1}\n        />\n      </div>\n\n      <div className=\"absolute inset-0 rounded-full pointer-events-none z-20 bg-[radial-gradient(circle_at_35%_30%,rgba(255,255,255,0.35)_0%,transparent_50%)]\" />\n\n      {showText && (\n        <div className=\"relative z-30 flex items-center justify-center gap-0.5 px-3 pointer-events-none select-none\">\n          {letters.map((char, index) => (\n            <motion.span\n              key={`${char}-${index}`}\n              initial={{ opacity: 0.35, y: 0, scale: 1 }}\n              animate={\n                reduceMotion\n                  ? { opacity: 0.85, scale: 1, y: 0 }\n                  : {\n                      opacity: [0.35, 1, 0.65, 0.35],\n                      scale: [1, 1.18, 1, 1],\n                      y: [0, -2.5, 0, 0],\n                    }\n              }\n              transition={\n                reduceMotion\n                  ? { duration: 0 }\n                  : {\n                      duration: 2.2 / speed,\n                      repeat: Infinity,\n                      ease: \"easeInOut\",\n                      delay: index * 0.1,\n                    }\n              }\n              className=\"inline-block font-sans font-light tracking-wide text-white text-sm drop-shadow-[0_0_8px_rgba(255,255,255,0.9)]\"\n            >\n              {char}\n            </motion.span>\n          ))}\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport default AiOrb;\n",
      "type": "registry:ui",
      "target": "@ui/ai-orb.tsx"
    }
  ],
  "type": "registry:ui",
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "title": "AI Orb"
}
