{"$schema":"https://ui.shadcn.com/schema/registry.json","homepage":"https://uicapsule.com/ui/gradient-orb","name":"gradient-orb","type":"registry:block","author":"Kaiyu Hsu <uicapsule@kyh.io>","dependencies":["@react-three/fiber","three"],"devDependencies":["@types/three"],"registryDependencies":[],"files":[{"type":"registry:file","path":"/gradient-orb.tsx","content":"\"use client\";\n\nimport { useRef, useMemo, useEffect } from \"react\";\nimport { Canvas, useFrame, useThree } from \"@react-three/fiber\";\nimport * as THREE from \"three\";\n\n/**\n * Configuration options for the gradient orb.\n * All fields are optional and fall back to sensible defaults.\n */\nexport type GradientOrbConfig = {\n  /** CSS color string for the canvas background. @default \"#0a0a0a\" */\n  background?: string;\n  /** Hue rotation in degrees applied to all gradient colors. @default 0 */\n  hue?: number;\n  /** Constant rotation speed of the orb (radians/sec). @default 0.3 */\n  rotationSpeed?: number;\n  /** Scale of the noise pattern inside the orb. @default 0.65 */\n  noiseScale?: number;\n  /** Inner radius of the orb glow (0–1). @default 0.1 */\n  innerRadius?: number;\n};\n\nconst defaults: Required<GradientOrbConfig> = {\n  background: \"#0a0a0a\",\n  hue: 0,\n  rotationSpeed: 0.3,\n  noiseScale: 0.65,\n  innerRadius: 0.1,\n};\n\n/**\n * GLSL vertex shader — pass-through that outputs clip-space positions\n * directly from a fullscreen triangle in NDC.\n */\nconst vertexShader = /* glsl */ `\n  varying vec2 vUv;\n\n  void main() {\n    vUv = uv;\n    gl_Position = vec4(position.xy, 0.0, 1.0);\n  }\n`;\n\n/**\n * GLSL fragment shader — renders a glowing orb with three noise-mixed colors,\n * hue rotation, breathing pulse, and constant rotation.\n */\nconst fragmentShader = /* glsl */ `\n  precision highp float;\n\n  uniform float iTime;\n  uniform vec3 iResolution;\n  uniform float hue;\n  uniform float rot;\n  uniform float noiseScale;\n  uniform float innerRadius;\n\n  varying vec2 vUv;\n\n  // --- YIQ color space hue rotation ---\n\n  vec3 rgb2yiq(vec3 c) {\n    return vec3(\n      dot(c, vec3(0.299, 0.587, 0.114)),\n      dot(c, vec3(0.596, -0.274, -0.322)),\n      dot(c, vec3(0.211, -0.523, 0.312))\n    );\n  }\n\n  vec3 yiq2rgb(vec3 c) {\n    return vec3(\n      c.x + 0.956 * c.y + 0.621 * c.z,\n      c.x - 0.272 * c.y - 0.647 * c.z,\n      c.x - 1.106 * c.y + 1.703 * c.z\n    );\n  }\n\n  vec3 adjustHue(vec3 color, float hueDeg) {\n    float hueRad = radians(hueDeg);\n    vec3 yiq = rgb2yiq(color);\n    float cosA = cos(hueRad);\n    float sinA = sin(hueRad);\n    yiq.yz = vec2(yiq.y * cosA - yiq.z * sinA, yiq.y * sinA + yiq.z * cosA);\n    return yiq2rgb(yiq);\n  }\n\n  // --- 3D simplex noise (hash-based) ---\n\n  vec3 hash33(vec3 p3) {\n    p3 = fract(p3 * vec3(0.1031, 0.11369, 0.13787));\n    p3 += dot(p3, p3.yxz + 19.19);\n    return -1.0 + 2.0 * fract(vec3(p3.x + p3.y, p3.x + p3.z, p3.y + p3.z) * p3.zyx);\n  }\n\n  float snoise3(vec3 p) {\n    const float K1 = 0.333333333;\n    const float K2 = 0.166666667;\n    vec3 i = floor(p + (p.x + p.y + p.z) * K1);\n    vec3 d0 = p - (i - (i.x + i.y + i.z) * K2);\n    vec3 e = step(vec3(0.0), d0 - d0.yzx);\n    vec3 i1 = e * (1.0 - e.zxy);\n    vec3 i2 = 1.0 - e.zxy * (1.0 - e);\n    vec3 d1 = d0 - (i1 - K2);\n    vec3 d2 = d0 - (i2 - K1);\n    vec3 d3 = d0 - 0.5;\n    vec4 h = max(0.6 - vec4(dot(d0, d0), dot(d1, d1), dot(d2, d2), dot(d3, d3)), 0.0);\n    vec4 n = h * h * h * h * vec4(\n      dot(d0, hash33(i)),\n      dot(d1, hash33(i + i1)),\n      dot(d2, hash33(i + i2)),\n      dot(d3, hash33(i + 1.0))\n    );\n    return dot(vec4(31.316), n);\n  }\n\n  // --- Orb rendering ---\n\n  vec4 extractAlpha(vec3 colorIn) {\n    float a = max(max(colorIn.r, colorIn.g), colorIn.b);\n    return vec4(colorIn.rgb / (a + 1e-5), a);\n  }\n\n  const vec3 baseColor0 = vec3(0.239, 0.353, 1.0);   // blue\n  const vec3 baseColor1 = vec3(0.616, 0.0, 1.0);     // purple\n  const vec3 baseColor2 = vec3(1.0, 0.373, 0.122);   // orange\n  const vec3 baseColor3 = vec3(0.0, 0.0, 0.0);       // black\n\n  float light1(float intensity, float attenuation, float dist) {\n    return intensity / (1.0 + dist * attenuation);\n  }\n\n  float light2(float intensity, float attenuation, float dist) {\n    return intensity / (1.0 + dist * dist * attenuation);\n  }\n\n  vec4 draw(vec2 uv) {\n    vec3 color0 = adjustHue(baseColor0, hue);\n    vec3 color1 = adjustHue(baseColor1, hue);\n    vec3 color2 = adjustHue(baseColor2, hue);\n    vec3 color3 = adjustHue(baseColor3, hue);\n\n    float len = length(uv);\n    float invLen = len > 0.0 ? 1.0 / len : 0.0;\n\n    float pulse = sin(iTime * 1.5) * 0.02;\n\n    float n0 = snoise3(vec3(uv * noiseScale, iTime * 0.5)) * 0.5 + 0.5;\n\n    float r0 = mix(mix(innerRadius + pulse, 1.0, 0.4), mix(innerRadius + pulse, 1.0, 0.6), n0);\n\n    float d0 = distance(uv, (r0 * invLen) * uv);\n    float v0 = light1(1.0, 10.0, d0);\n    v0 *= smoothstep(r0 * 1.05, r0, len);\n    float cl = cos(atan(uv.y, uv.x) + iTime * 2.0) * 0.5 + 0.5;\n\n    float a = iTime * -1.0;\n    vec2 pos = vec2(cos(a), sin(a)) * r0;\n    float d = distance(uv, pos);\n    float v1 = light2(1.5, 5.0, d);\n    v1 *= light1(1.0, 50.0, d0);\n\n    float v2 = smoothstep(1.0, mix(innerRadius, 1.0, n0 * 0.5), len);\n    float v3 = smoothstep(innerRadius, mix(innerRadius, 1.0, 0.5), len);\n\n    vec3 col = mix(color1, color2, cl);\n    col = mix(col, color0, n0);\n    col = mix(color3, col, v0);\n    col = (col + v1) * v2 * v3;\n    col = clamp(col, 0.0, 1.0);\n\n    return extractAlpha(col);\n  }\n\n  void main() {\n    vec2 center = iResolution.xy * 0.5;\n    float size = min(iResolution.x, iResolution.y);\n    vec2 uv = (vUv * iResolution.xy - center) / size * 2.0;\n\n    float s = sin(rot);\n    float c = cos(rot);\n    uv = vec2(c * uv.x - s * uv.y, s * uv.x + c * uv.y);\n\n    vec4 col = draw(uv);\n    gl_FragColor = vec4(col.rgb * col.a, col.a);\n  }\n`;\n\n/** Internal scene component for the gradient fullscreen shader. */\nfunction GradientScene({\n  hue,\n  rotationSpeed,\n  noiseScale,\n  innerRadius,\n}: Required<Omit<GradientOrbConfig, \"background\">>) {\n  const materialRef = useRef<THREE.ShaderMaterial>(null);\n  const { size, viewport } = useThree();\n  const rotRef = useRef(0);\n  const lastTimeRef = useRef(0);\n\n  const geometry = useMemo(() => {\n    const geo = new THREE.BufferGeometry();\n    geo.setAttribute(\n      \"position\",\n      new THREE.Float32BufferAttribute([-1, -1, 0, 3, -1, 0, -1, 3, 0], 3),\n    );\n    geo.setAttribute(\"uv\", new THREE.Float32BufferAttribute([0, 0, 2, 0, 0, 2], 2));\n    return geo;\n  }, []);\n\n  useEffect(() => {\n    return () => geometry.dispose();\n  }, [geometry]);\n\n  const uniforms = useMemo(\n    () => ({\n      iTime: { value: 0 },\n      iResolution: { value: new THREE.Vector3(size.width, size.height, 1) },\n      hue: { value: hue },\n      rot: { value: 0 },\n      noiseScale: { value: noiseScale },\n      innerRadius: { value: innerRadius },\n    }),\n    // `size` is intentionally omitted — iResolution is mutated in-place each\n    // frame via useFrame rather than triggering a uniform object re-creation.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [hue, noiseScale, innerRadius],\n  );\n\n  useFrame((state) => {\n    if (!materialRef.current) return;\n\n    const t = state.clock.elapsedTime;\n    const dt = t - lastTimeRef.current;\n    lastTimeRef.current = t;\n\n    rotRef.current += dt * rotationSpeed;\n\n    const u = materialRef.current.uniforms;\n    if (!u.iTime || !u.hue || !u.rot || !u.iResolution) return;\n    u.iTime.value = t;\n    u.hue.value = hue;\n    u.rot.value = rotRef.current;\n    u.iResolution.value.set(\n      size.width * viewport.dpr,\n      size.height * viewport.dpr,\n      size.width / size.height,\n    );\n  });\n\n  return (\n    <mesh geometry={geometry} frustumCulled={false}>\n      <shaderMaterial\n        ref={materialRef}\n        vertexShader={vertexShader}\n        fragmentShader={fragmentShader}\n        uniforms={uniforms}\n        transparent\n        depthWrite={false}\n        depthTest={false}\n      />\n    </mesh>\n  );\n}\n\n/**\n * Glowing shader orb with noise-based 3-color mixing, YIQ hue rotation,\n * breathing pulse, and constant rotation — all rendered on GPU as a single\n * fullscreen triangle.\n *\n * @example\n * ```tsx\n * <GradientOrb />\n * <GradientOrb config={{ hue: 120, rotationSpeed: 0.5 }} />\n * ```\n */\nexport function GradientOrb({\n  config,\n  className = \"\",\n}: {\n  config?: GradientOrbConfig;\n  className?: string;\n}) {\n  const { background, hue, rotationSpeed, noiseScale, innerRadius } = {\n    ...defaults,\n    ...config,\n  };\n\n  return (\n    <div className={`w-full h-full ${className}`} style={{ background }}>\n      {/* Camera is unused — the vertex shader outputs clip-space positions directly */}\n      <Canvas gl={{ antialias: true, alpha: false }}>\n        <color attach=\"background\" args={[background]} />\n        <GradientScene\n          hue={hue}\n          rotationSpeed={rotationSpeed}\n          noiseScale={noiseScale}\n          innerRadius={innerRadius}\n        />\n      </Canvas>\n    </div>\n  );\n}\n","target":"uicapsule/gradient-orb/gradient-orb.tsx"},{"type":"registry:file","path":"/package.json","content":"{\n  \"name\": \"@uicapsule/gradient-orb\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"exports\": {\n    \"./preview\": \"./preview.tsx\"\n  },\n  \"scripts\": {\n    \"clean\": \"git clean -xdf .cache .turbo dist node_modules\"\n  },\n  \"dependencies\": {\n    \"@react-three/fiber\": \"^9.7.0\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"three\": \"^0.185.1\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"@types/three\": \"^0.185.4\"\n  }\n}\n","target":"uicapsule/gradient-orb/package.json"},{"type":"registry:file","path":"/preview.tsx","content":"\"use client\";\n\nimport { GradientOrb } from \"./gradient-orb\";\n\nconst Preview = () => {\n  return (\n    <div className=\"h-screen w-full\">\n      <GradientOrb />\n    </div>\n  );\n};\n\nexport default Preview;\n","target":"uicapsule/gradient-orb/preview.tsx"}]}