{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "CrystalizedBall-JS-TW",
	"title": "CrystalizedBall",
	"description": "A glass ball with a crackling electric rim and glowing dust that swirls with the cursor.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "CrystalizedBall/CrystalizedBall.jsx",
			"content": "'use client';\n\nimport { useEffect, useMemo, useRef } from 'react';\nimport { Renderer, Program, Mesh, Triangle, Geometry, RenderTarget, Texture } from 'ogl';\n\nconst BALL_PRESETS = {\n  plasma: {\n    color: '#F25BD0',\n    strands: 6,\n    crackle: 0.85,\n    flares: 0.65,\n    glow: 0.9,\n    sparks: 0.6,\n    particleCount: 15000,\n    fill: 0.5,\n    motion: 'rise',\n    particleShape: 'square',\n    depth: 0.6,\n    sway: 0.5,\n    twinkle: 0.5,\n    haze: 0.7,\n    dustSpeed: 1\n  },\n  aurora: {\n    color: '#5CFFC8',\n    strands: 5,\n    crackle: 0.6,\n    flares: 0.5,\n    glow: 0.8,\n    sparks: 0.45,\n    particleCount: 15000,\n    fill: 0.5,\n    motion: 'rise',\n    particleShape: 'square',\n    depth: 0.6,\n    sway: 0.5,\n    twinkle: 0.5,\n    haze: 0.7,\n    dustSpeed: 1\n  },\n  nebula: {\n    color: '#9478FF',\n    strands: 6,\n    crackle: 1,\n    flares: 0.5,\n    glow: 0.9,\n    sparks: 0.6,\n    particleCount: 18000,\n    fill: 0.45,\n    motion: 'rise',\n    particleShape: 'square',\n    depth: 0.5,\n    sway: 0.4,\n    twinkle: 0.6,\n    haze: 0.8,\n    dustSpeed: 1.2\n  },\n  ember: {\n    color: '#FF8A2A',\n    strands: 5,\n    crackle: 0.9,\n    flares: 0.8,\n    glow: 1,\n    sparks: 0.8,\n    particleCount: 12000,\n    fill: 0.35,\n    motion: 'rise',\n    particleShape: 'round',\n    depth: 0.7,\n    sway: 0.3,\n    twinkle: 0.7,\n    haze: 0.9,\n    dustSpeed: 1.6\n  },\n  frost: {\n    color: '#BFE6FF',\n    strands: 3,\n    crackle: 0.3,\n    flares: 0.3,\n    glow: 0.6,\n    sparks: 0.2,\n    particleCount: 16000,\n    fill: 0.6,\n    motion: 'fall',\n    particleShape: 'round',\n    depth: 0.8,\n    sway: 0.4,\n    twinkle: 0.4,\n    haze: 0.5,\n    dustSpeed: 0.7\n  },\n  solar: {\n    color: '#FFD36E',\n    strands: 6,\n    crackle: 0.7,\n    flares: 1,\n    glow: 1.1,\n    sparks: 0.5,\n    particleCount: 15000,\n    fill: 0.55,\n    motion: 'orbit',\n    particleShape: 'square',\n    depth: 0.6,\n    sway: 0.7,\n    twinkle: 0.5,\n    haze: 0.8,\n    dustSpeed: 1\n  },\n  eclipse: {\n    color: '#FFFFFF',\n    strands: 4,\n    crackle: 0.5,\n    flares: 0.4,\n    glow: 0.7,\n    sparks: 0.35,\n    particleCount: 14000,\n    fill: 0.5,\n    motion: 'drift',\n    particleShape: 'square',\n    depth: 0.7,\n    sway: 0.5,\n    twinkle: 0.5,\n    haze: 0.5,\n    dustSpeed: 0.8\n  },\n  abyss: {\n    color: '#3F7BFF',\n    strands: 5,\n    crackle: 0.55,\n    flares: 0.6,\n    glow: 0.9,\n    sparks: 0.4,\n    particleCount: 20000,\n    fill: 0.8,\n    motion: 'orbit',\n    particleShape: 'round',\n    depth: 0.8,\n    sway: 0.8,\n    twinkle: 0.5,\n    haze: 0.6,\n    dustSpeed: 0.9\n  }\n};\n\nconst MOTIONS = { rise: 0, fall: 1, drift: 2, orbit: 3 };\nconst SHAPES = { square: 0, round: 1 };\nconst MAX_STRANDS = 8;\nconst MAX_ARCS = 4;\nconst MAX_PARTICLES = 40000;\nconst STATE_WIDTH = 256;\nconst PIXEL_BUDGET = 4.5e6;\nconst INTRO_SECONDS = 2.2;\nconst SETTLE_SECONDS = 7;\nconst WHITE = [1, 1, 1];\nconst BLACK = [0, 0, 0];\n\nconst clamp = (value, min, max) => Math.min(Math.max(value, min), max);\nconst smooth = (edge0, edge1, value) => {\n  const t = clamp((value - edge0) / (edge1 - edge0), 0, 1);\n  return t * t * (3 - 2 * t);\n};\nconst easeOut = t => 1 - Math.pow(1 - t, 3);\nconst mixColor = (a, b, t) => a.map((v, i) => v + (b[i] - v) * t);\nconst wrapAngle = a => a - Math.PI * 2 * Math.floor((a + Math.PI) / (Math.PI * 2));\n\nconst parseColor = (value, fallback) => {\n  try {\n    const ctx = document.createElement('canvas').getContext('2d');\n    if (!ctx) return fallback;\n    ctx.fillStyle = '#000000';\n    ctx.fillStyle = value;\n    const resolved = ctx.fillStyle;\n    if (resolved.startsWith('#')) {\n      const n = parseInt(resolved.slice(1), 16);\n      return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n    }\n    const parts = resolved.match(/[\\d.]+/g);\n    if (!parts || parts.length < 3) return fallback;\n    return [Number(parts[0]) / 255, Number(parts[1]) / 255, Number(parts[2]) / 255];\n  } catch {\n    return fallback;\n  }\n};\n\nconst toLinear = c => (c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));\nconst toGamma = c => (c <= 0.0031308 ? c * 12.92 : 1.055 * Math.pow(c, 1 / 2.4) - 0.055);\n\nconst toOklab = rgb => {\n  const [r, g, b] = rgb.map(toLinear);\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};\n\nconst fromOklab = ([L, a, b]) => {\n  const l = Math.pow(L + 0.3963377774 * a + 0.2158037573 * b, 3);\n  const m = Math.pow(L - 0.1055613458 * a - 0.0638541728 * b, 3);\n  const s = Math.pow(L - 0.0894841775 * a - 1.291485548 * b, 3);\n  return [\n    4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,\n    -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,\n    -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s\n  ].map(v => clamp(toGamma(clamp(v, 0, 1)), 0, 1));\n};\n\nconst blend = (a, b, t) => fromOklab(mixColor(toOklab(a), toOklab(b), t));\n\nconst shift = (rgb, hue, lightness, chroma) => {\n  const [L, a, b] = toOklab(rgb);\n  const c = Math.hypot(a, b) * chroma;\n  const h = Math.atan2(b, a) + (hue * Math.PI) / 180;\n  return fromOklab([clamp(L + lightness, 0, 1), c * Math.cos(h), c * Math.sin(h)]);\n};\n\nconst withLightness = (rgb, hue, lightness, chroma) => {\n  const [, a, b] = toOklab(rgb);\n  const c = Math.max(Math.hypot(a, b) * chroma, 0.02);\n  const h = Math.atan2(b, a) + (hue * Math.PI) / 180;\n  return fromOklab([clamp(lightness, 0, 1), c * Math.cos(h), c * Math.sin(h)]);\n};\n\nconst buildPalette = (color, light) => {\n  if (light) {\n    const ink = withLightness(color, 0, Math.min(toOklab(color)[0], 0.62), 1.15);\n    return {\n      rim: ink,\n      rimHot: ink,\n      rimMid: ink,\n      rimDeep: withLightness(color, 0, 0.72, 0.8),\n      spark: withLightness(color, 0, 0.6, 1.2),\n      sparkGlow: withLightness(color, 0, 0.78, 0.8),\n      haze: withLightness(color, 0, 0.8, 0.6),\n      edge: withLightness(color, 0, 0.72, 0.8),\n      tones: [\n        withLightness(color, 0, 0.56, 1.1),\n        withLightness(color, 16, 0.6, 1.05),\n        withLightness(color, -16, 0.5, 1.1),\n        withLightness(color, 0, 0.66, 0.9),\n        withLightness(color, 0, 0.74, 0.7)\n      ]\n    };\n  }\n  return {\n    rim: color,\n    rimHot: mixColor(color, WHITE, 0.72),\n    rimMid: mixColor(color, BLACK, 0.15),\n    rimDeep: mixColor(color, BLACK, 0.45),\n    spark: mixColor(color, WHITE, 0.45),\n    sparkGlow: shift(color, 0, -0.15, 1),\n    haze: mixColor(color, BLACK, 0.6),\n    edge: shift(color, 0, -0.3, 0.9),\n    tones: [\n      shift(color, 0, -0.06, 1),\n      shift(color, 16, -0.02, 1),\n      shift(color, -16, -0.12, 1.05),\n      shift(blend(color, WHITE, 0.25), 0, 0.04, 1),\n      blend(color, WHITE, 0.7)\n    ]\n  };\n};\n\nconst seeded = start => {\n  let state = start >>> 0;\n  return () => {\n    state = (state + 0x6d2b79f5) >>> 0;\n    let t = state;\n    t = Math.imul(t ^ (t >>> 15), t | 1);\n    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n};\n\nconst buildStrands = () => {\n  const random = seeded(99);\n  const bands = [\n    [6, 12, 0.6, 1.6],\n    [18, 34, 1.5, 3.5],\n    [40, 70, 3, 7],\n    [80, 130, 6, 12]\n  ];\n  const data = { harmonics: [], rates: [], phases: [], flareHarmonics: [], flareRates: [], flarePhases: [] };\n  for (let s = 0; s < MAX_STRANDS; s++) {\n    bands.forEach(([low, high, slow, fast]) => {\n      data.harmonics.push(Math.round(low + random() * (high - low)));\n      data.rates.push((slow + random() * (fast - slow)) * (random() < 0.5 ? -1 : 1));\n      data.phases.push(random() * Math.PI * 2);\n    });\n    for (let j = 0; j < 3; j++) {\n      data.flareHarmonics.push(4 + Math.floor(random() * 6));\n      data.flareRates.push((0.4 + random() * 0.8) * (random() < 0.5 ? -1 : 1));\n      data.flarePhases.push(random() * Math.PI * 2);\n    }\n    const lead = s === 0;\n    data.flareHarmonics.push(lead ? 0.75 : 0.95 + random() * 0.3);\n    data.flareRates.push(lead ? 1.15 : 0.8 + random() * 0.2);\n    data.flarePhases.push(lead ? 1 : 0.7 + random() * 0.25);\n  }\n  return data;\n};\n\nconst buildDust = count => {\n  const random = seeded(1337);\n  const rows = Math.max(1, Math.ceil(count / STATE_WIDTH));\n  const home = new Float32Array(STATE_WIDTH * rows * 4);\n  const seed = new Float32Array(STATE_WIDTH * rows * 4);\n  let k = 0;\n  let guard = 0;\n  while (k < count && guard < count * 80) {\n    guard++;\n    let x;\n    let y;\n    let tone;\n    if (random() < 0.28) {\n      const angle = random() * Math.PI * 2;\n      const radius = 0.87 + Math.sqrt(random()) * 0.105;\n      x = Math.cos(angle) * radius;\n      y = Math.sin(angle) * radius;\n      if (random() > smooth(-0.6, 0.3, -y)) continue;\n      const q = random();\n      tone = q < 0.5 ? 3 : q < 0.8 ? 2 : 1;\n    } else {\n      x = random() * 2 - 1;\n      y = random() * 2 - 1;\n      const radius = Math.hypot(x, y);\n      if (radius > 0.975) continue;\n      const bowl = Math.pow(smooth(-0.25, 0.85, -y), 1.3);\n      const band = smooth(0.66, 0.96, radius) * smooth(-0.7, 0.3, -y);\n      const weight = Math.max(bowl, band * 0.9);\n      if (random() > 0.012 + 0.988 * weight) continue;\n      if (weight < 0.12) tone = 4;\n      else {\n        const q = random();\n        tone = q < 0.32 ? 0 : q < 0.52 ? 1 : q < 0.8 ? 2 : 3;\n      }\n    }\n    const chord = Math.sqrt(Math.max(0, 0.95 - x * x - y * y));\n    home[k * 4] = x;\n    home[k * 4 + 1] = y;\n    home[k * 4 + 2] = (random() * 2 - 1) * chord;\n    home[k * 4 + 3] = tone;\n    seed[k * 4] = tone === 4 ? 1.8 : random() < 0.22 ? 2.1 : 1.3;\n    seed[k * 4 + 1] = random();\n    seed[k * 4 + 2] = 2.5 + random() * 3.5;\n    seed[k * 4 + 3] = 0.16 + random() * 0.26;\n    k++;\n  }\n  return { home, seed, count: k, rows };\n};\n\nconst passVertex = `#version 300 es\nin vec2 position;\nin vec2 uv;\nout vec2 vUv;\nvoid main() {\n  vUv = uv;\n  gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst hashChunk = `\nuint scramble(uint v) {\n  v = v * 747796405u + 2891336453u;\n  uint w = ((v >> ((v >> 28u) + 4u)) ^ v) * 277803737u;\n  return (w >> 22u) ^ w;\n}\n\nfloat random(uint v) {\n  return float(scramble(v)) * (1.0 / 4294967295.0);\n}\n`;\n\nconst dustChunk = `\nuniform float uDustTime;\nuniform vec2 uTurn;\nuniform float uMotion;\nuniform float uFill;\n\nvec3 settle(vec3 p, out float spread) {\n  float bend = exp2((0.5 - uFill) * 1.8);\n  float h = clamp((p.y + 1.0) * 0.5, 0.0, 1.0);\n  float y = 2.0 * pow(h, bend) - 1.0;\n  float from = sqrt(max(1.0 - p.y * p.y, 1e-4));\n  float to = sqrt(max(1.0 - y * y, 0.0));\n  spread = clamp(bend * pow(max(h, 1e-3), bend - 1.0) * to / from, 0.25, 1.0);\n  return vec3(p.xz * (to / from), y).xzy;\n}\n\nvec3 drift(vec4 home, vec4 seed, uint id, out float life, out float spread) {\n  vec3 p = settle(home.xyz, spread);\n  float t = uDustTime;\n  float u = fract(t / seed.z + seed.y);\n  life = sin(3.14159265 * u);\n  float travel = seed.w;\n  if (uMotion < 0.5) {\n    p.y += travel * (u - 0.5);\n  } else if (uMotion < 1.5) {\n    p.y -= travel * (u - 0.5);\n  } else if (uMotion < 2.5) {\n    p += 0.045 * vec3(\n      sin(t * 0.37 + seed.y * 17.0) + 0.5 * sin(t * 0.83 + seed.z * 5.0),\n      sin(t * 0.29 + seed.z * 11.0) + 0.5 * sin(t * 0.61 + seed.w * 7.0),\n      sin(t * 0.33 + seed.w * 23.0)\n    );\n  } else {\n    float loop = t * (0.6 + 0.9 * random(id * 7u + 3u)) * (random(id * 7u + 10u) < 0.5 ? -1.0 : 1.0) + seed.y * 6.2831853;\n    p.xy += (0.025 + 0.05 * random(id * 7u + 16u)) * vec2(cos(loop), sin(loop));\n  }\n  float wobble = 0.002 + 0.006 * random(id * 7u + 1u);\n  float w1 = 0.8 + 2.4 * random(id * 7u + 2u);\n  float w2 = 0.8 + 2.4 * random(id * 7u + 4u);\n  p += wobble * vec3(sin(t * w1 + seed.y * 40.0), cos(t * w2 + seed.z * 30.0), sin(t * (w1 + w2) * 0.5 + seed.w * 20.0));\n  float cy = cos(uTurn.x);\n  float sy = sin(uTurn.x);\n  p.xz = mat2(cy, -sy, sy, cy) * p.xz;\n  float cx = cos(uTurn.y);\n  float sx = sin(uTurn.y);\n  p.yz = mat2(cx, -sx, sx, cx) * p.yz;\n  return p;\n}\n`;\n\nconst fieldFragment = `#version 300 es\nprecision highp float;\n\nuniform vec2 uCenter;\nuniform float uRadius;\nuniform float uDpr;\nuniform float uLine;\nuniform float uTime;\nuniform float uFrame;\nuniform float uBins;\nuniform float uStrands;\nuniform float uCrackle;\nuniform float uFlares;\nuniform float uGlow;\nuniform float uHaze;\nuniform float uFill;\nuniform float uPresence;\nuniform float uUnfold;\nuniform float uBloom;\nuniform float uInside;\nuniform float uEncode;\nuniform vec3 uRim;\nuniform vec3 uRimHot;\nuniform vec3 uRimMid;\nuniform vec3 uRimDeep;\nuniform vec3 uSpark;\nuniform vec3 uSparkGlow;\nuniform vec3 uHazeColor;\nuniform vec3 uEdgeColor;\nuniform vec4 uHeat;\nuniform vec4 uHarmonics[8];\nuniform vec4 uRates[8];\nuniform vec4 uPhases[8];\nuniform vec4 uFlareHarmonics[8];\nuniform vec4 uFlareRates[8];\nuniform vec4 uFlarePhases[8];\nuniform vec4 uArcs[4];\n\nout vec4 fragColor;\n${hashChunk}\nconst float PI = 3.14159265;\nconst float TAU = 6.28318531;\nconst vec4 AMPS = vec4(0.35, 0.3, 0.22, 0.13);\n\nfloat bell(float d, float w) {\n  float x = d / w;\n  return exp(-x * x);\n}\n\nfloat segment(vec2 p, vec2 a, vec2 b) {\n  vec2 pa = p - a;\n  vec2 ba = b - a;\n  float h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-5), 0.0, 1.0);\n  return length(pa - ba * h);\n}\n\nfloat pool(vec2 q) {\n  float t = length(q - vec2(0.0, mix(-1.0, -0.66, uFill))) / mix(0.8, 1.35, uFill);\n  return t < 0.6 ? mix(0.5, 0.22, t / 0.6) : mix(0.22, 0.0, clamp((t - 0.6) / 0.4, 0.0, 1.0));\n}\n\nvoid main() {\n  vec2 p = gl_FragCoord.xy / uDpr - uCenter;\n  float r = length(p);\n  float R = uRadius;\n  float theta = atan(p.y, p.x);\n  float lift = p.y / max(r, 1e-3);\n  float topDim = 1.0 - 0.35 * pow(max(lift, 0.0), 1.5);\n  vec3 col = vec3(0.0);\n  float hot = 0.0;\n  uint frame = uint(uFrame);\n\n  if (r < R) {\n    vec2 q = p / R;\n    float chord = sqrt(max(1.0 - dot(q, q), 0.0));\n    float haze = pool(q) * (0.55 + 0.45 * chord);\n    col += uHazeColor * haze * uHaze * uInside;\n    float e = length(q - vec2(0.0, 0.2)) / 1.2;\n    float edge = 0.5 * pow(smoothstep(0.62, 1.0, e), 1.4);\n    col += uEdgeColor * edge * uInside * (0.5 + 0.5 * uHaze);\n  }\n\n  float dr = r - R;\n  float halo = 1.0 - smoothstep(R * 0.05, R * 0.105, abs(dr - R * 0.0213));\n  col += uRimDeep * 0.18 * halo * topDim * uGlow * uBloom;\n  col += uRim * 0.12 * bell(dr + R * 0.0383, R * 0.032) * uGlow * uBloom;\n  col += uRim * 0.32 * bell(dr, max(R * 0.0117, uLine)) * uPresence * uUnfold;\n\n  float delta = mod(theta - uHeat.x + PI, TAU) - PI;\n  float heat = uHeat.z * uHeat.y * exp(-delta * delta / 0.3);\n  float crackle = uCrackle + heat * 0.9;\n  float amp = R * 0.016 * (0.3 + 1.4 * crackle) * uUnfold;\n  float jitterAmp = R * 0.0048 * (0.3 + 1.4 * uCrackle) * uUnfold * (1.0 + heat * 0.3);\n  float flareAmp = R * 0.064 * (0.4 + 1.2 * crackle) * uFlares * 1.6 * uUnfold;\n\n  if (abs(dr) < amp * 2.0 + flareAmp + R * 0.3) {\n    float step = TAU / uBins;\n    float binPos = (theta + PI) / step;\n    float bin0 = floor(binPos);\n    float binF = binPos - bin0;\n    float th0 = bin0 * step - PI;\n    float th1 = th0 + step;\n    uint b0 = uint(mod(bin0, uBins));\n    uint b1 = uint(mod(bin0 + 1.0, uBins));\n    float gain = min(1.0, 2.4 / max(uStrands, 1.0));\n    for (int k = 0; k < 8; k++) {\n      if (float(k) >= uStrands) break;\n      vec4 m = uHarmonics[k];\n      vec4 lead = uRates[k] * uTime + uPhases[k];\n      float wave0 = dot(AMPS, sin(m * th0 + lead));\n      float wave1 = dot(AMPS, sin(m * th1 + lead));\n      uint salt = uint(k) * 1013u + frame * 7919u;\n      vec4 shape = uFlareHarmonics[k];\n      vec4 look = uFlareRates[k];\n      vec4 tone = uFlarePhases[k];\n      float a = amp * shape.w;\n      float off0 = a * wave0 + jitterAmp * (random(b0 + salt) - 0.5);\n      float off1 = a * wave1 + jitterAmp * (random(b1 + salt) - 0.5);\n      float rc = R + mix(off0, off1, binF);\n      float grade = (off1 - off0) / (step * max(r, 1.0));\n      float d = abs(r - rc) * inversesqrt(1.0 + grade * grade);\n      float swell = 0.8 + 0.4 * (0.5 + 0.5 * sin(3.0 * theta + uTime * 0.7 + tone.x * 1.7));\n      float width = uLine * look.w * swell * (1.0 + heat * 0.25);\n      float core = exp(-d * d / (width * width));\n      float sheath = exp(-d * d / (width * width * 3.0));\n      float bright = tone.w * (1.0 + heat * 0.45) * uPresence;\n      col += (uRimHot * core * 0.8 + uRim * sheath * 0.32) * bright;\n      hot += core * bright;\n      vec3 fargs = shape.xyz * theta + look.xyz * uTime + tone.xyz;\n      float flare = max(0.0, (sin(fargs.x) + sin(fargs.y) + sin(fargs.z)) / 3.0);\n      flare = (flare * flare + heat * 0.35) * flareAmp;\n      float dw = abs(r - rc - flare);\n      float dm = abs(r - rc - flare * 0.6);\n      float dn = abs(r - rc - flare * 0.25);\n      vec3 glow = uRimDeep * (0.07 * bell(dw, R * 0.09) + 0.16 * bell(dw, R * 0.05));\n      glow += uRimMid * 0.28 * bell(dm, R * 0.027) + uRim * 0.38 * bell(dn, R * 0.013);\n      col += glow * gain * topDim * uGlow * uBloom * (1.0 + heat * 1.4);\n    }\n  }\n\n  for (int i = 0; i < 4; i++) {\n    vec4 arc = uArcs[i];\n    if (arc.w <= 0.002) continue;\n    float mid = arc.x + arc.y * 0.5;\n    vec2 center = R * 0.93 * vec2(cos(mid), sin(mid));\n    if (distance(p, center) > R * (abs(arc.y) * 0.6 + 0.1) + 12.0) continue;\n    float ar = R * 0.955;\n    vec2 a0 = ar * vec2(cos(arc.x), sin(arc.x));\n    vec2 a2 = ar * vec2(cos(arc.x + arc.y), sin(arc.x + arc.y));\n    vec2 a1 = R * (0.955 - arc.z) * vec2(cos(mid), sin(mid));\n    float dmin = 1e5;\n    float along = 0.0;\n    vec2 prev = a0;\n    for (int s = 1; s <= 10; s++) {\n      float u = float(s) / 10.0;\n      vec2 b = mix(mix(a0, a1, u), mix(a1, a2, u), u);\n      vec2 pa = p - prev;\n      vec2 ba = b - prev;\n      float h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-5), 0.0, 1.0);\n      float dist = length(pa - ba * h);\n      if (dist < dmin) {\n        dmin = dist;\n        along = (float(s) - 1.0 + h) / 10.0;\n      }\n      prev = b;\n    }\n    float taper = sqrt(max(sin(PI * along), 0.0));\n    float arcWidth = uLine * (0.55 + 0.6 * taper);\n    float arcCore = exp(-dmin * dmin / (arcWidth * arcWidth));\n    float arcGlow = exp(-dmin * dmin / (arcWidth * arcWidth * 12.0));\n    col += (uSpark * 0.95 * arcCore + uSparkGlow * 0.4 * arcGlow) * arc.w * taper;\n    hot += arcCore * arc.w * taper * 0.6;\n  }\n\n  fragColor = vec4(col, hot) * uEncode;\n}\n`;\n\nconst dustVertex = `#version 300 es\nprecision highp float;\n\nin float aIndex;\n\nuniform sampler2D tHome;\nuniform sampler2D tSeed;\nuniform sampler2D tOffset;\nuniform float uStirred;\nuniform vec2 uCenter;\nuniform vec2 uViewport;\nuniform float uRadius;\nuniform float uDpr;\nuniform float uDepth;\nuniform float uTwinkle;\nuniform float uPointScale;\nuniform float uReveal;\nuniform float uEncode;\nuniform vec3 uTones[5];\nuniform vec3 uRimTone;\n\nout vec3 vColor;\nout float vSize;\n${hashChunk}\n${dustChunk}\nvoid main() {\n  ivec2 cell = ivec2(int(mod(aIndex, ${STATE_WIDTH}.0)), int(floor(aIndex / ${STATE_WIDTH}.0)));\n  uint id = uint(aIndex);\n  vec4 home = texelFetch(tHome, cell, 0);\n  vec4 seed = texelFetch(tSeed, cell, 0);\n  float life;\n  float spread;\n  vec3 p = drift(home, seed, id, life, spread);\n  if (uStirred > 0.5) p += texelFetch(tOffset, cell, 0).xyz;\n  float len = length(p);\n  float inside = 1.0 - smoothstep(0.955, 0.985, length(p.xy));\n  if (len > 0.975) p *= 0.975 / len;\n\n  float front = p.z * 0.5 + 0.5;\n  float depthSize = mix(1.0, 0.7 + 0.6 * front, uDepth);\n  float depthLight = mix(1.0, 0.35 + 0.8 * front, uDepth);\n  float blink = sin(uDustTime * (2.0 + 6.0 * random(id * 7u + 5u)) + random(id * 7u + 6u) * 6.2831853);\n  float twinkle = mix(1.0, smoothstep(-0.9, -0.6, blink), uTwinkle);\n  float fade = smoothstep(0.04, 0.12, life);\n  float order = (home.y + 1.0) * 0.5 * 0.55 + random(id * 7u + 8u) * 0.25;\n  float reveal = smoothstep(order, order + 0.2, uReveal * 1.0);\n  float alpha = random(id * 7u + 9u) < 0.5 || home.w > 3.5 ? 1.0 : 0.6;\n\n  float size = seed.x * uPointScale * (0.35 + 0.95 * life) * depthSize * mix(0.5, 1.0, reveal);\n  vec2 screen = uCenter + p.xy * uRadius;\n  gl_Position = vec4(screen / uViewport * 2.0 - 1.0, 0.0, 1.0);\n  float px = size * uDpr;\n  gl_PointSize = px + 2.0;\n  vSize = px;\n\n  int tone = int(home.w + 0.5);\n  float rimLit = smoothstep(0.8, 0.97, length(p.xy));\n  vec3 color = uTones[tone] + uRimTone * rimLit * rimLit * 0.12;\n  vColor = color * alpha * fade * twinkle * depthLight * reveal * inside * sqrt(spread) * uEncode;\n}\n`;\n\nconst dustFragment = `#version 300 es\nprecision highp float;\n\nuniform float uShape;\n\nin vec3 vColor;\nin float vSize;\n\nout vec4 fragColor;\n\nvoid main() {\n  vec2 q = (gl_PointCoord - 0.5) * (vSize + 2.0);\n  float half_ = max(vSize * 0.5, 0.5);\n  float cover;\n  if (uShape < 0.5) {\n    vec2 c = clamp(half_ - abs(q) + 0.5, 0.0, 1.0);\n    cover = c.x * c.y;\n  } else {\n    cover = clamp(half_ - length(q) + 0.5, 0.0, 1.0);\n  }\n  if (cover <= 0.0) discard;\n  fragColor = vec4(vColor * cover, 0.0);\n}\n`;\n\nconst stirFragment = `#version 300 es\nprecision highp float;\n\nuniform sampler2D tHome;\nuniform sampler2D tSeed;\nuniform sampler2D tOffset;\nuniform sampler2D tVelocity;\nuniform float uDt;\nuniform float uReset;\nuniform vec4 uBrush;\nuniform float uBrushPower;\nuniform vec4 uKick;\n\nlayout(location = 0) out vec4 outOffset;\nlayout(location = 1) out vec4 outVelocity;\n${hashChunk}\n${dustChunk}\nvec3 swirl(vec3 p) {\n  float t = uDustTime * 0.15;\n  vec3 a = vec3(1.7, 1.9, 1.5) * p.yzx + vec3(t, 1.2, 2.1);\n  vec3 b = vec3(2.3, 2.1, 2.7) * p.zxy + vec3(0.4, t * 1.3, 0.9);\n  vec3 ca = cos(a) * vec3(1.7, 1.9, 1.5);\n  vec3 cb = cos(b) * vec3(2.3, 2.1, 2.7);\n  return vec3(ca.z - cb.x, cb.y - ca.x, ca.y - cb.z);\n}\n\nvoid main() {\n  ivec2 cell = ivec2(gl_FragCoord.xy);\n  if (uReset > 0.5) {\n    outOffset = vec4(0.0);\n    outVelocity = vec4(0.0);\n    return;\n  }\n  uint id = uint(cell.y * ${STATE_WIDTH} + cell.x);\n  vec4 home = texelFetch(tHome, cell, 0);\n  vec4 seed = texelFetch(tSeed, cell, 0);\n  vec3 o = texelFetch(tOffset, cell, 0).xyz;\n  vec3 v = texelFetch(tVelocity, cell, 0).xyz;\n  float life;\n  float spread;\n  vec3 base = drift(home, seed, id, life, spread);\n  vec3 p = base + o;\n\n  float k = mix(0.8, 2.2, random(id * 7u + 11u));\n  vec3 acc = -k * o - 1.5 * sqrt(k) * v;\n\n  vec2 gap = p.xy - uBrush.xy;\n  float touch = exp(-dot(gap, gap) / 0.07) * uBrushPower;\n  acc += (vec3(uBrush.zw, 0.0) - v) * touch * 14.0;\n  float pace = length(v);\n  if (pace > 1e-4) {\n    vec3 heading = v / pace;\n    vec3 curl = swirl(p * 1.6);\n    acc += (curl - heading * dot(curl, heading)) * min(pace, 2.0) * 1.8;\n  }\n\n  if (uKick.z > 0.0) {\n    vec3 jolt = vec3(random(id * 7u + 12u), random(id * 7u + 13u), random(id * 7u + 14u)) - 0.5;\n    float near = exp(-dot(p.xy - uKick.xy, p.xy - uKick.xy) / 0.4);\n    vec3 spinKick = vec3(-p.z, 0.0, p.x) * 0.9;\n    vec3 lift = vec3(0.0, 0.5 + 0.8 * random(id * 7u + 15u), 0.0) * (0.3 + 0.7 * smoothstep(0.2, -0.8, p.y));\n    v += (jolt * 0.8 + spinKick + lift) * uKick.z * (0.4 + 0.45 * near);\n  }\n\n  v += acc * uDt;\n  o += v * uDt;\n  vec3 q = base + o;\n  float len = length(q);\n  if (len > 0.97) {\n    vec3 n = q / len;\n    o -= n * (len - 0.97);\n    v -= n * max(dot(v, n), 0.0) * 1.5;\n  }\n  outOffset = vec4(o, 1.0);\n  outVelocity = vec4(v, 1.0);\n}\n`;\n\nconst compositeFragment = `#version 300 es\nprecision highp float;\n\nuniform sampler2D tScene;\nuniform float uLight;\nuniform float uDecode;\n\nin vec2 vUv;\nout vec4 fragColor;\n\nvec3 soften(vec3 x) {\n  vec3 over = max(x - 0.6, 0.0);\n  return min(x, 0.6) + 0.4 * (1.0 - exp(-over / 0.4));\n}\n\nvoid main() {\n  vec4 scene = texture(tScene, vUv) * uDecode;\n  vec3 light = max(scene.rgb, 0.0);\n  float peak = max(light.r, max(light.g, light.b));\n  if (uLight > 0.5) {\n    vec3 hue = light / max(peak, 1e-4);\n    float cover = 1.0 - exp(-peak * 2.4);\n    vec3 ink = hue * mix(0.92, 0.72, cover);\n    float core = clamp(scene.a * 0.7, 0.0, 1.0) * cover;\n    vec3 tint = mix(vec3(1.0), hue, 0.22);\n    fragColor = vec4(tint * core + ink * cover * (1.0 - core), core + cover * (1.0 - core));\n    return;\n  }\n  light += vec3(max(peak - 1.8, 0.0) * 0.25);\n  vec3 shown = soften(light);\n  fragColor = vec4(shown, max(shown.r, max(shown.g, shown.b)));\n}\n`;\n\nconst CrystalizedBall = ({\n  preset = 'plasma',\n  color,\n  theme = 'dark',\n  size = 0.7,\n  strands,\n  crackle,\n  flares,\n  glow,\n  sparks,\n  particleCount,\n  fill,\n  motion,\n  particleShape,\n  depth,\n  sway,\n  twinkle,\n  haze,\n  speed = 1,\n  dustSpeed,\n  interactive = true,\n  hoverStrength = 0.7,\n  intro = true,\n  paused = false,\n  className = '',\n  style\n}) => {\n  const containerRef = useRef(null);\n  const settingsRef = useRef(null);\n  const wakeRef = useRef(null);\n\n  const base = BALL_PRESETS[preset] || BALL_PRESETS.plasma;\n  const pick = (value, key) => (value === undefined || value === null ? base[key] : value);\n  const tint = pick(color, 'color');\n\n  const colors = useMemo(() => {\n    const light = theme === 'light';\n    return { light, palette: buildPalette(parseColor(tint, [0.95, 0.36, 0.82]), light) };\n  }, [tint, theme]);\n\n  useEffect(() => {\n    settingsRef.current = {\n      ...colors,\n      size,\n      strands: pick(strands, 'strands'),\n      crackle: pick(crackle, 'crackle'),\n      flares: pick(flares, 'flares'),\n      glow: pick(glow, 'glow'),\n      sparks: pick(sparks, 'sparks'),\n      particleCount: pick(particleCount, 'particleCount'),\n      fill: pick(fill, 'fill'),\n      motion: pick(motion, 'motion'),\n      particleShape: pick(particleShape, 'particleShape'),\n      depth: pick(depth, 'depth'),\n      sway: pick(sway, 'sway'),\n      twinkle: pick(twinkle, 'twinkle'),\n      haze: pick(haze, 'haze'),\n      dustSpeed: pick(dustSpeed, 'dustSpeed'),\n      speed,\n      interactive,\n      hoverStrength,\n      intro,\n      paused\n    };\n    wakeRef.current?.();\n  });\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return undefined;\n\n    const renderer = new Renderer({ alpha: true, premultipliedAlpha: true, antialias: false, depth: false });\n    const gl = renderer.gl;\n    if (!renderer.isWebgl2) {\n      gl.getExtension('WEBGL_lose_context')?.loseContext();\n      return undefined;\n    }\n    const canvas = gl.canvas;\n    canvas.style.display = 'block';\n    canvas.style.width = '100%';\n    canvas.style.height = '100%';\n    canvas.setAttribute('aria-hidden', 'true');\n    container.appendChild(canvas);\n\n    const fullFloat = !!gl.getExtension('EXT_color_buffer_float');\n    const halfFloat = fullFloat || !!gl.getExtension('EXT_color_buffer_half_float');\n    const encode = halfFloat ? 1 : 0.25;\n    const geometry = new Triangle(gl);\n    const blank = new Texture(gl);\n    const strandData = buildStrands();\n\n    const sceneTarget = new RenderTarget(gl, {\n      width: 1,\n      height: 1,\n      depth: false,\n      type: halfFloat ? gl.HALF_FLOAT : gl.UNSIGNED_BYTE,\n      format: gl.RGBA,\n      internalFormat: halfFloat ? gl.RGBA16F : gl.RGBA,\n      minFilter: gl.NEAREST,\n      magFilter: gl.NEAREST\n    });\n\n    const arcData = new Array(MAX_ARCS * 4).fill(0);\n    const fieldUniforms = {\n      uCenter: { value: [0, 0] },\n      uRadius: { value: 1 },\n      uDpr: { value: 1 },\n      uLine: { value: 0.6 },\n      uTime: { value: 0 },\n      uFrame: { value: 0 },\n      uBins: { value: 420 },\n      uStrands: { value: 5 },\n      uCrackle: { value: 0.6 },\n      uFlares: { value: 0.5 },\n      uGlow: { value: 0.8 },\n      uHaze: { value: 0.7 },\n      uFill: { value: 0.5 },\n      uPresence: { value: 0 },\n      uUnfold: { value: 0 },\n      uBloom: { value: 0 },\n      uInside: { value: 0 },\n      uEncode: { value: encode },\n      uRim: { value: [1, 1, 1] },\n      uRimHot: { value: [1, 1, 1] },\n      uRimMid: { value: [1, 1, 1] },\n      uRimDeep: { value: [1, 1, 1] },\n      uSpark: { value: [1, 1, 1] },\n      uSparkGlow: { value: [1, 1, 1] },\n      uHazeColor: { value: [0, 0, 0] },\n      uEdgeColor: { value: [0, 0, 0] },\n      uHeat: { value: [0, 0, 0, 0] },\n      uHarmonics: { value: strandData.harmonics },\n      uRates: { value: strandData.rates },\n      uPhases: { value: strandData.phases },\n      uFlareHarmonics: { value: strandData.flareHarmonics },\n      uFlareRates: { value: strandData.flareRates },\n      uFlarePhases: { value: strandData.flarePhases },\n      uArcs: { value: arcData }\n    };\n\n    const dustShared = {\n      uDustTime: { value: 0 },\n      uTurn: { value: [0, 0] },\n      uMotion: { value: 0 },\n      uFill: fieldUniforms.uFill,\n      tHome: { value: blank },\n      tSeed: { value: blank }\n    };\n\n    const dustUniforms = {\n      ...dustShared,\n      tOffset: { value: blank },\n      uStirred: { value: 0 },\n      uCenter: fieldUniforms.uCenter,\n      uViewport: { value: [1, 1] },\n      uRadius: fieldUniforms.uRadius,\n      uDpr: fieldUniforms.uDpr,\n      uDepth: { value: 0.6 },\n      uTwinkle: { value: 0.5 },\n      uPointScale: { value: 1 },\n      uReveal: { value: 0 },\n      uEncode: { value: encode },\n      uTones: { value: new Array(15).fill(1) },\n      uRimTone: { value: [1, 1, 1] },\n      uShape: { value: 0 }\n    };\n\n    const stirUniforms = {\n      ...dustShared,\n      tOffset: { value: blank },\n      tVelocity: { value: blank },\n      uDt: { value: 0.016 },\n      uReset: { value: 1 },\n      uBrush: { value: [0, 0, 0, 0] },\n      uBrushPower: { value: 0 },\n      uKick: { value: [0, 0, 0, 0] }\n    };\n\n    const compositeUniforms = {\n      tScene: { value: sceneTarget.texture },\n      uLight: { value: 0 },\n      uDecode: { value: 1 / encode }\n    };\n\n    const additive = program => {\n      program.setBlendFunc(gl.ONE, gl.ONE);\n      return program;\n    };\n\n    const fieldMesh = new Mesh(gl, {\n      geometry,\n      program: additive(\n        new Program(gl, {\n          vertex: passVertex,\n          fragment: fieldFragment,\n          uniforms: fieldUniforms,\n          transparent: true,\n          depthTest: false,\n          depthWrite: false\n        })\n      )\n    });\n    const dustProgram = additive(\n      new Program(gl, {\n        vertex: dustVertex,\n        fragment: dustFragment,\n        uniforms: dustUniforms,\n        transparent: true,\n        depthTest: false,\n        depthWrite: false\n      })\n    );\n    const stirMesh = halfFloat\n      ? new Mesh(gl, {\n          geometry,\n          program: new Program(gl, {\n            vertex: passVertex,\n            fragment: stirFragment,\n            uniforms: stirUniforms,\n            depthTest: false,\n            depthWrite: false\n          })\n        })\n      : null;\n    const compositeMesh = new Mesh(gl, {\n      geometry,\n      program: new Program(gl, {\n        vertex: passVertex,\n        fragment: compositeFragment,\n        uniforms: compositeUniforms,\n        depthTest: false,\n        depthWrite: false\n      })\n    });\n\n    const disposeTarget = target => {\n      if (!target) return;\n      gl.deleteFramebuffer(target.buffer);\n      target.textures.forEach(texture => gl.deleteTexture(texture.texture));\n    };\n\n    const stateTarget = rows =>\n      new RenderTarget(gl, {\n        width: STATE_WIDTH,\n        height: rows,\n        color: 2,\n        depth: false,\n        type: fullFloat ? gl.FLOAT : gl.HALF_FLOAT,\n        format: gl.RGBA,\n        internalFormat: fullFloat ? gl.RGBA32F : gl.RGBA16F,\n        minFilter: gl.NEAREST,\n        magFilter: gl.NEAREST\n      });\n\n    const dataTexture = (data, rows) =>\n      new Texture(gl, {\n        image: data,\n        width: STATE_WIDTH,\n        height: rows,\n        type: gl.FLOAT,\n        format: gl.RGBA,\n        internalFormat: gl.RGBA32F,\n        minFilter: gl.NEAREST,\n        magFilter: gl.NEAREST,\n        generateMipmaps: false,\n        flipY: false\n      });\n\n    let dust = null;\n\n    const disposeDust = () => {\n      if (!dust) return;\n      dust.mesh.geometry.remove();\n      gl.deleteTexture(dust.home.texture);\n      gl.deleteTexture(dust.seed.texture);\n      disposeTarget(dust.read);\n      disposeTarget(dust.write);\n      dust = null;\n    };\n\n    const resetStir = () => {\n      if (!stirMesh || !dust || !dust.read || !dust.write) return;\n      stirUniforms.tOffset.value = blank;\n      stirUniforms.tVelocity.value = blank;\n      stirUniforms.uReset.value = 1;\n      renderer.render({ scene: stirMesh, target: dust.read, clear: false });\n      renderer.render({ scene: stirMesh, target: dust.write, clear: false });\n      stirUniforms.uReset.value = 0;\n    };\n\n    const rebuildDust = requested => {\n      const total = clamp(Math.round(requested / 100) * 100, 0, MAX_PARTICLES);\n      if (dust && dust.requested === total) return;\n      disposeDust();\n      if (!total) {\n        dust = null;\n        return;\n      }\n      const data = buildDust(total);\n      const indices = new Float32Array(data.count);\n      for (let i = 0; i < data.count; i++) indices[i] = i;\n      const points = new Geometry(gl, { aIndex: { size: 1, data: indices } });\n      dust = {\n        requested: total,\n        rows: data.rows,\n        home: dataTexture(data.home, data.rows),\n        seed: dataTexture(data.seed, data.rows),\n        mesh: new Mesh(gl, { mode: gl.POINTS, geometry: points, program: dustProgram }),\n        read: stirMesh ? stateTarget(data.rows) : null,\n        write: stirMesh ? stateTarget(data.rows) : null\n      };\n      dustShared.tHome.value = dust.home;\n      dustShared.tSeed.value = dust.seed;\n      resetStir();\n      stirring = 0;\n    };\n\n    const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;\n    const random = seeded(913);\n\n    let width = 1;\n    let height = 1;\n    let raf = 0;\n    let last = performance.now();\n    let rimTime = 0;\n    let dustTime = 0;\n    let swayClock = 0;\n    let introClock = 0;\n    let visible = true;\n    let alive = true;\n    let surge = 0;\n    let stirring = 0;\n    let stirEnergy = 0;\n    let kick = null;\n    let nextArc = 0.3;\n    const arcs = [];\n    const pointer = { x: 0, y: 0, inside: false, fresh: true };\n    const heat = { angle: 0, velocity: 0, power: 0, near: 0 };\n    const brush = { x: 0, y: 0, vx: 0, vy: 0 };\n    const look = { x: 0, y: 0, vx: 0, vy: 0 };\n\n    let resizePending = false;\n    const applyResize = () => {\n      width = Math.max(1, container.clientWidth);\n      height = Math.max(1, container.clientHeight);\n      renderer.dpr = Math.min(window.devicePixelRatio || 1, 2, Math.sqrt(PIXEL_BUDGET / (width * height)));\n      renderer.setSize(width, height);\n      sceneTarget.setSize(gl.canvas.width, gl.canvas.height);\n    };\n\n    const resize = () => {\n      resizePending = true;\n      start();\n    };\n\n    const spawnArc = (angle, strength) => {\n      if (arcs.length >= MAX_ARCS) return;\n      arcs.push({\n        start: angle,\n        span: (0.05 + random() * 0.07) * (random() < 0.5 ? -1 : 1),\n        curl: 0.02 + random() * 0.04,\n        life: 0,\n        duration: 0.2 + random() * 0.2,\n        drift: (random() - 0.5) * 0.3,\n        strength\n      });\n    };\n\n    const frame = now => {\n      raf = 0;\n      if (resizePending) {\n        resizePending = false;\n        applyResize();\n      }\n      if (!alive) return;\n      const s = settingsRef.current;\n      const dt = Math.min(0.05, Math.max(1 / 240, (now - last) / 1000));\n      last = now;\n      if (!s) return;\n\n      const moving = !s.paused && !reducedMotion;\n      introClock = s.intro && !reducedMotion ? Math.min(1, introClock + dt / INTRO_SECONDS) : 1;\n      const presence = easeOut(smooth(0, 0.42, introClock));\n      const unfold = easeOut(smooth(0, 0.32, introClock));\n      const charge = Math.sin(Math.PI * smooth(0.22, 0.78, introClock)) * 0.35;\n      const radius = Math.max(8, (clamp(s.size, 0.1, 1.5) * Math.min(width, height)) / 2);\n\n      const strength = s.interactive && !reducedMotion ? clamp(s.hoverStrength, 0, 1) : 0;\n      const px = pointer.x - width / 2;\n      const py = height / 2 - pointer.y;\n      const reach = Math.hypot(px, py);\n      const target = pointer.inside ? strength : 0;\n      if (heat.power < 0.02) {\n        heat.angle = Math.atan2(py, px);\n        heat.velocity = 0;\n      }\n      const turn = wrapAngle(Math.atan2(py, px) - heat.angle);\n      heat.velocity += (120 * turn - 19 * heat.velocity) * dt;\n      heat.angle = wrapAngle(heat.angle + heat.velocity * dt);\n      heat.power += (target - heat.power) * (1 - Math.exp(-dt / (target > heat.power ? 0.3 : 0.55)));\n      const nearTarget = Math.exp(-Math.pow((reach - radius) / (radius * 0.45), 2));\n      heat.near += (nearTarget - heat.near) * (1 - Math.exp(-dt / 0.15));\n      surge *= Math.exp(-dt / 0.7);\n\n      const bx = px / radius;\n      const by = py / radius;\n      if (Math.hypot(bx - brush.x, by - brush.y) > 0.6) pointer.fresh = true;\n      if (pointer.fresh) {\n        brush.x = bx;\n        brush.y = by;\n        brush.vx = 0;\n        brush.vy = 0;\n        pointer.fresh = false;\n      }\n      const follow = 1 - Math.exp(-dt / 0.05);\n      brush.vx += ((bx - brush.x) / dt - brush.vx) * follow;\n      brush.vy += ((by - brush.y) / dt - brush.vy) * follow;\n      brush.x = bx;\n      brush.y = by;\n      const brushSpeed = Math.hypot(brush.vx, brush.vy);\n      const cap = brushSpeed > 4 ? 4 / brushSpeed : 1;\n      const inBall = Math.exp(-Math.max(0, Math.hypot(bx, by) - 1) * 6);\n      const brushPower = pointer.inside ? strength * inBall * presence : 0;\n      stirEnergy = stirEnergy * Math.exp(-dt / 1.2) + brushPower * Math.min(brushSpeed, 4) * dt;\n\n      if (moving) {\n        const pace = 1 + surge * 0.6 + heat.power * heat.near * 0.3;\n        const step = dt * Math.max(0, s.speed) * 2 * pace;\n        rimTime += step;\n        dustTime += dt * Math.max(0, s.dustSpeed) * (1 + surge * 0.4);\n        swayClock += dt * Math.max(0, s.dustSpeed);\n        const crackleNow = clamp(s.crackle + surge * 0.5, 0, 1.5);\n        if (s.sparks > 0.01 && introClock > 0.55) {\n          nextArc -= step * s.sparks * 2 * (0.6 + crackleNow * 0.6);\n          if (nextArc <= 0) {\n            const local = heat.power * heat.near;\n            const angle =\n              random() < local * 0.7\n                ? heat.angle + (random() - 0.5) * 0.8\n                : random() < 0.7\n                  ? Math.PI / 2 + (random() - 0.5) * 1.5\n                  : random() * Math.PI * 2;\n            spawnArc(angle, 0.8 + local * 0.6 + surge * 0.4);\n            nextArc = 0.15 + random() * 0.6;\n          }\n        }\n        for (let i = arcs.length - 1; i >= 0; i--) {\n          const arc = arcs[i];\n          arc.life += step;\n          arc.start += arc.drift * step;\n          if (arc.life >= arc.duration) arcs.splice(i, 1);\n        }\n      } else {\n        arcs.length = 0;\n      }\n      for (let i = 0; i < MAX_ARCS; i++) {\n        const arc = arcs[i];\n        arcData[i * 4] = arc ? arc.start : 0;\n        arcData[i * 4 + 1] = arc ? arc.span : 0;\n        arcData[i * 4 + 2] = arc ? arc.curl : 0;\n        arcData[i * 4 + 3] = arc ? Math.sin((Math.PI * arc.life) / arc.duration) * arc.strength : 0;\n      }\n\n      rebuildDust(s.particleCount);\n      const palette = s.palette;\n\n      fieldUniforms.uCenter.value = [width / 2, height / 2];\n      fieldUniforms.uRadius.value = radius;\n      fieldUniforms.uDpr.value = renderer.dpr;\n      fieldUniforms.uLine.value = Math.max(0.6, radius * 0.0035);\n      fieldUniforms.uTime.value = rimTime;\n      fieldUniforms.uFrame.value = Math.floor(rimTime * 24) % 100000;\n      fieldUniforms.uBins.value = clamp(Math.round((Math.PI * 2 * radius) / 2.6), 240, 900);\n      fieldUniforms.uStrands.value = Math.round(clamp(s.strands, 1, MAX_STRANDS));\n      fieldUniforms.uCrackle.value = clamp(s.crackle, 0, 1) + surge * 0.5 + charge;\n      fieldUniforms.uFlares.value = clamp(s.flares, 0, 1);\n      fieldUniforms.uGlow.value = clamp(s.glow, 0, 2) * (1 + surge * 0.5 + charge) * (s.light ? 0.35 : 1);\n      fieldUniforms.uHaze.value = clamp(s.haze, 0, 2) * (s.light ? 0.6 : 1);\n      fieldUniforms.uFill.value = clamp(s.fill, 0, 1);\n      fieldUniforms.uPresence.value = presence;\n      fieldUniforms.uUnfold.value = unfold;\n      fieldUniforms.uBloom.value = presence * presence;\n      fieldUniforms.uInside.value = smooth(0.2, 0.95, introClock);\n      fieldUniforms.uRim.value = palette.rim;\n      fieldUniforms.uRimHot.value = palette.rimHot;\n      fieldUniforms.uRimMid.value = palette.rimMid;\n      fieldUniforms.uRimDeep.value = palette.rimDeep;\n      fieldUniforms.uSpark.value = palette.spark;\n      fieldUniforms.uSparkGlow.value = palette.sparkGlow;\n      fieldUniforms.uHazeColor.value = palette.haze;\n      fieldUniforms.uEdgeColor.value = palette.edge;\n      fieldUniforms.uHeat.value = [heat.angle, heat.near, heat.power * presence, 0];\n\n      dustShared.uDustTime.value = dustTime;\n      const gaze = pointer.inside ? strength * presence : 0;\n      const lookX = clamp(bx, -1.6, 1.6) * gaze;\n      const lookY = clamp(by, -1.6, 1.6) * gaze;\n      look.vx += (40 * (lookX - look.x) - 11 * look.vx) * dt;\n      look.vy += (40 * (lookY - look.y) - 11 * look.vy) * dt;\n      look.x += look.vx * dt;\n      look.y += look.vy * dt;\n      const swing = clamp(s.sway, 0, 1);\n      dustShared.uTurn.value = [\n        swing * 0.42 * Math.sin(swayClock * 0.23) + look.x * 0.3,\n        swing * 0.12 * Math.sin(swayClock * 0.17 + 1.3) - look.y * 0.16\n      ];\n      dustShared.uMotion.value = MOTIONS[s.motion] ?? 0;\n      dustUniforms.uViewport.value = [width, height];\n      dustUniforms.uDepth.value = clamp(s.depth, 0, 1);\n      dustUniforms.uTwinkle.value = clamp(s.twinkle, 0, 1);\n      dustUniforms.uPointScale.value = Math.max(1, radius / 491);\n      dustUniforms.uReveal.value = smooth(0.18, 1, introClock);\n      dustUniforms.uTones.value = palette.tones.flat();\n      dustUniforms.uRimTone.value = palette.rim;\n      dustUniforms.uShape.value = SHAPES[s.particleShape] ?? 0;\n\n      const layer = dust;\n      if (layer && stirMesh && (stirEnergy > 0.002 || kick)) stirring = SETTLE_SECONDS;\n      let stirred = false;\n      if (layer && layer.read && layer.write && stirMesh && stirring > 0) {\n        stirUniforms.uDt.value = Math.min(dt, 1 / 30);\n        stirUniforms.uBrush.value = [brush.x, brush.y, brush.vx * cap, brush.vy * cap];\n        stirUniforms.uBrushPower.value = brushPower;\n        stirUniforms.uKick.value = kick ? [kick.x, kick.y, kick.strength, 0] : [0, 0, 0, 0];\n        stirUniforms.tOffset.value = layer.read.textures[0];\n        stirUniforms.tVelocity.value = layer.read.textures[1];\n        renderer.render({ scene: stirMesh, target: layer.write, clear: false });\n        const swap = layer.read;\n        layer.read = layer.write;\n        layer.write = swap;\n        stirring -= dt;\n        stirred = stirring > 0;\n        if (!stirred) {\n          stirring = 0;\n          resetStir();\n        }\n      }\n      kick = null;\n      dustUniforms.uStirred.value = stirred ? 1 : 0;\n      dustUniforms.tOffset.value = layer && layer.read ? layer.read.textures[0] : blank;\n\n      gl.clearColor(0, 0, 0, 0);\n      renderer.render({ scene: fieldMesh, target: sceneTarget, clear: true });\n      if (dust) renderer.render({ scene: dust.mesh, target: sceneTarget, clear: false });\n\n      compositeUniforms.uLight.value = s.light ? 1 : 0;\n      renderer.render({ scene: compositeMesh, clear: false });\n\n      const settling =\n        Math.abs(target - heat.power) > 0.002 ||\n        surge > 0.002 ||\n        stirring > 0 ||\n        Math.abs(heat.velocity) > 0.01 ||\n        Math.hypot(look.vx, look.vy, look.x - lookX, look.y - lookY) > 0.001;\n      if (visible && (moving || introClock < 1 || settling)) raf = requestAnimationFrame(frame);\n    };\n\n    const start = () => {\n      if (raf || !visible || !alive) return;\n      last = performance.now();\n      raf = requestAnimationFrame(frame);\n    };\n\n    const locate = e => {\n      const rect = container.getBoundingClientRect();\n      const x = e.clientX - rect.left;\n      const y = e.clientY - rect.top;\n      return { x, y, inside: x >= 0 && y >= 0 && x <= rect.width && y <= rect.height };\n    };\n\n    const onPointerMove = e => {\n      const spot = locate(e);\n      if (spot.inside && !pointer.inside) pointer.fresh = true;\n      pointer.x = spot.x;\n      pointer.y = spot.y;\n      pointer.inside = spot.inside;\n      if (spot.inside) start();\n    };\n\n    const onPointerDown = e => {\n      const s = settingsRef.current;\n      const spot = locate(e);\n      if (!s || !s.interactive || reducedMotion || !spot.inside) return;\n      if (!pointer.inside) pointer.fresh = true;\n      pointer.x = spot.x;\n      pointer.y = spot.y;\n      pointer.inside = true;\n      const radius = Math.max(8, (clamp(s.size, 0.1, 1.5) * Math.min(width, height)) / 2);\n      const kx = (spot.x - width / 2) / radius;\n      const ky = (height / 2 - spot.y) / radius;\n      const distance = Math.hypot(kx, ky);\n      if (distance > 1.3) return;\n      const strength = clamp(s.hoverStrength, 0, 1);\n      kick = { x: kx, y: ky, strength: strength * 1.2 };\n      surge = Math.max(surge, strength);\n      const angle = Math.atan2(ky, kx);\n      spawnArc(angle + (random() - 0.5) * 0.5, 1.3);\n      spawnArc(angle + Math.PI + (random() - 0.5) * 1.2, 1.1);\n      start();\n    };\n\n    const onPointerLeave = () => {\n      pointer.inside = false;\n      start();\n    };\n\n    const onPointerOut = e => {\n      if (!e.relatedTarget) onPointerLeave();\n    };\n\n    const onPointerUp = e => {\n      if (e.pointerType === 'touch') onPointerLeave();\n    };\n\n    const onVisibility = () => {\n      if (!document.hidden) start();\n    };\n\n    window.addEventListener('pointermove', onPointerMove, { passive: true });\n    window.addEventListener('pointerdown', onPointerDown, { passive: true });\n    window.addEventListener('pointerout', onPointerOut, { passive: true });\n    window.addEventListener('pointerup', onPointerUp, { passive: true });\n    window.addEventListener('blur', onPointerLeave);\n    document.addEventListener('visibilitychange', onVisibility);\n\n    const resizeObserver = new ResizeObserver(resize);\n    resizeObserver.observe(container);\n    const intersectionObserver = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting;\n      start();\n    });\n    intersectionObserver.observe(container);\n\n    wakeRef.current = start;\n    resize();\n\n    return () => {\n      alive = false;\n      visible = false;\n      cancelAnimationFrame(raf);\n      resizeObserver.disconnect();\n      intersectionObserver.disconnect();\n      window.removeEventListener('pointermove', onPointerMove);\n      window.removeEventListener('pointerdown', onPointerDown);\n      window.removeEventListener('pointerout', onPointerOut);\n      window.removeEventListener('pointerup', onPointerUp);\n      window.removeEventListener('blur', onPointerLeave);\n      document.removeEventListener('visibilitychange', onVisibility);\n      wakeRef.current = null;\n      disposeDust();\n      disposeTarget(sceneTarget);\n      gl.getExtension('WEBGL_lose_context')?.loseContext();\n      if (canvas.parentNode) canvas.parentNode.removeChild(canvas);\n    };\n  }, []);\n\n  return (\n    <div ref={containerRef} className={`relative w-full h-full overflow-hidden ${className}`.trim()} style={style} />\n  );\n};\n\nexport default CrystalizedBall;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"ogl@^1.0.11"
	]
}