{"$schema":"https://ui.shadcn.com/schema/registry.json","author":"Kaiyu Hsu <uicapsule@kyh.io>","categories":["effects","geometric"],"dependencies":[],"description":"Liquid-glass shader orb that morphs through material presets — siri, aurora, plasma, chrome, opal, blue drop, violet ember","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\n// The flow fields and glass shell are ported from LerSent001/orb (MIT),\n// rewritten from WebGPU/WGSL to WebGL2/GLSL so they run everywhere, with a\n// morph pipeline added on top: every material parameter tweens between\n// presets while the two flow fields crossfade, so the orb transforms\n// instead of cutting.\n\nexport type LiquidOrbPreset =\n  | \"siri\"\n  | \"chrome\"\n  | \"aurora\"\n  | \"violetEmber\"\n  | \"blueDrop\"\n  | \"opal\"\n  | \"plasma\";\n\n/** Display names for each preset, for captions and controls. */\nexport const presetLabel = {\n  aurora: \"Aurora\",\n  blueDrop: \"Blue Drop\",\n  chrome: \"Chrome\",\n  opal: \"Opal\",\n  plasma: \"Plasma\",\n  siri: \"Siri\",\n  violetEmber: \"Violet Ember\",\n} satisfies Record<LiquidOrbPreset, string>;\n\n/** Cycle order — adjacent presets are chosen for maximum material contrast. */\nexport const presetOrder: readonly LiquidOrbPreset[] = [\n  \"siri\",\n  \"chrome\",\n  \"aurora\",\n  \"violetEmber\",\n  \"blueDrop\",\n  \"opal\",\n  \"plasma\",\n];\n\n/**\n * Configuration options for the liquid orb.\n * All fields are optional and fall back to sensible defaults.\n */\nexport interface LiquidOrbConfig {\n  /** Preset to start on. @default \"siri\" */\n  preset?: LiquidOrbPreset;\n  /** Continuously morph through every preset. @default true */\n  cycle?: boolean;\n  /** Seconds each preset holds before morphing. @default 1.8 */\n  holdDuration?: number;\n  /** Seconds a morph between presets takes. @default 1.25 */\n  morphDuration?: number;\n  /** Global time multiplier on top of each preset's own speed. @default 1 */\n  speed?: number;\n  /** Refractive glass shell with chromatic edge dispersion. @default true */\n  glass?: boolean;\n  /** Halo strength past the orb's limb, in the preset's glow color. @default 0.55 */\n  glow?: number;\n  /** Fired when a morph toward a new preset begins. */\n  onPresetChange?: (preset: LiquidOrbPreset) => void;\n}\n\ninterface PresetSpec {\n  speed: number;\n  radius: number;\n  contourDeform: number;\n  zoom: number;\n  warp: number;\n  ridgeAmt: number;\n  sharp: number;\n  shade: number;\n  sheen: number;\n  gloss: number;\n  glassOpacity: number;\n  shellMidAlpha: number;\n  shellEdgeAlpha: number;\n  exposure: number;\n  colorA: string;\n  colorB: string;\n  colorC: string;\n  colorD: string;\n  highlight: string;\n  shellInner: string;\n  shellMid: string;\n  shellEdge: string;\n  sheenColor: string;\n  specColor: string;\n  canvasColor: string;\n  glowColor: string;\n}\n\n// Preset values carried over verbatim from the source editor's style bank.\nconst base = {\n  contourDeform: 0,\n  highlight: \"#FFFFFF\",\n  radius: 0.72,\n  sheenColor: \"#EAF4FF\",\n  shellInner: \"#FFFFFF\",\n  specColor: \"#DCEAFF\",\n};\n\nconst presets = {\n  aurora: {\n    ...base,\n    canvasColor: \"#010207\",\n    colorA: \"#030816\",\n    colorB: \"#20F0B6\",\n    colorC: \"#32A8FF\",\n    colorD: \"#A34BFF\",\n    contourDeform: 0.08,\n    exposure: 1.18,\n    glassOpacity: 0.42,\n    gloss: 0.28,\n    glowColor: \"#20F0B6\",\n    ridgeAmt: 0.62,\n    shade: 0.18,\n    sharp: 2.1,\n    sheen: 0.36,\n    shellEdge: \"#20F0B6\",\n    shellEdgeAlpha: 0.22,\n    shellMid: \"#32A8FF\",\n    shellMidAlpha: 0.2,\n    speed: 3,\n    warp: 4.2,\n    zoom: 0.4,\n  },\n  blueDrop: {\n    ...base,\n    canvasColor: \"#010207\",\n    colorA: \"#020B1D\",\n    colorB: \"#0756B8\",\n    colorC: \"#1EC8FF\",\n    colorD: \"#DDFBFF\",\n    contourDeform: 0.08,\n    exposure: 1.24,\n    glassOpacity: 0.66,\n    gloss: 0.42,\n    glowColor: \"#168DFF\",\n    highlight: \"#EAFBFF\",\n    radius: 0.74,\n    ridgeAmt: 0.42,\n    shade: 0.16,\n    sharp: 2.4,\n    sheen: 0.22,\n    sheenColor: \"#DDFBFF\",\n    shellEdge: \"#466DFF\",\n    shellEdgeAlpha: 0.24,\n    shellInner: \"#F6FDFF\",\n    shellMid: \"#4FD7FF\",\n    shellMidAlpha: 0.32,\n    specColor: \"#A8D9FF\",\n    speed: 0.9,\n    warp: 2.65,\n    zoom: 0.48,\n  },\n  chrome: {\n    ...base,\n    canvasColor: \"#050608\",\n    colorA: \"#FFFFFF\",\n    colorB: \"#B9C0CA\",\n    colorC: \"#343A43\",\n    colorD: \"#030405\",\n    exposure: 1.08,\n    glassOpacity: 0.42,\n    gloss: 0.28,\n    glowColor: \"#FFFFFF\",\n    ridgeAmt: 0.44,\n    shade: 0.58,\n    sharp: 5.2,\n    sheen: 0.36,\n    shellEdge: \"#FFFFFF\",\n    shellEdgeAlpha: 0.22,\n    shellMid: \"#B9C0CA\",\n    shellMidAlpha: 0.2,\n    speed: 2,\n    warp: 3.8,\n    zoom: 0.36,\n  },\n  opal: {\n    ...base,\n    canvasColor: \"#07080D\",\n    colorA: \"#FFF6E8\",\n    colorB: \"#6EF2CF\",\n    colorC: \"#FF91D8\",\n    colorD: \"#756BFF\",\n    exposure: 1.12,\n    glassOpacity: 0.38,\n    gloss: 0.26,\n    glowColor: \"#9E8CFF\",\n    ridgeAmt: 0.36,\n    shade: 0.1,\n    sharp: 2,\n    sheen: 0.3,\n    shellEdge: \"#D9C8FF\",\n    shellEdgeAlpha: 0.2,\n    shellMid: \"#CDE5FF\",\n    shellMidAlpha: 0.2,\n    speed: 1.5,\n    warp: 2.8,\n    zoom: 0.3,\n  },\n  plasma: {\n    ...base,\n    canvasColor: \"#020105\",\n    colorA: \"#06020E\",\n    colorB: \"#0099FF\",\n    colorC: \"#258BFF\",\n    colorD: \"#1375FF\",\n    contourDeform: 0.05,\n    exposure: 1.25,\n    glassOpacity: 0.42,\n    gloss: 0.28,\n    glowColor: \"#0099FF\",\n    ridgeAmt: 0.78,\n    shade: 0.16,\n    sharp: 4.2,\n    sheen: 0.36,\n    shellEdge: \"#00E9FF\",\n    shellEdgeAlpha: 0.22,\n    shellInner: \"#FFFFFF\",\n    shellMid: \"#1951C2\",\n    shellMidAlpha: 0.2,\n    speed: 1.32,\n    warp: 5.4,\n    zoom: 0.55,\n  },\n  siri: {\n    ...base,\n    canvasColor: \"#030409\",\n    colorA: \"#FFD86B\",\n    colorB: \"#82F4FF\",\n    colorC: \"#FF7BD5\",\n    colorD: \"#8E6CFF\",\n    exposure: 2,\n    glassOpacity: 0.44,\n    gloss: 0.24,\n    glowColor: \"#956CFF\",\n    ridgeAmt: 0.5,\n    shade: 0.12,\n    sharp: 2.2,\n    sheen: 0.28,\n    shellEdge: \"#C5A9FF\",\n    shellEdgeAlpha: 0.18,\n    shellMid: \"#9BF4FF\",\n    shellMidAlpha: 0.18,\n    speed: 0.82,\n    warp: 3.2,\n    zoom: 0.36,\n  },\n  violetEmber: {\n    ...base,\n    canvasColor: \"#030006\",\n    colorA: \"#100016\",\n    colorB: \"#4A0E8F\",\n    colorC: \"#A52EFF\",\n    colorD: \"#F1A7FF\",\n    contourDeform: 0.04,\n    exposure: 1.28,\n    glassOpacity: 0.62,\n    gloss: 0.34,\n    glowColor: \"#A52EFF\",\n    highlight: \"#FFD6FF\",\n    ridgeAmt: 0.73,\n    shade: 0.18,\n    sharp: 3.3,\n    sheen: 0.2,\n    sheenColor: \"#F8E6FF\",\n    shellEdge: \"#6C2DFF\",\n    shellEdgeAlpha: 0.24,\n    shellInner: \"#FCF5FF\",\n    shellMid: \"#C257FF\",\n    shellMidAlpha: 0.28,\n    specColor: \"#D4B7FF\",\n    speed: 1.12,\n    warp: 4.7,\n    zoom: 0.58,\n  },\n} satisfies Record<LiquidOrbPreset, PresetSpec>;\n\n// Style indices the shader dispatches on — must match presetFluid() below.\nconst styleIndex = {\n  aurora: 1,\n  blueDrop: 5,\n  chrome: 3,\n  opal: 4,\n  plasma: 2,\n  siri: 0,\n  violetEmber: 6,\n} satisfies Record<LiquidOrbPreset, number>;\n\nconst scalarKeys = [\n  \"speed\",\n  \"radius\",\n  \"contourDeform\",\n  \"zoom\",\n  \"warp\",\n  \"ridgeAmt\",\n  \"sharp\",\n  \"shade\",\n  \"sheen\",\n  \"gloss\",\n  \"glassOpacity\",\n  \"shellMidAlpha\",\n  \"shellEdgeAlpha\",\n  \"exposure\",\n] as const;\n\nconst colorKeys = [\n  \"colorA\",\n  \"colorB\",\n  \"colorC\",\n  \"colorD\",\n  \"highlight\",\n  \"shellInner\",\n  \"shellMid\",\n  \"shellEdge\",\n  \"sheenColor\",\n  \"specColor\",\n  \"canvasColor\",\n  \"glowColor\",\n] as const;\n\n// Uniform names aligned with scalarKeys[1..] (speed stays CPU-side).\nconst scalarUniforms = [\n  \"uRadius\",\n  \"uContourDeform\",\n  \"uZoom\",\n  \"uWarp\",\n  \"uRidgeAmt\",\n  \"uSharp\",\n  \"uShade\",\n  \"uSheen\",\n  \"uGloss\",\n  \"uGlassOpacity\",\n  \"uShellMidAlpha\",\n  \"uShellEdgeAlpha\",\n  \"uExposure\",\n] as const;\n\nconst colorUniforms = [\n  \"uColorA\",\n  \"uColorB\",\n  \"uColorC\",\n  \"uColorD\",\n  \"uHighlight\",\n  \"uShellInner\",\n  \"uShellMid\",\n  \"uShellEdge\",\n  \"uSheenColor\",\n  \"uSpecColor\",\n  \"uCanvasColor\",\n  \"uGlowColor\",\n] as const;\n\nconst COLOR_BASE = scalarKeys.length;\nconst VEC_SIZE = COLOR_BASE + colorKeys.length * 3;\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n  const v = hex.slice(1);\n  return [\n    Number.parseInt(v.slice(0, 2), 16) / 255,\n    Number.parseInt(v.slice(2, 4), 16) / 255,\n    Number.parseInt(v.slice(4, 6), 16) / 255,\n  ];\n};\n\nconst presetVec = (spec: PresetSpec): Float32Array => {\n  const out = new Float32Array(VEC_SIZE);\n  for (const [i, key] of scalarKeys.entries()) {\n    out[i] = spec[key];\n  }\n  for (const [i, key] of colorKeys.entries()) {\n    out.set(hexToRgb(spec[key]), COLOR_BASE + i * 3);\n  }\n  return out;\n};\n\nconst presetVecs = new Map(presetOrder.map((name) => [name, presetVec(presets[name])]));\n\nconst vertexSrc = `#version 300 es\nvoid main() {\n  vec2 v = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n  gl_Position = vec4(v * 2.0 - 1.0, 0.0, 1.0);\n}\n`;\n\n// The style pair is compiled in via STYLE_A/STYLE_B defines rather than\n// dispatched on uniforms: software rasterizers (SwiftShader) execute every\n// branch of a uniform if-chain per pixel, which made one style cost as much\n// as all seven. Specialized programs are compiled lazily per cycle segment\n// and cached.\nconst fragmentBody = `precision highp float;\n\nuniform vec2 uSize;\nuniform float uTime;\nuniform float uStyleMix;\nuniform float uGlass;\nuniform float uGlow;\n\nuniform float uRadius;\nuniform float uContourDeform;\nuniform float uZoom;\nuniform float uWarp;\nuniform float uRidgeAmt;\nuniform float uSharp;\nuniform float uShade;\nuniform float uSheen;\nuniform float uGloss;\nuniform float uGlassOpacity;\nuniform float uShellMidAlpha;\nuniform float uShellEdgeAlpha;\nuniform float uExposure;\n\nuniform vec3 uColorA;\nuniform vec3 uColorB;\nuniform vec3 uColorC;\nuniform vec3 uColorD;\nuniform vec3 uHighlight;\nuniform vec3 uShellInner;\nuniform vec3 uShellMid;\nuniform vec3 uShellEdge;\nuniform vec3 uSheenColor;\nuniform vec3 uSpecColor;\nuniform vec3 uCanvasColor;\nuniform vec3 uGlowColor;\n\nout vec4 outColor;\n\nconst float GL_KA = 6.0;\nconst float GL_KG = 4.1209;\nconst float GL_KR = 0.32;\nconst float GL_GH = 1.73205081;\nconst float GL_CLEAR_EA = 0.995;\nconst float GL_CLEAR_EB = 1.04;\n\n// ── liquid noise bank ───────────────────────────────────────────────────────\nfloat lqHash(vec2 p) {\n  p = fract(p * vec2(123.34, 456.21));\n  p += dot(p, p + 45.32);\n  return fract(p.x * p.y);\n}\n\nfloat lqNoise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  f = f * f * (3.0 - 2.0 * f);\n  return mix(mix(lqHash(i), lqHash(i + vec2(1.0, 0.0)), f.x),\n             mix(lqHash(i + vec2(0.0, 1.0)), lqHash(i + vec2(1.0, 1.0)), f.x), f.y);\n}\n\n// Pre-blurred fbm: .x the attenuated value, .y the standard deviation of the\n// detail the attenuation removed, for nonlinearities to integrate back out.\nvec2 lqFbm(vec2 p, float bs) {\n  float s = 0.0;\n  float a = 0.5;\n  float m = 0.0;\n  float vr = 0.0;\n  float e = -GL_KA * bs * bs;\n  float g = 1.0;\n  for (int i = 0; i < 5; i++) {\n    float b = exp(e * g);\n    s += a * (0.5 + b * (lqNoise(p) - 0.5));\n    vr += a * a * (1.0 - b * b);\n    m += a;\n    a *= 0.5;\n    g *= GL_KG;\n    p = vec2(0.8 * p.x - 0.6 * p.y, 0.6 * p.x + 0.8 * p.y) * 2.03;\n  }\n  return vec2(s / m, GL_KR * sqrt(vr) / m);\n}\n\nfloat lqRidge(float v, float k) {\n  return pow(clamp(1.0 - abs(v * 2.0 - 1.0), 0.0, 1.0), k);\n}\n\n// Three-point Gauss-Hermite over the removed detail, so ridged filaments\n// spread as they dim instead of just dimming.\nfloat lqRidgeS(vec2 vs, float k) {\n  float d = GL_GH * vs.y;\n  return (lqRidge(vs.x - d, k) + 4.0 * lqRidge(vs.x, k) + lqRidge(vs.x + d, k)) / 6.0;\n}\n\nvec3 lqRamp(float v, vec3 cA, vec3 cB, vec3 cC, vec3 cD) {\n  vec3 c = mix(cA, cB, smoothstep(0.0, 0.45, v));\n  c = mix(c, cC, smoothstep(0.38, 0.72, v));\n  c = mix(c, cD, smoothstep(0.68, 1.0, v));\n  return c;\n}\n\nvec2 rot2(vec2 p, float angle) {\n  float c = cos(angle);\n  float s = sin(angle);\n  return vec2(c * p.x - s * p.y, s * p.x + c * p.y);\n}\n\nvec3 finishPreset(vec3 colorIn, vec2 p) {\n  vec3 color = colorIn;\n  color = mix(color, uHighlight, uShade * 0.22 * smoothstep(0.15, 1.15, dot(p, vec2(-0.32, 0.78))));\n  color *= 1.0 - uShade * 0.34 * smoothstep(-0.1, 1.2, dot(p, vec2(0.45, -0.62)));\n  color *= 1.0 - uShade * 0.22 * smoothstep(0.72, 1.08, length(p));\n  return clamp(color, 0.0, 1.0);\n}\n\nvec3 finishEmission(vec3 colorIn, vec2 p) {\n  vec3 color = colorIn;\n  if (uGlass > 0.5) {\n    color = mix(color, uHighlight, uShade * 0.22 * smoothstep(0.15, 1.15, dot(p, vec2(-0.32, 0.78))));\n  }\n  color *= 1.0 - uShade * 0.34 * smoothstep(-0.1, 1.2, dot(p, vec2(0.45, -0.62)));\n  color *= 1.0 - uShade * 0.22 * smoothstep(0.72, 1.08, length(p));\n  return clamp(color, 0.0, 1.0);\n}\n\n// ── preset flow fields ──────────────────────────────────────────────────────\nvec2 siriBand(vec2 q, float drift, float phaseOffset, float amplitude,\n              float mainY, float envelope, float softness) {\n  float y = amplitude * envelope * sin(q.x + drift + phaseOffset);\n  float distanceToLine = abs(q.y - y);\n  float line = 0.018 / (sqrt(distanceToLine * distanceToLine + softness * softness) + 0.026);\n  float bandDistance = max(0.0, max(q.y - max(mainY, y), min(mainY, y) - q.y));\n  float band = 0.018 / (bandDistance + 0.075);\n  return vec2(line, band);\n}\n\nvec3 siriFluid(vec2 p, float t) {\n  float scale = 0.74 + uZoom * 0.34;\n  vec2 q = p / scale;\n  float envelopeBase = cos(1.57079633 * min(abs(0.9 * q.x), 1.0));\n  float envelope = envelopeBase * envelopeBase;\n  float low = 0.5 + 0.5 * cos(t * 0.37);\n  float mid = 0.5 + 0.5 * sin(t * 0.51 + 1.2);\n  float high = 0.5 + 0.5 * cos(t * 0.73 + 2.1);\n  float drift = t * 2.4;\n  float mainAmplitude = 0.25 + uRidgeAmt * 0.075 + low * 0.018;\n  float bandAmplitude = mainAmplitude + mid * 0.025 + high * 0.018;\n  float mainY = mainAmplitude * envelope * sin(q.x * 1.1 + drift);\n  float separation = 1.85 + uWarp * 0.2 + mid * 0.28;\n  float softness = 0.035 + (1.0 - uRidgeAmt) * 0.018 + mid * 0.006;\n  vec2 band0 = siriBand(q, drift, -separation, bandAmplitude, mainY, envelope, softness);\n  vec2 band1 = siriBand(q, drift, -separation * 0.34, bandAmplitude, mainY, envelope, softness);\n  vec2 band2 = siriBand(q, drift, separation * 0.34, bandAmplitude, mainY, envelope, softness);\n  vec2 band3 = siriBand(q, drift, separation, bandAmplitude, mainY, envelope, softness);\n  float w0 = band0.x + band0.y;\n  float w1 = band1.x + band1.y;\n  float w2 = band2.x + band2.y;\n  float w3 = band3.x + band3.y;\n  float total = w0 + w1 + w2 + w3;\n  float d0 = w0 * w0;\n  float d1 = w1 * w1;\n  float d2 = w2 * w2;\n  float d3 = w3 * w3;\n  float dTotal = d0 + d1 + d2 + d3;\n  vec3 spectral = (uColorA * d0 + uColorC * d1 + uColorB * d2 + uColorD * d3) / max(dTotal, 0.0001);\n  float energy = (1.0 - exp(-total * 0.58)) * envelope;\n  float mainDistance = abs(q.y - mainY);\n  float whiteCore = exp(-mainDistance * mainDistance / 0.0028) * envelope;\n  float glassFill = uGlass > 0.5 ? 1.0 : 0.0;\n  vec3 atmosphere = mix(uColorD, uColorB, smoothstep(-0.7, 0.7, q.y)) * 0.018 * glassFill;\n  vec3 color = atmosphere + spectral * energy * 1.14;\n  color += uHighlight * whiteCore * (0.18 + 0.1 * low);\n  float emissionMask = mix(smoothstep(0.08, 0.25, energy + whiteCore * 0.12), 1.0, glassFill);\n  color *= emissionMask;\n  color = color / (vec3(1.0) + color * 0.18);\n  return finishEmission(color, p);\n}\n\nfloat auroraLayer(vec2 p, float t, float off) {\n  float drift = t * 0.18 + off * 2.5;\n  float wave1 = sin(p.x * (2.0 + uWarp * 0.13) + drift + off * 6.0) * 0.25;\n  float wave2 = sin(p.x * 3.7 + drift * 1.3 + off * 4.0) * 0.12;\n  float wave3 = sin(p.x * 7.2 + drift * 0.7 + off * 8.0) * 0.055;\n  float noiseValue = lqFbm(vec2(p.x * 1.6 + drift * 0.35, p.y * 0.8 + off * 3.0), 0.018).x;\n  float center = off * 0.46 + wave1 + wave2 + wave3 + (noiseValue - 0.5) * 0.28;\n  float dist = abs(p.y - center);\n  float curtain = exp(-dist * dist * (13.0 - 5.0 * uRidgeAmt));\n  float shimmer = lqFbm(vec2(p.x * 4.0 + t * 0.22, p.y * 7.0 + off * 5.0), 0.012).x;\n  return curtain * (0.64 + 0.36 * shimmer);\n}\n\nvec3 auroraFluid(vec2 p, float t) {\n  vec2 q = p * (0.82 + uZoom * 0.58);\n  float l0 = auroraLayer(q, t, -0.72);\n  float l1 = auroraLayer(q, t, 0.0);\n  float l2 = auroraLayer(q, t, 0.72);\n  vec3 color = uColorA * (0.46 + 0.18 * (q.y + 1.0));\n  color += uColorB * l0 * 1.3;\n  color += uColorC * l1 * 1.15;\n  color += uColorD * l2 * 1.2;\n  color += mix(uColorB, uColorD, 0.5) * min(l0 * l2, l1) * 0.65;\n  vec2 starUv = (q + vec2(1.0)) * 18.0;\n  float starHash = lqHash(floor(starUv));\n  vec2 starF = fract(starUv) - vec2(0.5);\n  float starPoint = exp(-dot(starF, starF) * 90.0);\n  float stars = step(0.965, starHash) * starPoint\n              * (0.55 + 0.45 * sin(t * (1.0 + starHash * 2.0) + starHash * 6.28));\n  color += uHighlight * stars * (1.0 - clamp(l0 + l1 + l2, 0.0, 1.0));\n  color = color / (vec3(1.0) + color * 0.28);\n  return finishPreset(color, p);\n}\n\nfloat neuroShape(vec2 pIn, float t) {\n  vec2 p = pIn * (0.34 + 0.08 * uZoom);\n  vec2 sineAccum = vec2(0.0);\n  vec2 result = vec2(0.0);\n  float scale = 8.0;\n  for (int j = 0; j < 11; j++) {\n    p = rot2(p, 1.0);\n    sineAccum = rot2(sineAccum, 1.0);\n    vec2 layer = p * scale + vec2(float(j)) + sineAccum - vec2(t * 0.34);\n    sineAccum += sin(layer);\n    result += (vec2(0.5) + 0.5 * cos(layer)) / scale;\n    scale *= 1.16;\n  }\n  return result.x + result.y;\n}\n\nvec3 plasmaFluid(vec2 p, float t) {\n  float shape = neuroShape(p, t);\n  float phase = shape * (10.0 + uWarp) + p.x * 1.7 - p.y * 1.3 - t * 0.52;\n  float ridgeWidth = 0.62 - 0.24 * uRidgeAmt;\n  float primary = pow(abs(cos(phase)), max(1.3, uSharp * ridgeWidth));\n  float secondary = pow(abs(cos(phase * 0.53 + atan(p.y, p.x) * 2.0 + t * 0.21)),\n                        max(1.6, uSharp * (ridgeWidth + 0.1)));\n  float filaments = max(primary, secondary * 0.64);\n  float core = pow(primary, 4.0);\n  float polarity = 0.5 + 0.5 * sin(phase * 0.37 + shape * 3.0);\n  vec3 color = mix(uColorA * 0.42, uColorD * 0.48, polarity * 0.46);\n  color = mix(color, uColorB, filaments * 0.72);\n  color = mix(color, uColorC, core * 0.68);\n  color += uHighlight * pow(core, 3.0) * 0.16;\n  color = color / (vec3(1.0) + color * 0.34);\n  return finishPreset(color, p);\n}\n\nvec3 chromeFluid(vec2 p, float t) {\n  vec2 q = p * (1.0 + uZoom * 0.35);\n  float amplitude = 0.028 * uWarp;\n  for (int i = 1; i <= 9; i++) {\n    float fi = float(i);\n    q.x += amplitude / fi * cos(fi * 2.7 * q.y + t * 0.46);\n    q.y += amplitude / fi * cos(fi * 3.1 * q.x - t * 0.4);\n  }\n  float denominator = max(abs(sin(t * 0.24 - q.y - q.x)), 0.045);\n  float flare = clamp(1.0 / denominator, 0.0, 18.0);\n  float metal = smoothstep(1.15, 7.5, flare);\n  float fold = 0.5 + 0.5 * cos((q.x - q.y) * (3.2 + uSharp * 0.28) + t * 0.32);\n  float value = clamp(metal * 0.74 + fold * 0.36, 0.0, 1.0);\n  vec3 color = lqRamp(value, uColorD, uColorC, uColorB, uColorA);\n  color = mix(color, uColorA, pow(metal, 5.0) * 0.62);\n  return finishPreset(color, p);\n}\n\nvec3 opalFluid(vec2 p, float t) {\n  vec2 q = p * (0.8 + uZoom * 0.64);\n  float complexity = 0.76 + uWarp * 0.085;\n  float d = -t * 0.42;\n  float a = 0.0;\n  for (int i = 0; i < 8; i++) {\n    float fi = float(i);\n    a += cos(fi - d - a * q.x * complexity);\n    d += sin(q.y * fi * complexity + a);\n  }\n  d += t * 0.42;\n  vec2 c1 = cos(q * vec2(d, a)) * 0.6 + vec2(0.4);\n  float c2 = cos(a + d) * 0.5 + 0.5;\n  vec3 interference = 0.5 + 0.5 * cos(vec3(c1, c2) * cos(vec3(d, a, 2.5)) * 0.5 + vec3(0.5));\n  float tone = fract(interference.r * 0.37 + interference.g * 0.51\n                     + interference.b * 0.73 + c1.x * 0.22 - c1.y * 0.15);\n  vec3 color = lqRamp(tone, uColorB, uColorC, uColorD, uColorA);\n  color = mix(color, uColorA, 0.16 + 0.1 * interference.b);\n  color = color / (vec3(1.0) + color * 0.16);\n  return finishPreset(color, p);\n}\n\nvec3 blueDropFluid(vec2 p, float t) {\n  float depth = sqrt(max(1.0 - clamp(dot(p, p), 0.0, 1.0), 0.0));\n  vec2 q = p * mix(0.72, 1.0, depth * 0.62 + 0.38);\n  q = rot2(q, -0.24 + 0.06 * sin(t * 0.17));\n  float scale = 1.0 + uZoom * 1.12;\n  float blur = 0.012 + 0.006 * uZoom;\n  vec2 driftA = lqFbm(q * 1.28 + vec2(t * 0.095, -t * 0.034), blur * 1.28);\n  vec2 driftB = lqFbm(rot2(q, 1.08) * 1.62 + vec2(-t * 0.042, t * 0.078), blur * 1.62);\n  vec2 flowed = q + vec2(driftA.x - 0.5, driftB.x - 0.5) * (0.24 + uWarp * 0.1);\n  flowed.x += sin(flowed.y * 2.15 + t * 0.24) * (0.035 + uWarp * 0.012);\n  flowed.y += sin(flowed.x * 1.38 - t * 0.18) * (0.045 + uWarp * 0.01);\n  vec2 body = lqFbm(flowed * scale + vec2(t * 0.025, -t * 0.018), blur * scale);\n  float marble = lqRidgeS(lqFbm(flowed * (1.72 + uZoom * 0.9) + vec2(2.7, -t * 0.035),\n                                blur * (1.72 + uZoom * 0.9)),\n                          0.8 + uSharp * 0.46);\n  float value = clamp(mix(body.x, body.x * 0.62 + marble * 0.58, uRidgeAmt), 0.0, 1.0);\n  vec3 color = lqRamp(value, uColorA, uColorB, uColorC, uColorD);\n  float light = pow(max(dot(normalize(vec3(p, depth)), normalize(vec3(-0.48, 0.62, 0.92))), 0.0), 3.2);\n  color = mix(color, uHighlight, light * (0.035 + 0.05 * uShade));\n  color *= 0.74 + 0.26 * depth;\n  return finishPreset(color, p);\n}\n\nvec3 violetEmberFluid(vec2 p, float t) {\n  float scale = 1.08 + uZoom * 1.18;\n  float blur = 0.011 + 0.005 * uZoom;\n  float radius = length(p);\n  float twist = t * 0.055 + radius * (0.72 + uWarp * 0.11) + 0.08 * sin(t * 0.31 + radius * 4.0);\n  vec2 q = rot2(p * scale, twist);\n  vec2 low = lqFbm(q * 1.18 + vec2(t * 0.068, -t * 0.105), blur * 1.18);\n  vec2 crossFlow = lqFbm(rot2(q, -1.12) * 1.52 + vec2(-t * 0.094, t * 0.042)\n                         + vec2(low.x * 1.35, -low.x * 0.72), blur * 1.52);\n  vec2 warped = q + vec2(low.x - 0.5, crossFlow.x - 0.5) * (0.3 + uWarp * 0.12);\n  vec2 melt = lqFbm(warped * 1.34 + vec2(crossFlow.x * 1.48, low.x * 1.12), blur * 1.34);\n  float veins = lqRidgeS(lqFbm(warped * (2.05 + uZoom * 0.72) + vec2(-2.1, t * 0.052),\n                               blur * (2.05 + uZoom * 0.72)),\n                         0.82 + uSharp * 0.58);\n  float heat = smoothstep(0.18, 0.92,\n                          melt.x * (0.72 - uRidgeAmt * 0.16) + veins * (0.32 + uRidgeAmt * 0.5));\n  vec3 color = lqRamp(heat, uColorA, uColorB, uColorC, uColorD);\n  float pulse = 0.94 + 0.06 * sin(t * 0.44 + melt.x * 5.0);\n  color *= pulse;\n  color = mix(color, uHighlight, pow(veins, 4.0) * 0.045);\n  return finishPreset(color, p);\n}\n\nvec3 presetFluidA(vec2 p, float t) {\n#if STYLE_A == 0\n  return siriFluid(p, t);\n#elif STYLE_A == 1\n  return auroraFluid(p, t);\n#elif STYLE_A == 2\n  return plasmaFluid(p, t);\n#elif STYLE_A == 3\n  return chromeFluid(p, t);\n#elif STYLE_A == 4\n  return opalFluid(p, t);\n#elif STYLE_A == 5\n  return blueDropFluid(p, t);\n#else\n  return violetEmberFluid(p, t);\n#endif\n}\n\nvec3 presetFluidB(vec2 p, float t) {\n#if STYLE_B == 0\n  return siriFluid(p, t);\n#elif STYLE_B == 1\n  return auroraFluid(p, t);\n#elif STYLE_B == 2\n  return plasmaFluid(p, t);\n#elif STYLE_B == 3\n  return chromeFluid(p, t);\n#elif STYLE_B == 4\n  return opalFluid(p, t);\n#elif STYLE_B == 5\n  return blueDropFluid(p, t);\n#else\n  return violetEmberFluid(p, t);\n#endif\n}\n\n// Crossfade of the two flow fields; a solo (A == B) program pays for one.\nvec3 fluidMix(vec2 p, float t) {\n#if STYLE_A == STYLE_B\n  return presetFluidA(p, t);\n#else\n  return mix(presetFluidA(p, t), presetFluidB(p, t), uStyleMix);\n#endif\n}\n\n// ── glass shell ─────────────────────────────────────────────────────────────\nvec3 over(vec3 dst, vec3 src, float a) {\n  float k = clamp(a, 0.0, 1.0);\n  return src * k + dst * (1.0 - k);\n}\n\nfloat refractionProfile(float t) {\n  float depth = clamp(t, 0.0, 1.0);\n  float circular = sqrt(max(1.0 - (1.0 - depth) * (1.0 - depth), 0.0));\n  return 1.0 - circular;\n}\n\nfloat highlightLobe(vec2 n, vec2 direction, float cut, float strength) {\n  float angular = clamp((dot(n, direction) - cut) / max(1.0 - cut, 0.001), 0.0, 1.0);\n  return pow(angular, strength);\n}\n\nvec2 contourWave(float angle, float t) {\n  float wave = sin(angle * 3.0 + t * 0.62) * 0.52\n             + sin(angle * 5.0 - t * 0.41 + 1.7) * 0.31\n             + sin(angle * 2.0 + t * 0.23 + 3.1) * 0.17;\n  float slope = cos(angle * 3.0 + t * 0.62) * 1.56\n              + cos(angle * 5.0 - t * 0.41 + 1.7) * 1.55\n              + cos(angle * 2.0 + t * 0.23 + 3.1) * 0.34;\n  return vec2(wave, slope);\n}\n\nfloat contourStrengthMixed() {\n  return mix(STYLE_A >= 5 ? 0.11 : 0.09, STYLE_B >= 5 ? 0.11 : 0.09, uStyleMix);\n}\n\nfloat contourScale(vec2 uv, float t, float amount) {\n  if (amount <= 0.0) { return 1.0; }\n  vec2 c = contourWave(atan(uv.y, uv.x), t);\n  return 1.0 + clamp(amount, 0.0, 1.0) * contourStrengthMixed() * c.x;\n}\n\nvec2 contourNormal(vec2 uv, float rad, float t, float amount) {\n  float dist = length(uv);\n  if (dist <= 0.0001) { return vec2(0.0); }\n  vec2 radial = uv / dist;\n  vec2 c = contourWave(atan(uv.y, uv.x), t);\n  float slope = clamp(amount, 0.0, 1.0) * contourStrengthMixed() * c.y;\n  vec2 tangent = vec2(-radial.y, radial.x);\n  return normalize(radial - tangent * (rad * slope / dist));\n}\n\n// Additive halo past the limb, fenced to the outside of the boundary.\nvec3 edgeGlowAdd(vec3 col, vec2 uv, float rad) {\n  if (uGlow <= 0.0) { return col; }\n  float r = length(uv);\n  float outside = smoothstep(rad - 0.005, rad + 0.005, r);\n  return col + uGlowColor * (uGlow * exp(-max(r - rad, 0.0) * 11.0) * outside);\n}\n\nvoid main() {\n  vec2 uv = (2.0 * gl_FragCoord.xy - uSize) / max(min(uSize.x, uSize.y), 1.0);\n  float rad = max(uRadius, 0.05);\n  float t = uTime;\n  float contourRad = rad * contourScale(uv, t, uContourDeform);\n\n  // Off the ball entirely — only the halo lives out here.\n  if (length(uv) > contourRad * 1.01) {\n    vec3 halo = clamp(edgeGlowAdd(vec3(0.0), uv, contourRad), 0.0, 1.0);\n    float haloAlpha = max(halo.r, max(halo.g, halo.b));\n    outColor = vec4(halo, haloAlpha);\n    return;\n  }\n\n  vec2 p = uv / contourRad;\n  float pd = length(p);\n\n  float clearFa = 1.0 - smoothstep(GL_CLEAR_EA, GL_CLEAR_EB, pd);\n  vec2 normal = contourNormal(uv, rad, t, uContourDeform);\n  float edgeDepth = max(1.0 - pd, 0.0);\n  float refractionWidth = 0.015 + 0.95 * clamp(uShellMidAlpha, 0.0, 1.0);\n  float refractionT = edgeDepth / max(refractionWidth, 0.001);\n  float refractionProf = pow(refractionProfile(refractionT), 0.68);\n  float refractionAmount = 1.6 * clamp(uGlassOpacity, 0.0, 1.0) * refractionProf;\n  vec2 refractedP = p - normal * refractionAmount;\n\n  vec3 fcol = vec3(0.0);\n  if (clearFa > 0.0) {\n    if (uGlass > 0.5) {\n      // Three fluid evaluations produce optical dispersion at the limb; the\n      // channels converge continuously at the inner edge of the band.\n      // refractionProf is exactly 0 inward of the band, so the interior skips\n      // two of the three evaluations with no visual change.\n      float channelSplit = 0.14 * clamp(uGloss, 0.0, 2.0)\n                         * clamp(uGlassOpacity, 0.0, 1.0) * refractionProf;\n      if (channelSplit > 0.0008) {\n        vec3 redSample = fluidMix(refractedP - normal * channelSplit, t);\n        vec3 greenSample = fluidMix(refractedP, t);\n        vec3 blueSample = fluidMix(refractedP + normal * channelSplit, t);\n        fcol = vec3(redSample.r, greenSample.g, blueSample.b);\n      } else {\n        fcol = fluidMix(refractedP, t);\n      }\n    } else {\n      fcol = fluidMix(p, t);\n    }\n  }\n\n  float lum = dot(fcol, vec3(0.213, 0.715, 0.072));\n  vec3 clearSat = clamp(vec3(lum) + (fcol - vec3(lum)) * 1.22, 0.0, 1.0);\n\n  // With glass off, the siri field is a true emissive layer instead of a disc.\n  float emissionW = 0.0;\n  if (uGlass <= 0.5) {\n#if STYLE_A == 0\n    emissionW += 1.0 - uStyleMix;\n#endif\n#if STYLE_B == 0\n    emissionW += uStyleMix;\n#endif\n  }\n  float signal = max(clearSat.r, max(clearSat.g, clearSat.b));\n  vec3 col = mix(over(uCanvasColor, clearSat, 0.99 * clearFa),\n                 clearSat * smoothstep(0.025, 0.16, signal),\n                 emissionW);\n\n  if (uGlass > 0.5) {\n    float surfaceWidth = 0.026 + 0.055 * clamp(uShellEdgeAlpha, 0.0, 1.0);\n    float surfaceBand = (1.0 - smoothstep(0.0, surfaceWidth, edgeDepth)) * clearFa;\n    float opticalRim = pow(surfaceBand, 1.8);\n    col = over(col, uShellInner, opticalRim * uGlassOpacity * 0.45);\n\n    vec2 coolDirection = normalize(vec2(0.84, 0.54));\n    vec2 warmDirection = normalize(vec2(-0.62, -0.78));\n    float coolSplit = highlightLobe(normal, coolDirection, -0.32, 1.8);\n    float warmSplit = highlightLobe(normal, warmDirection, -0.28, 2.0);\n    float dispersion = opticalRim * clamp(uGloss, 0.0, 2.0) * (0.8 + 0.8 * uShellEdgeAlpha);\n    col = over(col, uShellMid, dispersion * coolSplit);\n    col = over(col, uShellEdge, dispersion * warmSplit);\n\n    float edgeShadow = opticalRim * (0.015 + 0.15 * uShellEdgeAlpha)\n                     * (0.15 + 0.85 * max(dot(normal, vec2(0.45, -0.89)), 0.0));\n    col *= 1.0 - edgeShadow;\n\n    vec2 keyDirection = normalize(vec2(-0.68, 0.73));\n    vec2 fillDirection = normalize(vec2(0.74, -0.67));\n    float key = opticalRim * highlightLobe(normal, keyDirection, 0.2, 2.8)\n              * clamp(uSheen, 0.0, 2.0) * 1.4;\n    float fill = opticalRim * highlightLobe(normal, fillDirection, 0.4, 3.6)\n              * clamp(uSheen, 0.0, 2.0);\n    col = over(col, uSheenColor, key);\n    col = over(col, uSpecColor, fill);\n  }\n\n  float ballA = 1.0 - smoothstep(0.99, 1.01, pd);\n  col = clamp(col * max(uExposure, 0.0), 0.0, 1.0) * ballA;\n  vec3 finalColor = clamp(edgeGlowAdd(col, uv, contourRad), 0.0, 1.0);\n  float emissionAlpha = max(finalColor.r, max(finalColor.g, finalColor.b));\n  float sphereAlpha = clamp(max(ballA, emissionAlpha), 0.0, 1.0);\n  float finalAlpha = mix(sphereAlpha, emissionAlpha, clamp(emissionW, 0.0, 1.0));\n  outColor = vec4(finalColor, finalAlpha);\n}\n`;\n\nconst compileShader = (gl: WebGL2RenderingContext, type: number, source: string) => {\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    console.error(\"liquid-orb shader compile failed:\", gl.getShaderInfoLog(shader));\n    gl.deleteShader(shader);\n    return null;\n  }\n  return shader;\n};\n\nconst easeInOut = (x: number) => x * x * (3 - 2 * x);\n\nexport const LiquidOrb = ({\n  preset = \"siri\",\n  cycle = true,\n  holdDuration = 1.8,\n  morphDuration = 1.25,\n  speed = 1,\n  glass = true,\n  glow = 0.55,\n  onPresetChange,\n}: LiquidOrbConfig = {}) => {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const onPresetChangeRef = useRef(onPresetChange);\n  useEffect(() => {\n    onPresetChangeRef.current = onPresetChange;\n  }, [onPresetChange]);\n\n  useEffect(() => {\n    const container = rootRef.current;\n    if (!container) {\n      return;\n    }\n\n    const canvas = document.createElement(\"canvas\");\n    canvas.style.display = \"block\";\n    canvas.style.width = \"100%\";\n    canvas.style.height = \"100%\";\n    container.append(canvas);\n\n    const gl = canvas.getContext(\"webgl2\", {\n      alpha: true,\n      antialias: false,\n      depth: false,\n      premultipliedAlpha: true,\n      stencil: false,\n    });\n    if (!gl) {\n      canvas.remove();\n      return;\n    }\n\n    const vs = compileShader(gl, gl.VERTEX_SHADER, vertexSrc);\n    if (!vs) {\n      canvas.remove();\n      return;\n    }\n    gl.disable(gl.BLEND);\n\n    interface ProgramEntry {\n      program: WebGLProgram;\n      uSize: WebGLUniformLocation | null;\n      uTime: WebGLUniformLocation | null;\n      uStyleMix: WebGLUniformLocation | null;\n      scalars: (WebGLUniformLocation | null)[];\n      colors: (WebGLUniformLocation | null)[];\n    }\n    const programs = new Map<string, ProgramEntry | null>();\n\n    const getProgram = (a: number, b: number): ProgramEntry | null => {\n      const key = `${a}:${b}`;\n      const cached = programs.get(key);\n      if (cached !== undefined) {\n        return cached;\n      }\n      const src = `#version 300 es\\n#define STYLE_A ${a}\\n#define STYLE_B ${b}\\n${fragmentBody}`;\n      const fs = compileShader(gl, gl.FRAGMENT_SHADER, src);\n      const program = gl.createProgram();\n      let entry: ProgramEntry | null = null;\n      if (fs && program) {\n        gl.attachShader(program, vs);\n        gl.attachShader(program, fs);\n        gl.linkProgram(program);\n        gl.deleteShader(fs);\n        if (gl.getProgramParameter(program, gl.LINK_STATUS)) {\n          const loc = (name: string) => gl.getUniformLocation(program, name);\n          // eslint-disable-next-line react/hooks -- WebGL method, not a React hook.\n          gl.useProgram(program);\n          gl.uniform1f(loc(\"uGlass\"), glass ? 1 : 0);\n          gl.uniform1f(loc(\"uGlow\"), Math.max(glow, 0));\n          entry = {\n            colors: colorUniforms.map(loc),\n            program,\n            scalars: scalarUniforms.map(loc),\n            uSize: loc(\"uSize\"),\n            uStyleMix: loc(\"uStyleMix\"),\n            uTime: loc(\"uTime\"),\n          };\n        } else {\n          console.error(\"liquid-orb program link failed:\", gl.getProgramInfoLog(program));\n          gl.deleteProgram(program);\n        }\n      }\n      programs.set(key, entry);\n      return entry;\n    };\n\n    // Adaptive render resolution: the canvas upscales via CSS, and the scale\n    // seeks the largest value the machine sustains — discrete GPUs settle at\n    // 1, software rasterizers sink toward the floor instead of dropping to\n    // slideshow frame rates.\n    const SCALE_MIN = 0.3;\n    const SCALE_MAX = 1;\n    let renderScale = 0.5;\n    let frameCostMs = 33;\n    let lastAdjust = 0;\n\n    const resize = () => {\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      const width = Math.max(1, Math.round(container.clientWidth * dpr * renderScale));\n      const height = Math.max(1, Math.round(container.clientHeight * dpr * renderScale));\n      if (canvas.width !== width || canvas.height !== height) {\n        canvas.width = width;\n        canvas.height = height;\n        gl.viewport(0, 0, width, height);\n      }\n    };\n    resize();\n    const observer = new ResizeObserver(resize);\n    observer.observe(container);\n\n    const order = presetOrder;\n    let current = Math.max(order.indexOf(preset), 0);\n    let next = (current + 1) % order.length;\n    let morphing = false;\n    let morphT = 0;\n    let holdT = 0;\n    let phase = 0;\n    let last = performance.now();\n    let frame = 0;\n\n    const mixed = new Float32Array(VEC_SIZE);\n\n    const adaptRenderScale = (now: number, elapsed: number) => {\n      frameCostMs += (Math.min(elapsed, 1000) - frameCostMs) * 0.2;\n      if (now - lastAdjust <= 700) {\n        return;\n      }\n      if (frameCostMs > 50 && renderScale > SCALE_MIN) {\n        renderScale = Math.max(SCALE_MIN, renderScale * 0.8);\n        lastAdjust = now;\n        resize();\n      } else if (frameCostMs < 25 && renderScale < SCALE_MAX) {\n        renderScale = Math.min(SCALE_MAX, renderScale * 1.25);\n        lastAdjust = now;\n        resize();\n      }\n    };\n\n    const advanceCycle = (dt: number) => {\n      if (morphing) {\n        morphT += dt / Math.max(morphDuration, 0.001);\n        if (morphT >= 1) {\n          current = next;\n          next = (current + 1) % order.length;\n          morphing = false;\n          morphT = 0;\n          holdT = 0;\n        }\n        return;\n      }\n      holdT += dt;\n      if (holdT >= holdDuration) {\n        morphing = true;\n        morphT = 0;\n        const targetPreset = order[next];\n        if (targetPreset === undefined) {\n          throw new RangeError(\"Liquid orb preset index is out of bounds\");\n        }\n        onPresetChangeRef.current?.(targetPreset);\n      }\n    };\n\n    const draw = (now: number) => {\n      frame = requestAnimationFrame(draw);\n      const elapsed = now - last;\n      const dt = Math.min(elapsed / 1000, 0.25);\n      last = now;\n\n      adaptRenderScale(now, elapsed);\n      if (cycle) {\n        advanceCycle(dt);\n      }\n\n      const k = morphing ? easeInOut(Math.min(morphT, 1)) : 0;\n      const currentPreset = order[current];\n      const nextPreset = order[next];\n      if (!currentPreset || !nextPreset) {\n        return;\n      }\n      const a = presetVecs.get(currentPreset);\n      const b = presetVecs.get(nextPreset);\n      if (!a || !b) {\n        return;\n      }\n      for (let i = 0; i < VEC_SIZE; i += 1) {\n        const from = a[i];\n        const to = b[i];\n        if (from === undefined || to === undefined) {\n          throw new RangeError(\"Liquid orb preset buffer is incomplete\");\n        }\n        mixed[i] = from + (to - from) * k;\n      }\n\n      const [phaseSpeed] = mixed;\n      if (phaseSpeed === undefined) {\n        throw new Error(\"Liquid orb preset buffer is missing its speed\");\n      }\n      phase += dt * phaseSpeed * speed;\n\n      const styleA = styleIndex[currentPreset];\n      const styleB = styleIndex[nextPreset];\n      const entry = morphing ? getProgram(styleA, styleB) : getProgram(styleA, styleA);\n      if (!entry) {\n        return;\n      }\n      // Warm the next segment's program mid-segment so switches don't hitch.\n      if (!morphing && holdT > 0.3) {\n        getProgram(styleA, styleB);\n      } else if (morphing && morphT > 0.5) {\n        getProgram(styleB, styleB);\n      }\n\n      // eslint-disable-next-line react/hooks -- WebGL method, not a React hook.\n      gl.useProgram(entry.program);\n      gl.uniform2f(entry.uSize, canvas.width, canvas.height);\n      gl.uniform1f(entry.uTime, phase);\n      gl.uniform1f(entry.uStyleMix, k);\n      for (const [i, location] of entry.scalars.entries()) {\n        gl.uniform1fv(location, mixed, i + 1, 1);\n      }\n      for (const [i, location] of entry.colors.entries()) {\n        gl.uniform3fv(location, mixed, COLOR_BASE + i * 3, 3);\n      }\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n    };\n    frame = requestAnimationFrame(draw);\n\n    return () => {\n      cancelAnimationFrame(frame);\n      observer.disconnect();\n      for (const entry of programs.values()) {\n        if (entry) {\n          gl.deleteProgram(entry.program);\n        }\n      }\n      gl.deleteShader(vs);\n      gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n      canvas.remove();\n    };\n  }, [preset, cycle, holdDuration, morphDuration, speed, glass, glow]);\n\n  return <div ref={rootRef} className=\"h-full w-full\" />;\n};\n","path":"/liquid-orb.tsx","target":"uicapsule/liquid-orb/liquid-orb.tsx","type":"registry:file"},{"content":"\"use client\";\n\nimport { useState } from \"react\";\nimport { LiquidOrb, presetLabel } from \"./liquid-orb\";\nimport type { LiquidOrbPreset } from \"./liquid-orb\";\n\nconst Preview = () => {\n  const [preset, setPreset] = useState<LiquidOrbPreset>(\"siri\");\n\n  return (\n    <main className=\"relative h-dvh w-full overflow-hidden bg-[radial-gradient(ellipse_at_center,#0d0d14_0%,#050508_55%,#000000_100%)]\">\n      <LiquidOrb onPresetChange={setPreset} />\n      <div className=\"pointer-events-none absolute inset-x-0 bottom-12 flex justify-center\">\n        <span\n          key={preset}\n          className=\"animate-[liquid-orb-label_700ms_ease] text-sm font-medium tracking-[0.35em] text-white/50 uppercase\"\n        >\n          {presetLabel[preset]}\n        </span>\n      </div>\n      <style>{`\n        @keyframes liquid-orb-label {\n          from { opacity: 0; transform: translateY(8px); }\n          to { opacity: 1; transform: none; }\n        }\n      `}</style>\n    </main>\n  );\n};\n\nexport default Preview;\n","path":"/preview.tsx","target":"uicapsule/liquid-orb/preview.tsx","type":"registry:file"}],"homepage":"https://www.uicapsule.com/ui/liquid-orb","name":"liquid-orb","registryDependencies":[],"title":"Liquid Orb","type":"registry:block"}