{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glass-orb",
  "title": "Glass Orb",
  "description": "An interactive ambient orb that follows the cursor, emits soft glass particle trails, and updates its glow based on status.",
  "files": [
    {
      "path": "registry/innovative/glass-orb.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\n\ntype GlassOrbStatus = \"idle\" | \"active\" | \"loading\"\n\ninterface GlassOrbProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Visual state that changes the ring, glow gradient, and pulse. */\n  status?: GlassOrbStatus\n  /** Size of the orb. */\n  size?: \"sm\" | \"md\" | \"lg\"\n  /** Accessible label. When set, the orb is announced as a status region. */\n  label?: string\n  /** Whether the orb follows the cursor with elastic motion. */\n  followCursor?: boolean\n  /** Whether to emit soft glass particles when moving quickly. */\n  showTrails?: boolean\n}\n\nconst sizes = {\n  sm: 96,\n  md: 140,\n  lg: 190,\n} as const\n\ninterface StatusConfig {\n  ring: string\n  glow: string\n  pulse: boolean\n}\n\nconst statusStyles: Record<GlassOrbStatus, StatusConfig> = {\n  idle: {\n    ring: \"border-cyan-300/20\",\n    glow: [\n      \"radial-gradient(circle at top left, rgba(255,255,255,0.3), transparent 35%)\",\n      \"radial-gradient(circle at bottom right, rgba(34,211,238,0.24), transparent 28%)\",\n      \"radial-gradient(circle at center, rgba(139,92,246,0.2), rgba(56,189,248,0.08) 50%)\",\n      \"linear-gradient(180deg, rgba(255,255,255,0.2), rgba(15,23,42,0.1))\",\n    ].join(\", \"),\n    pulse: false,\n  },\n  active: {\n    ring: \"border-blue-200/30\",\n    glow: [\n      \"radial-gradient(circle at top left, rgba(255,255,255,0.34), transparent 35%)\",\n      \"radial-gradient(circle at bottom right, rgba(96,165,250,0.3), transparent 28%)\",\n      \"radial-gradient(circle at center, rgba(168,85,247,0.28), rgba(59,130,246,0.12) 50%)\",\n      \"linear-gradient(180deg, rgba(255,255,255,0.24), rgba(15,23,42,0.1))\",\n    ].join(\", \"),\n    pulse: false,\n  },\n  loading: {\n    ring: \"border-white/25\",\n    glow: [\n      \"radial-gradient(circle at top left, rgba(255,255,255,0.32), transparent 35%)\",\n      \"radial-gradient(circle at bottom right, rgba(103,232,249,0.3), transparent 28%)\",\n      \"radial-gradient(circle at center, rgba(255,255,255,0.24), rgba(34,211,238,0.12) 50%)\",\n      \"linear-gradient(180deg, rgba(255,255,255,0.24), rgba(15,23,42,0.1))\",\n    ].join(\", \"),\n    pulse: true,\n  },\n}\n\nconst MAX_TRAIL_PARTICLES = 16\nconst TRAIL_LIFE = 24\nconst TRAIL_FADE = 0.018\n\ninterface TrailParticle {\n  el: HTMLSpanElement\n  x: number\n  y: number\n  vx: number\n  vy: number\n  opacity: number\n  life: number\n}\n\nconst GlassOrb = React.forwardRef<HTMLDivElement, GlassOrbProps>(\n  (\n    {\n      className,\n      size = \"md\",\n      status = \"idle\",\n      label,\n      followCursor = true,\n      showTrails = true,\n      ...props\n    },\n    ref,\n  ) => {\n    const rootRef = React.useRef<HTMLDivElement>(null)\n    const orbRef = React.useRef<HTMLDivElement>(null)\n    const particleLayerRef = React.useRef<HTMLDivElement>(null)\n    const particlesRef = React.useRef<TrailParticle[]>([])\n\n    const targetRef = React.useRef({ x: 0, y: 0 })\n    const motionRef = React.useRef({ x: 0, y: 0, vx: 0, vy: 0 })\n    const frameRef = React.useRef<number | null>(null)\n\n    const [reducedMotion, setReducedMotion] = React.useState(false)\n\n    const sizePx = sizes[size]\n    const config = statusStyles[status]\n\n    React.useImperativeHandle(ref, () => rootRef.current as HTMLDivElement)\n\n    // Respect the user's reduced-motion preference.\n    React.useEffect(() => {\n      if (typeof window.matchMedia !== \"function\") return\n\n      const mql = window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n      const onChange = () => setReducedMotion(mql.matches)\n      onChange()\n      mql.addEventListener(\"change\", onChange)\n      return () => mql.removeEventListener(\"change\", onChange)\n    }, [])\n\n    // Center the orb, re-centering when the container resizes.\n    React.useLayoutEffect(() => {\n      const el = rootRef.current\n      if (!el) return\n\n      const center = () => {\n        const rect = el.getBoundingClientRect()\n        const x = Math.max(0, rect.width / 2 - sizePx / 2)\n        const y = Math.max(0, rect.height / 2 - sizePx / 2)\n        targetRef.current = { x, y }\n        motionRef.current = { x, y, vx: 0, vy: 0 }\n        if (orbRef.current) {\n          orbRef.current.style.transform = `translate3d(${x}px, ${y}px, 0)`\n        }\n      }\n\n      center()\n\n      if (typeof ResizeObserver !== \"undefined\") {\n        const observer = new ResizeObserver(center)\n        observer.observe(el)\n        return () => observer.disconnect()\n      }\n    }, [sizePx])\n\n    // Track the pointer for cursor-following.\n    React.useEffect(() => {\n      if (!followCursor || reducedMotion) return\n\n      const handlePointerMove = (event: PointerEvent) => {\n        const el = rootRef.current\n        if (!el) return\n        const rect = el.getBoundingClientRect()\n        targetRef.current = {\n          x: event.clientX - rect.left - sizePx / 2,\n          y: event.clientY - rect.top - sizePx / 2,\n        }\n      }\n\n      window.addEventListener(\"pointermove\", handlePointerMove, { passive: true })\n      return () => window.removeEventListener(\"pointermove\", handlePointerMove)\n    }, [followCursor, reducedMotion, sizePx])\n\n    // Spring physics + particle trails, written directly to the DOM.\n    React.useEffect(() => {\n      if (!followCursor || reducedMotion) return\n\n      const spring = 0.14\n      const damping = 0.8\n\n      const animate = () => {\n        const target = targetRef.current\n        const motion = motionRef.current\n\n        const dx = target.x - motion.x\n        const dy = target.y - motion.y\n\n        motion.vx = motion.vx * damping + dx * spring\n        motion.vy = motion.vy * damping + dy * spring\n        motion.x += motion.vx\n        motion.y += motion.vy\n\n        const speed = Math.sqrt(motion.vx ** 2 + motion.vy ** 2)\n        const scale = 1 + Math.min(speed / 120, 0.16)\n        const rotation = speed * 0.16\n\n        if (orbRef.current) {\n          orbRef.current.style.transform = `translate3d(${motion.x}px, ${motion.y}px, 0) scale(${scale}) rotate(${rotation}deg)`\n        }\n\n        if (showTrails) {\n          const particles = particlesRef.current\n\n          if (\n            speed > 12 &&\n            particles.length < MAX_TRAIL_PARTICLES &&\n            Math.random() < 0.15 &&\n            particleLayerRef.current\n          ) {\n            const layer = particleLayerRef.current\n            const trailSize = 4 + Math.min(speed / 8, 12)\n            const el = document.createElement(\"span\")\n            el.className = \"absolute rounded-full bg-white/70 blur-[2px] pointer-events-none\"\n            el.style.left = `${motion.x + sizePx / 2}px`\n            el.style.top = `${motion.y + sizePx / 2}px`\n            el.style.width = `${trailSize}px`\n            el.style.height = `${trailSize}px`\n            el.style.opacity = \"0.5\"\n            el.style.transform = \"translate(-50%, -50%)\"\n            layer.appendChild(el)\n\n            particles.push({\n              el,\n              x: 0,\n              y: 0,\n              vx: motion.vx * 0.1 + (Math.random() - 0.5) * 0.8,\n              vy: motion.vy * 0.1 + (Math.random() - 0.5) * 0.8,\n              opacity: 0.5,\n              life: TRAIL_LIFE,\n            })\n          }\n\n          for (let index = particles.length - 1; index >= 0; index--) {\n            const particle = particles[index]\n            particle.life -= 1\n            particle.opacity -= TRAIL_FADE\n            particle.x += particle.vx\n            particle.y += particle.vy\n            particle.el.style.transform = `translate(-50%, -50%) translate(${particle.x}px, ${particle.y}px)`\n            particle.el.style.opacity = particle.opacity.toFixed(3)\n            if (particle.life <= 0 || particle.opacity <= 0) {\n              particle.el.remove()\n              particles.splice(index, 1)\n            }\n          }\n        }\n\n        frameRef.current = window.requestAnimationFrame(animate)\n      }\n\n      frameRef.current = window.requestAnimationFrame(animate)\n      return () => {\n        if (frameRef.current) window.cancelAnimationFrame(frameRef.current)\n        for (const particle of particlesRef.current) particle.el.remove()\n        particlesRef.current = []\n      }\n    }, [followCursor, reducedMotion, showTrails, sizePx])\n\n    return (\n      <div\n        ref={rootRef}\n        className={cn(\"relative overflow-hidden pointer-events-none\", className)}\n        {...props}\n        role={label ? \"status\" : undefined}\n        aria-label={label}\n        aria-hidden={label ? undefined : true}\n        style={{\n          // Give the wrapper intrinsic dimensions so the absolutely positioned\n          // orb stays visible (with overflow-hidden) even without sizing classes.\n          minWidth: sizePx,\n          minHeight: sizePx,\n          ...props.style,\n        }}\n      >\n        <div\n          ref={orbRef}\n          className={cn(\n            \"absolute rounded-full\",\n            \"border border-white/10 shadow-[0_0_120px_rgba(56,189,248,0.15)]\",\n            config.ring,\n            config.pulse && \"animate-pulse\",\n          )}\n          style={{\n            width: sizePx,\n            height: sizePx,\n            transition: \"border-color 200ms ease\",\n          }}\n        >\n          <div\n            className=\"absolute inset-0 rounded-full border border-white/10\"\n            style={{ background: config.glow }}\n          />\n\n          <div\n            className=\"absolute inset-4 rounded-full bg-white/10 blur-[2px]\"\n            style={{\n              background: \"radial-gradient(circle at 35% 35%, rgba(255,255,255,0.55), transparent 45%)\",\n            }}\n          />\n\n          <div className=\"absolute inset-0 rounded-full mix-blend-screen opacity-80\" />\n\n          <div\n            className=\"absolute inset-0 rounded-full border border-white/10\"\n            style={{ background: \"radial-gradient(circle at center, rgba(255,255,255,0.16), transparent 55%)\" }}\n          />\n\n          <div\n            className=\"absolute inset-0 rounded-full\"\n            style={{\n              background:\n                \"radial-gradient(circle at 60% 30%, rgba(255,255,255,0.35), transparent 18%), \" +\n                \"radial-gradient(circle at 30% 60%, rgba(56,189,248,0.22), transparent 23%), \" +\n                \"linear-gradient(135deg, rgba(56,189,248,0.18), rgba(168,85,247,0.1))\",\n              mixBlendMode: \"screen\",\n            }}\n          />\n\n          {label ? (\n            <div className=\"absolute bottom-3 left-1/2 -translate-x-1/2 rounded-full bg-black/40 px-3 py-1 text-[11px] uppercase tracking-[0.2em] text-white/85 shadow-[0_0_25px_rgba(0,0,0,0.2)]\">\n              {label}\n            </div>\n          ) : null}\n        </div>\n\n        {/* Particle trail layer (populated imperatively by the animation loop). */}\n        <div ref={particleLayerRef} className=\"absolute inset-0 overflow-hidden pointer-events-none\" />\n      </div>\n    )\n  },\n)\n\nGlassOrb.displayName = \"GlassOrb\"\n\nexport { GlassOrb }\nexport type { GlassOrbStatus, GlassOrbProps }\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}