{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"An Orb component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useEffect, useRef, useState } from \"react\";\n\n/**\n * A configurable WebGL2 orb: a lit sphere with colour marbling inside it.\n *\n * Two ideas carry the whole thing.\n *\n * The loop is seamless by construction, not by crossfade. Every point walks a\n * circle in a flow field whose angle advances by exactly one turn per loop, so\n * the displacement returns to where it started. Neighbouring points begin at\n * different angles, and that is what reads as swirling — offset the whole\n * domain by one shared vector instead and you get a pulse.\n *\n * Colour is mixed in OkLab. The midpoint between two stops stays clean instead\n * of going muddy through sRGB, each picked colour reproduces exactly at its own\n * stop, and the hue ripple is a real rotation in the a/b plane.\n *\n * Everything else is lighting on a sphere impostor: the normal is derived per\n * pixel from the distance to the centre, which is what lets a rim, a highlight\n * and a shaded side exist at all. A radial gradient has no surface, and that is\n * the entire difference between this and a coloured circle.\n *\n * Contains two public-domain pieces of prior art, marked at their definitions:\n * the MurmurHash3 finalizer (Austin Appleby) and the OkLab transform matrices\n * (Björn Ottosson).\n */\n\nexport interface OrbProps {\n  /**\n   * Rim dispersion. Splits the palette lookup per channel toward the edge, the\n   * way a lens fringes.\n   */\n  aberration?: number;\n  /** Which colour dominates. `0.5` is even; below favours the first stop. */\n  balance?: number;\n  /** Interior cell size. Small is busy, large is one slow swell. */\n  blobScale?: number;\n  /** Hue ripple through the palette. */\n  chroma?: number;\n  className?: string;\n  /** Two to four stops. Two read as a material, four as iridescent. */\n  colors?: string[];\n  /** Lightness contrast around the midpoint. */\n  contrast?: number;\n  /** Seconds for one full, seamless loop. */\n  duration?: number;\n  /** How far the flow field carries the field over a loop. */\n  flow?: number;\n  /** A halo cast outside the silhouette. `0` is none. */\n  glow?: number;\n  /** Film grain. Also hides the banding large soft gradients show. */\n  grain?: number;\n  /** Internal scatter, offset away from the highlight. Fake subsurface. */\n  inner?: number;\n  /** Fresnel-driven hue rotation — thin-film sheen. */\n  iridescence?: number;\n  /** Light direction, x. */\n  lightX?: number;\n  /** Light direction, y. */\n  lightY?: number;\n  /** Freeze on the current frame. */\n  paused?: boolean;\n  /** Bends the noise lookup by the surface normal. */\n  refraction?: number;\n  /** Fresnel edge brightness. This is what reads as glass. */\n  rim?: number;\n  /** Blends the palette toward plain diffuse shading. */\n  shading?: number;\n  /** Rotates the palette. */\n  shift?: number;\n  /** Rendered size. Numbers are pixels. */\n  size?: number | string;\n  /** Edge feather. `0` still resolves to about 1.5 device pixels. */\n  softness?: number;\n  /** Specular highlight strength. */\n  specular?: number;\n  /** Strength of the second, nested warp — marbling rather than blobs. */\n  turbulence?: number;\n  /** Silhouette deformation, so the orb is a drop rather than a ball. */\n  wobble?: number;\n}\n\n/** The editor's own defaults, which are a good orb rather than a neutral one. */\nconst DEFAULTS = {\n  aberration: 0.8,\n  balance: 0.5,\n  blobScale: 3,\n  chroma: 1,\n  colors: [\"#f25aed\", \"#ffffff\"],\n  contrast: 1,\n  duration: 7,\n  flow: 0.6,\n  glow: 0,\n  grain: 0.6,\n  inner: 0.38,\n  iridescence: 0,\n  lightX: -0.6,\n  lightY: -1,\n  refraction: 0.25,\n  rim: 1.3,\n  shading: 0.05,\n  shift: 0.23,\n  softness: 0.005,\n  specular: 0.2,\n  turbulence: 0.4,\n  wobble: 0,\n} as const;\n\nconst MAX_STOPS = 4;\nconst MAX_DPR = 2;\nconst RGB_MAX = 255;\nconst SRGB_KNEE = 0.040_45;\nconst TAU = Math.PI * 2;\n/** Ambient motion gains nothing from 120 Hz. */\nconst FRAME_CAP_MS = 1000 / 60;\nconst LIGHT_Z = 0.65;\nconst BALANCE_BASE = 5;\n/** Keeps rendering slightly before the orb scrolls into view. */\nconst ROOT_MARGIN = \"120px\";\n\n/* The covering triangle is built from gl_VertexID, so there is no vertex\n   buffer and no attribute state to manage. */\nconst VERTEX = `#version 300 es\nvoid main() {\n  vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n  gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n}`;\n\nconst FRAGMENT = `#version 300 es\nprecision highp float;\nprecision highp int;   // the hash needs full 32-bit ints\n\nuniform vec2  uRes;\nuniform float uPhase;\nuniform vec3  uLab[4];\nuniform float uCount;\nuniform float uScaleN;\nuniform float uFlow;\nuniform float uTurb;\nuniform float uShift;\nuniform float uBalance;\nuniform float uChroma;\nuniform float uContrast;\nuniform float uRim;\nuniform float uSpec;\nuniform float uInner;\nuniform float uShading;\nuniform float uRefract;\nuniform float uIrid;\nuniform float uAberr;\nuniform float uGrain;\nuniform float uSoft;\nuniform float uWobble;\nuniform float uGlow;\nuniform vec3  uLight;\n\nout vec4 fragColor;\n\nconst float TAU = 6.28318530718;\nconst float RAD = 0.86;         // sphere radius in uv units; the rest is glow room\nconst float RMAX = 1.0 / RAD;   // radius reached at the nearest canvas edge\n\n/* MurmurHash3 finalizer, Austin Appleby, public domain. Exact at any lattice\n   coordinate, so unlike a fract(sin(...)) hash it never bands as the input\n   grows — which matters for the grain, whose input runs into the thousands. */\nuint hashU(uint x) {\n  x ^= x >> 16; x *= 0x85EBCA6Bu;\n  x ^= x >> 13; x *= 0xC2B2AE35u;\n  x ^= x >> 16;\n  return x;\n}\nuint hashU2(uvec2 p) { return hashU(p.x * 0x9E3779B9u ^ hashU(p.y)); }\nuint hashU3(uvec3 p) { return hashU(p.x * 0x9E3779B9u ^ hashU(p.y) ^ hashU(p.z) * 0x27D4EB2Fu); }\nfloat rand2(ivec2 p) { return float(hashU2(uvec2(p + 4096)) >> 8) / 16777216.0; }\nfloat rand3(ivec3 p) { return float(hashU3(uvec3(p + 4096)) >> 8) / 16777216.0; }\n\nfloat vnoise(vec2 p) {\n  ivec2 i = ivec2(floor(p));\n  vec2 f = fract(p);\n  vec2 u = f * f * (3.0 - 2.0 * f);\n  return mix(mix(rand2(i),               rand2(i + ivec2(1, 0)), u.x),\n             mix(rand2(i + ivec2(0, 1)), rand2(i + ivec2(1, 1)), u.x), u.y);\n}\n\nfloat fbm(vec2 p) {\n  float s = 0.0, a = 0.5;\n  for (int k = 0; k < 3; k++) { s += a * vnoise(p); p *= 2.03; a *= 0.5; }\n  return s / 0.875;\n}\n\n/* Each point walks its own circle, completing exactly one turn per loop. */\nvec2 flowField(vec2 p) {\n  float ang = TAU * vnoise(p + 11.3) + uPhase;\n  float mag = 0.35 + 0.65 * vnoise(p + 27.9);\n  return vec2(cos(ang), sin(ang)) * mag;\n}\n\n/* Fresnel-driven hue rotation in the a/b plane — thin-film sheen. */\nvec3 iridesce(vec3 lab, float f) {\n  float a = uIrid * f * -2.2;\n  float c = cos(a), s = sin(a);\n  return vec3(lab.x, c * lab.y - s * lab.z, s * lab.y + c * lab.z);\n}\n\nvec3 labRamp(float x) {\n  float xx = clamp(x, 0.0, 1.0) * (uCount - 1.0);\n  vec3 c = mix(uLab[0], uLab[1], clamp(xx, 0.0, 1.0));\n  c = mix(c, uLab[2], clamp(xx - 1.0, 0.0, 1.0));\n  c = mix(c, uLab[3], clamp(xx - 2.0, 0.0, 1.0));\n  return c;\n}\n\nvec3 paletteLab(float t) {\n  float x = pow(clamp(0.5 + 0.5 * cos(TAU * t), 0.0, 1.0), uBalance);\n  vec3 lab = labRamp(x);\n  float ang = TAU * (t * 2.0 + 0.123);\n  lab.yz += 0.17 * uChroma * length(lab.yz) * vec2(sin(ang), cos(ang * 1.37 + 1.1));\n  /* Cap to roughly the sRGB chroma ceiling. Scaling a/b keeps the hue angle;\n     overshooting and clamping RGB later posterises into flat patches. */\n  float ch = length(lab.yz);\n  if (ch > 0.33) { lab.yz *= 0.33 / ch; }\n  lab.x = clamp(0.5 + (lab.x - 0.5) * uContrast, 0.0, 1.0);\n  return lab;\n}\n\n/* OkLab transform, Björn Ottosson, public domain. */\nvec3 oklabToLinear(vec3 c) {\n  float l_ = c.x + 0.3963377774 * c.y + 0.2158037573 * c.z;\n  float m_ = c.x - 0.1055613458 * c.y - 0.0638541728 * c.z;\n  float s_ = c.x - 0.0894841775 * c.y - 1.2914855480 * c.z;\n  vec3 lms = vec3(l_ * l_ * l_, m_ * m_ * m_, s_ * s_ * s_);\n  return mat3( 4.0767416621, -1.2684380046, -0.0041960863,\n              -3.3077115913,  2.6097574011, -0.7034186147,\n               0.2309699292, -0.3413193965,  1.7076147010) * lms;\n}\n\nvec3 linearToSrgb(vec3 c) {\n  c = clamp(c, 0.0, 1.0);\n  vec3 lo = c * 12.92;\n  vec3 hi = 1.055 * pow(max(c, 1e-5), vec3(1.0 / 2.4)) - 0.055;\n  return mix(lo, hi, step(0.0031308, c));\n}\n\nvoid main() {\n  vec2 uv = (gl_FragCoord.xy * 2.0 - uRes) / min(uRes.x, uRes.y);\n  uv /= RAD;\n\n  /* Silhouette wobble. Integer harmonics of the angle keep it continuous all\n     the way round; integer multiples of the phase keep it periodic in time.\n     Faded in with radius so the rim deforms like a drop while the core stays\n     put — which also keeps it away from atan()'s singularity at the centre. */\n  if (uWobble > 0.0) {\n    float r0 = length(uv);\n    float th = atan(uv.y, uv.x + 1e-6);\n    float wob = 0.60 * sin(3.0 * th + uPhase)\n              + 0.40 * sin(5.0 * th - 2.0 * uPhase)\n              + 0.25 * sin(7.0 * th + 3.0 * uPhase);\n    uv *= 1.0 - uWobble * 0.055 * wob * smoothstep(0.0, 0.5, r0);\n  }\n\n  float r = length(uv);\n  vec2 pd = uv / max(r, 1.0);\n  float z = sqrt(max(1.0 - dot(pd, pd), 0.0));\n  vec3 n = vec3(pd, z);\n\n  /* Domain warping. The base fBm is static; all the motion comes from warping\n     its input through the looping flow field, and nesting a second warp is\n     what turns smooth blobs into marbled, ink-in-water flow. */\n  vec2 p0 = (pd - n.xy * uRefract) * uScaleN;\n  vec2 p1 = p0 + uFlow * flowField(p0);\n  vec2 p2 = p1 + uTurb * flowField(p1 * 1.7 + 5.2);\n  float t = mix(0.5, fbm(p2 + 3.7), 1.35) + uShift;\n\n  float fres3 = pow(1.0 - n.z, 3.0);\n  vec3 lab = iridesce(paletteLab(t), fres3);\n\n  vec3 base;\n  if (uAberr > 0.001) {\n    // Dispersion: the palette lookup shifts per channel, strongest at the rim.\n    float d = uAberr * 0.05 * r * r;\n    base = vec3(oklabToLinear(iridesce(paletteLab(t - d), fres3)).r,\n                oklabToLinear(lab).g,\n                oklabToLinear(iridesce(paletteLab(t + d), fres3)).b);\n  } else {\n    base = oklabToLinear(lab);\n  }\n  base = max(base, 0.0);\n\n  // Lighting is added in linear light, which is where it belongs.\n  vec3 L = normalize(uLight);\n  vec3 col = mix(base, vec3(clamp(dot(n, L), 0.0, 1.0)), uShading);\n\n  // Internal scatter offset away from the highlight: a translucent body rather\n  // than a lit opaque ball.\n  vec2 op = pd + L.xy * 0.45;\n  col += uInner * exp(-dot(op, op) * 2.2) * base;\n\n  vec3 half_ = normalize(L + vec3(0.0, 0.0, 1.0));\n  col += pow(1.0 - n.z, 8.0) * uRim\n       + pow(max(dot(n, half_), 0.0), 24.3) * uSpec;\n\n  vec3 lit = linearToSrgb(col);\n  vec3 flat_ = linearToSrgb(base);\n\n  // Never thinner than ~1.5 device px, so the rim stays smooth when the\n  // character is rendered small.\n  float w = max(uSoft, 1.5 * fwidth(r));\n  float body = 1.0 - smoothstep(1.0 - w, 1.0, r);\n\n  /* The halo has to reach exactly zero before the canvas boundary. Left alone\n     it is still ~1.5/255 at the edge and gets cut off square, which shows the\n     canvas box as a faint rectangle around the orb. */\n  float glowF = uGlow * exp(-max(r - 1.0, 0.0) * 20.0);\n  glowF *= 1.0 - smoothstep(0.45, 1.0, (r - 1.0) / (RMAX - 1.0));\n  float alpha = clamp(body + glowF * (1.0 - body), 0.0, 1.0);\n\n  vec3 outCol = mix(flat_, lit, body);\n\n  // Grain plus an always-on deband dither, stepped to 24 frames per loop so it\n  // animates without breaking the seam.\n  int frame = int(uPhase / TAU * 24.0);\n  outCol += (rand3(ivec3(ivec2(gl_FragCoord.xy), frame)) - 0.5)\n          * (uGrain * 0.1 + 1.0 / 255.0);\n\n  outCol = clamp(outCol, 0.0, 1.0);\n  fragColor = vec4(outCol * alpha, alpha);   // premultiplied\n}`;\n\n/**\n * Any CSS colour to OkLab, in two steps because neither alone is enough.\n *\n * `getComputedStyle` resolves `var()` and named colours, but Chrome hands back\n * `oklch(0.72 0.21 322)` verbatim rather than converting it — read that as three\n * sRGB channels and a magenta comes out blue. A 1x1 2D canvas rasterises\n * whatever syntax survives into actual pixels, which is the only step that\n * cannot lie about the result.\n */\nconst cssToOklab = (input: string): [number, number, number] => {\n  const probe = document.createElement(\"span\");\n  probe.style.color = input;\n  document.body.appendChild(probe);\n  const resolved = getComputedStyle(probe).color;\n  probe.remove();\n\n  const surface = document.createElement(\"canvas\");\n  surface.width = 1;\n  surface.height = 1;\n  const ctx = surface.getContext(\"2d\", { willReadFrequently: true });\n  if (!ctx) {\n    return [1, 0, 0];\n  }\n  ctx.fillStyle = resolved;\n  ctx.fillRect(0, 0, 1, 1);\n  const [r8, g8, b8] = ctx.getImageData(0, 0, 1, 1).data;\n\n  // biome-ignore-start lint/style/useNumericSeparators: published colour-space constants, kept digit-for-digit against the reference\n  const [r, g, b] = [r8, g8, b8].map((channel) => {\n    const u = channel / RGB_MAX;\n    return u <= SRGB_KNEE ? u / 12.92 : ((u + 0.055) / 1.055) ** 2.4;\n  });\n  const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);\n  const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);\n  const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);\n  return [\n    0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,\n    1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,\n    0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,\n  ];\n  // biome-ignore-end lint/style/useNumericSeparators: published colour-space constants\n};\n\nconst compileShader = (\n  gl: WebGL2RenderingContext,\n  type: number,\n  source: string\n) => {\n  const shader = gl.createShader(type);\n  if (!shader) {\n    return null;\n  }\n  gl.shaderSource(shader, source);\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\nexport default function Orb({ className, size = 240, ...rest }: OrbProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  // The fallback lives here rather than in the caller: a stand-in drawn by\n  // whoever embeds the orb ends up painted over the canvas, which hides the\n  // real thing on every machine that can render it.\n  const [supported, setSupported] = useState(true);\n\n  // Read inside the loop rather than closed over, so turning a knob pushes a\n  // uniform instead of tearing down and rebuilding the GL context.\n  const syncConfig = useRef<(() => void) | null>(null);\n  const config = useRef({ ...DEFAULTS, ...rest });\n  config.current = { ...DEFAULTS, ...rest };\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) {\n      return;\n    }\n    const gl = canvas.getContext(\"webgl2\", {\n      alpha: true,\n      antialias: false, // a covering triangle has no geometric edges to sample\n      depth: false,\n      powerPreference: \"low-power\",\n      premultipliedAlpha: true,\n      stencil: false,\n    });\n    if (!gl) {\n      setSupported(false);\n      return;\n    }\n\n    const vs = compileShader(gl, gl.VERTEX_SHADER, VERTEX);\n    const fs = compileShader(gl, gl.FRAGMENT_SHADER, FRAGMENT);\n    const program = gl.createProgram();\n    if (!(vs && fs && program)) {\n      setSupported(false);\n      return;\n    }\n    gl.attachShader(program, vs);\n    gl.attachShader(program, fs);\n    gl.linkProgram(program);\n    gl.deleteShader(vs);\n    gl.deleteShader(fs);\n    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n      setSupported(false);\n      return;\n    }\n    // biome-ignore lint/correctness/useHookAtTopLevel: WebGL2RenderingContext.useProgram is not a React hook.\n    gl.useProgram(program);\n    // Recoverable: a failed first attempt must not disable the orb forever.\n    setSupported(true);\n    gl.disable(gl.DEPTH_TEST);\n    gl.disable(gl.BLEND); // the shader writes premultiplied RGBA straight out\n\n    const at = (name: string) => gl.getUniformLocation(program, name);\n    const u = {\n      aberr: at(\"uAberr\"),\n      balance: at(\"uBalance\"),\n      chroma: at(\"uChroma\"),\n      contrast: at(\"uContrast\"),\n      count: at(\"uCount\"),\n      flow: at(\"uFlow\"),\n      glow: at(\"uGlow\"),\n      grain: at(\"uGrain\"),\n      inner: at(\"uInner\"),\n      irid: at(\"uIrid\"),\n      lab: at(\"uLab\"),\n      light: at(\"uLight\"),\n      phase: at(\"uPhase\"),\n      refract: at(\"uRefract\"),\n      res: at(\"uRes\"),\n      rim: at(\"uRim\"),\n      scaleN: at(\"uScaleN\"),\n      shading: at(\"uShading\"),\n      shift: at(\"uShift\"),\n      soft: at(\"uSoft\"),\n      spec: at(\"uSpec\"),\n      turb: at(\"uTurb\"),\n    };\n\n    const pushConfig = () => {\n      const c = config.current;\n      const stops = c.colors.slice(0, MAX_STOPS);\n      const lab = new Float32Array(MAX_STOPS * 3);\n      for (let i = 0; i < MAX_STOPS; i++) {\n        const [x, y, z] = cssToOklab(stops[Math.min(i, stops.length - 1)]);\n        lab[i * 3] = x;\n        lab[i * 3 + 1] = y;\n        lab[i * 3 + 2] = z;\n      }\n      gl.uniform3fv(u.lab, lab);\n      gl.uniform1f(u.count, Math.max(2, Math.min(stops.length, MAX_STOPS)));\n      // The prop reads as blob size, so invert it into cell frequency.\n      gl.uniform1f(u.scaleN, 1.45 / Math.max(c.blobScale, 0.05));\n      gl.uniform1f(u.flow, c.flow);\n      gl.uniform1f(u.turb, c.turbulence);\n      gl.uniform1f(u.shift, c.shift);\n      gl.uniform1f(u.balance, BALANCE_BASE ** ((0.5 - c.balance) * 2));\n      gl.uniform1f(u.chroma, c.chroma);\n      gl.uniform1f(u.contrast, c.contrast);\n      gl.uniform1f(u.rim, c.rim);\n      gl.uniform1f(u.spec, c.specular);\n      gl.uniform1f(u.inner, c.inner);\n      gl.uniform1f(u.shading, c.shading);\n      gl.uniform1f(u.refract, c.refraction);\n      gl.uniform1f(u.irid, c.iridescence);\n      gl.uniform1f(u.aberr, c.aberration);\n      gl.uniform1f(u.grain, c.grain);\n      gl.uniform1f(u.soft, c.softness);\n      gl.uniform1f(at(\"uWobble\"), c.wobble);\n      gl.uniform1f(u.glow, c.glow);\n      gl.uniform3f(u.light, c.lightX, c.lightY, LIGHT_Z);\n    };\n\n    const resize = (entry?: ResizeObserverEntry) => {\n      const box = entry?.devicePixelContentBoxSize;\n      let w: number;\n      let h: number;\n      if (box?.length) {\n        w = box[0].inlineSize;\n        h = box[0].blockSize;\n      } else {\n        const dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);\n        const rect = canvas.getBoundingClientRect();\n        w = Math.round(rect.width * dpr);\n        h = Math.round(rect.height * dpr);\n      }\n      w = Math.max(1, w);\n      h = Math.max(1, h);\n      if (canvas.width === w && canvas.height === h) {\n        return false;\n      }\n      canvas.width = w;\n      canvas.height = h;\n      gl.viewport(0, 0, w, h);\n      gl.uniform2f(u.res, w, h);\n      return true;\n    };\n\n    const draw = () => {\n      gl.clearColor(0, 0, 0, 0);\n      gl.clear(gl.COLOR_BUFFER_BIT);\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n    };\n\n    let phase = 0;\n    let pending = 0;\n    let last = 0;\n    let raf = 0;\n    let running = false;\n    let onScreen = true;\n    const reduce = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n\n    const tick = (now: number) => {\n      raf = requestAnimationFrame(tick);\n      const dt = last ? Math.min(now - last, 100) : FRAME_CAP_MS;\n      last = now;\n      pending += dt;\n      /* Half a frame of slack: without it, a display whose vsync divides the\n         cap almost exactly drops an extra frame and the cadence stutters. */\n      if (pending < FRAME_CAP_MS - dt * 0.5) {\n        return;\n      }\n      phase = (phase + pending / 1000 / config.current.duration) % 1;\n      pending = 0;\n      gl.uniform1f(u.phase, phase * TAU);\n      draw();\n    };\n\n    const start = () => {\n      if (running || reduce.matches || config.current.paused || !onScreen) {\n        return;\n      }\n      running = true;\n      last = 0;\n      pending = 0;\n      raf = requestAnimationFrame(tick);\n    };\n\n    const stop = () => {\n      running = false;\n      if (raf) {\n        cancelAnimationFrame(raf);\n      }\n      raf = 0;\n    };\n\n    const applyMotionPreference = () => {\n      if (reduce.matches || config.current.paused) {\n        stop();\n        if (reduce.matches) {\n          gl.uniform1f(u.phase, 0);\n        }\n        draw();\n        return;\n      }\n      start();\n    };\n\n    syncConfig.current = () => {\n      pushConfig();\n      applyMotionPreference();\n      if (!running) {\n        draw();\n      }\n    };\n\n    pushConfig();\n    resize();\n    gl.uniform1f(u.phase, 0);\n    draw();\n\n    const observer = new ResizeObserver((entries) => {\n      if (resize(entries[0]) && !running) {\n        draw();\n      }\n    });\n    try {\n      observer.observe(canvas, { box: \"device-pixel-content-box\" });\n    } catch {\n      observer.observe(canvas);\n    }\n\n    /* Scrolled out of view is the common case. Nothing is gained by burning\n       GPU on an orb nobody can see. */\n    const seen = new IntersectionObserver(\n      (entries) => {\n        onScreen = entries.at(-1)?.isIntersecting ?? true;\n        if (onScreen) {\n          start();\n        } else {\n          stop();\n        }\n      },\n      { rootMargin: ROOT_MARGIN }\n    );\n    seen.observe(canvas);\n\n    reduce.addEventListener(\"change\", applyMotionPreference);\n    applyMotionPreference();\n\n    return () => {\n      syncConfig.current = null;\n      stop();\n      observer.disconnect();\n      seen.disconnect();\n      reduce.removeEventListener(\"change\", applyMotionPreference);\n      gl.deleteProgram(program);\n      /* Deliberately NOT calling WEBGL_lose_context here. React remounts\n         effects in development, and the second pass gets the same canvas back\n         with a context that is still lost — the program then fails to link and\n         the component latches into its fallback for good. Dropping the canvas\n         is enough; the context goes with it. */\n    };\n  }, []);\n\n  // Synchronize committed props even when animation is paused or reduced.\n  // Keep the GPU resources stable; never convert colors on every frame.\n  useEffect(() => {\n    syncConfig.current?.();\n  });\n\n  const dimension = typeof size === \"number\" ? `${size}px` : size;\n\n  if (!supported) {\n    const stops = (rest.colors ?? DEFAULTS.colors).slice(0, MAX_STOPS);\n    return (\n      <div\n        aria-hidden=\"true\"\n        className={cn(\"block rounded-full\", className)}\n        style={{\n          // Lit from the same corner as the shader, so the character does not\n          // change which way it faces when it falls back.\n          backgroundImage: `radial-gradient(circle at 32% 28%, ${stops.join(\", \")})`,\n          height: dimension,\n          width: dimension,\n        }}\n      />\n    );\n  }\n\n  return (\n    <canvas\n      aria-hidden=\"true\"\n      className={cn(\"block\", className)}\n      ref={canvasRef}\n      style={{ height: dimension, width: dimension }}\n      tabIndex={-1}\n    />\n  );\n}\n","path":"index.tsx","target":"components/smoothui/orb/index.tsx","type":"registry:ui"}],"name":"orb","registryDependencies":[],"title":"Orb","type":"registry:ui"}