{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"orb","type":"registry:ui","title":"Orb","description":"A glowing, breathing energy orb with a swirling surface that brightens and wobbles under the pointer.","dependencies":["motion"],"registryDependencies":["https://ui.mekaigames.com/r/theme.json"],"files":[{"path":"components/ui/orb.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useReducedMotion } from \"motion/react\";\nimport { useEffect, useRef } from \"react\";\n\ntype OrbProps = {\n  /** Orb color as a hex value. */\n  color?: string;\n  /** Diameter as a share of the shorter side, from 0.2 to 1. */\n  size?: number;\n  /** How much the orb distorts and brightens under the pointer, from 0 to 1. */\n  hoverIntensity?: number;\n  /** Spin the swirling surface faster while hovered. */\n  rotateOnHover?: boolean;\n  /** Render resolution, from 0.25 to 1. Lower is cheaper. */\n  quality?: number;\n  className?: string;\n};\n\nconst VERTEX = `\nattribute vec2 aPos;\nvoid main() { gl_Position = vec4(aPos, 0.0, 1.0); }\n`;\n\nconst FRAGMENT = `\n#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\nuniform vec2 uRes;\nuniform float uTime;\nuniform vec3 uColor;\nuniform float uSize;\nuniform float uHover;\nuniform float uSpin;\n\nvoid main() {\n  float unit = min(uRes.x, uRes.y);\n  vec2 p = (gl_FragCoord.xy - 0.5 * uRes) / unit;\n  float t = uTime;\n  float hover = uHover;\n\n  // Breathing radius, and a liquid wobble while hovered.\n  float radius = 0.5 * uSize * (1.0 + 0.025 * sin(t * 1.4) + 0.03 * hover);\n  p += hover * 0.016 * vec2(sin(p.y * 22.0 + t * 5.0), cos(p.x * 22.0 + t * 4.3));\n  float r = length(p) / radius;\n  float a = atan(p.y, p.x + 1e-5) + uSpin;\n\n  // Swirling bands: the angle is twisted more toward the rim.\n  float swirl = a + r * 2.6 - t * 0.4;\n  float bands = 0.5 + 0.25 * sin(swirl * 3.0 + sin(r * 7.0 - t * 1.3) * 1.4) + 0.15 * sin(swirl * 5.0 - t * 0.9 + r * 4.0);\n\n  vec3 deep = uColor * 0.3;\n  vec3 hot = mix(uColor, vec3(1.0, 0.93, 0.86), 0.55);\n  float rim = pow(clamp(r, 0.0, 1.0), 4.0);\n  vec3 inner = mix(deep, uColor, bands);\n  inner += hot * (rim * 0.85 + pow(bands, 4.0) * 0.45);\n  inner += hot * (1.0 - smoothstep(0.0, 0.6, r)) * 0.3;\n  inner *= 1.0 + 0.45 * hover;\n\n  float body = 1.0 - smoothstep(0.97, 1.0, r);\n  float glow = exp(-max(r - 1.0, 0.0) * 4.5) * (0.5 + 0.35 * hover + 0.07 * sin(t * 1.4));\n  float alpha = clamp(body + glow * (1.0 - body), 0.0, 1.0);\n  vec3 col = inner * body + uColor * glow * (1.0 - body);\n  // Premultiplied output: color can never exceed its alpha.\n  gl_FragColor = vec4(min(col, vec3(alpha)), alpha);\n}\n`;\n\nfunction hexToVec3(hex: string): [number, number, number] {\n  let h = hex.replace(\"#\", \"\");\n  if (h.length === 3) h = h.replace(/./g, (c) => c + c);\n  const n = parseInt(h, 16) || 0;\n  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n}\n\nfunction createProgram(gl: WebGLRenderingContext, fragment: string) {\n  const program = gl.createProgram();\n  const shaders: WebGLShader[] = [];\n  for (const [type, source] of [\n    [gl.VERTEX_SHADER, VERTEX],\n    [gl.FRAGMENT_SHADER, fragment],\n  ] as const) {\n    const shader = gl.createShader(type);\n    if (!shader || !program) return null;\n    gl.shaderSource(shader, source);\n    gl.compileShader(shader);\n    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n      console.warn(gl.getShaderInfoLog(shader));\n      return null;\n    }\n    gl.attachShader(program, shader);\n    shaders.push(shader);\n  }\n  if (!program) return null;\n  gl.linkProgram(program);\n  for (const shader of shaders) gl.deleteShader(shader);\n  return gl.getProgramParameter(program, gl.LINK_STATUS) ? program : null;\n}\n\n/** Releases the GPU context once the canvas has really left the page (not on a Strict Mode re-run). */\nfunction releaseLater(gl: WebGLRenderingContext, canvas: HTMLCanvasElement) {\n  window.setTimeout(() => {\n    if (!canvas.isConnected) gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n  }, 0);\n}\n\n/** A glowing, breathing energy orb with a swirling surface that brightens and wobbles under the pointer. */\nexport function Orb({\n  color = \"#ff784f\",\n  size = 0.6,\n  hoverIntensity = 0.6,\n  rotateOnHover = true,\n  quality = 1,\n  className = \"\",\n}: OrbProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const reduce = useReducedMotion();\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const [r, g, b] = hexToVec3(color);\n    const tone = (a: number) => `rgba(${Math.round(r * 255)},${Math.round(g * 255)},${Math.round(b * 255)},${a})`;\n    const diameter = Math.min(1, Math.max(0.2, size));\n    const fallback = `radial-gradient(circle closest-side, ${tone(1)} 0%, ${tone(0.9)} ${diameter * 80}%, ${tone(0.25)} ${diameter * 100}%, transparent ${Math.min(100, diameter * 130)}%)`;\n    const gl = canvas.getContext(\"webgl\", { alpha: true, premultipliedAlpha: true, antialias: false, depth: false, powerPreference: \"low-power\" });\n    const program = gl ? createProgram(gl, FRAGMENT) : null;\n    if (!gl || !program) {\n      canvas.style.background = fallback;\n      return;\n    }\n    canvas.style.background = \"\";\n\n    const buffer = gl.createBuffer();\n    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n    const aPos = gl.getAttribLocation(program, \"aPos\");\n    gl.enableVertexAttribArray(aPos);\n    gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);\n    gl.useProgram(program);\n\n    const uniform = (name: string) => gl.getUniformLocation(program, name);\n    const uRes = uniform(\"uRes\");\n    const uTime = uniform(\"uTime\");\n    const uHover = uniform(\"uHover\");\n    const uSpin = uniform(\"uSpin\");\n    gl.uniform3f(uniform(\"uColor\"), r, g, b);\n    gl.uniform1f(uniform(\"uSize\"), diameter);\n\n    const strength = Math.min(1, Math.max(0, hoverIntensity));\n    let hover = 0;\n    let hoverTarget = 0;\n    let spin = 0;\n    let time = 10;\n    let last = 0;\n    let frame = 0;\n    let visible = true;\n    let lost = false;\n\n    const render = (t: number) => {\n      if (lost) return;\n      const dt = last ? Math.min(50, t - last) : 16;\n      last = t;\n      if (!reduce) {\n        time += dt / 1000;\n        hover += (hoverTarget - hover) * Math.min(1, dt * 0.005);\n        spin += (dt / 1000) * (0.15 + (rotateOnHover ? hover * 1.6 : 0));\n      }\n      gl.uniform1f(uTime, time % 3600);\n      gl.uniform1f(uHover, hover * strength);\n      gl.uniform1f(uSpin, spin % (Math.PI * 2));\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n      if (!reduce && visible) frame = requestAnimationFrame(render);\n      else frame = 0;\n    };\n\n    const resize = () => {\n      const dpr = Math.min(window.devicePixelRatio || 1, 2) * Math.min(1, Math.max(0.25, quality));\n      canvas.width = Math.max(1, Math.round(canvas.clientWidth * dpr));\n      canvas.height = Math.max(1, Math.round(canvas.clientHeight * dpr));\n      gl.viewport(0, 0, canvas.width, canvas.height);\n      gl.uniform2f(uRes, canvas.width, canvas.height);\n      if (reduce) render(0);\n    };\n\n    // Hovered means the pointer is over the orb itself, not just anywhere on the canvas.\n    const onMove = (e: PointerEvent) => {\n      const rect = canvas.getBoundingClientRect();\n      const dx = e.clientX - (rect.left + rect.width / 2);\n      const dy = e.clientY - (rect.top + rect.height / 2);\n      const orbRadius = (Math.min(rect.width, rect.height) * diameter) / 2;\n      hoverTarget = Math.hypot(dx, dy) < orbRadius * 1.1 ? 1 : 0;\n    };\n    const onLost = () => {\n      lost = true;\n      cancelAnimationFrame(frame);\n      canvas.style.background = fallback;\n    };\n\n    resize();\n    const observer = new ResizeObserver(resize);\n    observer.observe(canvas);\n    const io = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting;\n      if (visible && !reduce && !frame && !lost) {\n        last = 0;\n        frame = requestAnimationFrame(render);\n      }\n    });\n    io.observe(canvas);\n    canvas.addEventListener(\"webglcontextlost\", onLost);\n    if (!reduce) {\n      window.addEventListener(\"pointermove\", onMove, { passive: true });\n      frame = requestAnimationFrame(render);\n    }\n\n    return () => {\n      cancelAnimationFrame(frame);\n      observer.disconnect();\n      io.disconnect();\n      window.removeEventListener(\"pointermove\", onMove);\n      canvas.removeEventListener(\"webglcontextlost\", onLost);\n      gl.deleteBuffer(buffer);\n      gl.deleteProgram(program);\n      releaseLater(gl, canvas);\n    };\n  }, [color, size, hoverIntensity, rotateOnHover, quality, reduce]);\n\n  return (\n    <canvas\n      ref={canvasRef}\n      className={`pointer-events-none absolute inset-0 block size-full ${className}`}\n      aria-hidden=\"true\"\n    />\n  );\n}\n"}]}