{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-orb",
  "type": "registry:ui",
  "title": "AI Orb",
  "description": "Luminescent AI generation sphere with native WebGL Bayer 8x8 dithered plasma and animated letter wave typography.",
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.devclubxnst.com/r/dither.json"
  ],
  "files": [
    {
      "path": "src/registry/ui/ai-orb.tsx",
      "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"
    }
  ]
}
