{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "utu",
  "type": "registry:ui",
  "dependencies": [
    "clsx",
    "motion",
    "ogl",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "https://usva.build/r/atmospheres-core.json"
  ],
  "files": [
    {
      "path": "utu-field.ts",
      "target": "components/ui/utu-field.ts",
      "type": "registry:ui",
      "content": "/**\n * Pure math for Utu: the static field parameters, the slow breathing and\n * pointer-lean easing, and the plum -> orchid -> hot-magenta colour ramp. No DOM\n * and no sula imports, so the atmosphere stays genuinely standalone (its motion never\n * settles, so it borrows none of sula's spring/energy machinery either).\n */\n\nexport type Rgb = [number, number, number];\n\nconst TAU = Math.PI * 2;\n\nexport function clamp01(t: number): number {\n  return Math.min(1, Math.max(0, t));\n}\n\n/** Quintic smootherstep, the same easing the shader uses for its radial falloff. */\nexport function smoother(t: number): number {\n  const c = clamp01(t);\n  return c * c * c * (c * (c * 6 - 15) + 10);\n}\n\n/** Eased approach toward a target, per frame. Used for the pointer lean. */\nexport function approach(\n  current: number,\n  target: number,\n  ease: number,\n): number {\n  return current + (target - current) * ease;\n}\n\n/** How quickly the volume leans toward the cursor. Slow, so it drifts. */\nexport const LEAN_EASE = 0.06;\n\n/**\n * A very slow sine around a base value: the body inhales, its radius and band\n * count drifting so the contours never sit perfectly still.\n */\nexport function breathe(\n  elapsed: number,\n  base: number,\n  amount: number,\n  rate: number,\n): number {\n  return base * (1 + amount * Math.sin(TAU * rate * elapsed));\n}\n\n/** The static field description handed to the shader once, not per frame. */\nexport interface UtuParams {\n  /** Sphere radius in normalized (short-side) units. */\n  radius: number;\n  /** Contour count: how many glowing shells stack through the body. */\n  bands: number;\n  /** Twist per unit height, the helix strength. */\n  swirl: number;\n  /** Rotation speed of the whole body, rad/s. Slow. */\n  omega: number;\n  noiseFreq: number;\n  noiseAmp: number;\n  /** Floor density inside the body so fills stay dim-but-present. */\n  noiseBase: number;\n  /** How fast the noise field advects, so the texture travels with the twist. */\n  drift: number;\n  /** Vertical width of the equator wisp mask. */\n  wispSigma: number;\n  /** How far density leaks past the sphere into the tails. */\n  wispAmt: number;\n  /** Sideways drift speed of the wisps, so they read as shedding. */\n  wispDrift: number;\n  /** Beer-Lambert self-occlusion strength. */\n  absorb: number;\n  /** Tone-map exposure; hot cores clip toward white-magenta. */\n  exposure: number;\n  breathAmt: number;\n  breathRate: number;\n}\n\nexport const DEFAULT_PARAMS: UtuParams = {\n  radius: 1.65,\n  bands: 5,\n  swirl: 2.2,\n  omega: 0.12,\n  noiseFreq: 1.6,\n  noiseAmp: 0.85,\n  noiseBase: 0.22,\n  drift: 0.08,\n  wispSigma: 0.12,\n  wispAmt: 0.35,\n  wispDrift: 0.2,\n  absorb: 1.1,\n  exposure: 10,\n  breathAmt: 0.06,\n  breathRate: 0.05,\n};\n\nexport function resolveParams(overrides?: Partial<UtuParams>): UtuParams {\n  return { ...DEFAULT_PARAMS, ...overrides };\n}\n\nfunction clampChannel(v: number): number {\n  return Math.min(1, Math.max(0, v));\n}\n\n/** Scale an rgb toward black, for the deep valley colour. */\nexport function scaleRgb(color: Rgb, factor: number): Rgb {\n  return [\n    clampChannel(color[0] * factor),\n    clampChannel(color[1] * factor),\n    clampChannel(color[2] * factor),\n  ];\n}\n\n/** Push an rgb toward white, for the blown-out hot rim. */\nexport function mixWhite(color: Rgb, amount: number): Rgb {\n  const a = clamp01(amount);\n  return [\n    clampChannel(color[0] + (1 - color[0]) * a),\n    clampChannel(color[1] + (1 - color[1]) * a),\n    clampChannel(color[2] + (1 - color[2]) * a),\n  ];\n}\n\nexport interface UtuEmissionColors {\n  deep: Rgb;\n  mid: Rgb;\n  hot: Rgb;\n}\n\nexport interface UtuColors extends UtuEmissionColors {\n  /** kosteus: the hue the clay takes where the damp is deepest. */\n  pigment: Rgb;\n}\n\n/**\n * The default emission ramp: a dawn/dusk horizon sweep. Cool violet in the\n * valleys, magenta-rose through the body, warm gold at the hot cores, so the\n * sphere reads as a glow on the horizon rather than one flat tint.\n */\nexport const DAWN: UtuEmissionColors = {\n  deep: [0.2, 0.13, 0.42],\n  mid: [0.8, 0.3, 0.72],\n  hot: [1.0, 0.72, 0.52],\n};\n\n/** The dawn ramp with any stop overridden. Omitted stops keep the dawn colour. */\nexport function buildRamp(\n  overrides?: Partial<UtuEmissionColors>,\n): UtuEmissionColors {\n  return {\n    deep: overrides?.deep ?? DAWN.deep,\n    mid: overrides?.mid ?? DAWN.mid,\n    hot: overrides?.hot ?? DAWN.hot,\n  };\n}\n\n/**\n * A single-hue ramp from one accent, for callers who want the sphere to match a\n * brand colour instead of the dawn gradient. Deep sinks toward black, mid holds\n * the accent, hot blows toward white for the clipping cores.\n */\nexport function monoRamp(accent: Rgb): UtuEmissionColors {\n  return {\n    deep: scaleRgb(accent, 0.3),\n    mid: scaleRgb(accent, 0.95),\n    hot: mixWhite(accent, 0.3),\n  };\n}\n"
    },
    {
      "path": "utu-shader.ts",
      "target": "components/ui/utu-shader.ts",
      "type": "registry:ui",
      "content": "/**\n * Utu is a luminous fog volume, not glass. It raymarches an analytic\n * sphere and emits on the isolines of a drifting 3D noise field, so the body\n * reads as stacked glowing contour-shells you can see through, with wispy tails\n * shearing off the equator. Everything here is clean-room field math; it shares\n * nothing with sula-core's metaball shader (which is a lit glass surface, the\n * exact look this avoids). The one forbidden move is a fresnel silhouette rim.\n */\n\nimport { glsl } from \"./atmospheres-glsl\";\n\nexport const utuVertexShader = /* glsl */ `#version 300 es\nin vec2 position;\nvoid main() {\n  gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\n/** Fixed march depth. Baked as a constant so the loop stays uniform across the\n * quad, which keeps fwidth() (used for the isoline anti-alias) well defined. */\nconst STEPS = 32;\n\nexport const utuFragmentShader = /* glsl */ `#version 300 es\nprecision highp float;\n\nuniform float uTime;\nuniform vec2  uResolution;\nuniform float uRadius;\nuniform float uBands;\nuniform float uSwirl;\nuniform float uOmega;\nuniform float uNoiseFreq;\nuniform float uNoiseAmp;\nuniform float uNoiseBase;\nuniform float uDrift;\nuniform float uWispSigma;\nuniform float uWispAmt;\nuniform float uWispDrift;\nuniform float uExtinction;\nuniform float uAbsorb;\nuniform float uExposure;\nuniform vec3  uDeep;\nuniform vec3  uMid;\nuniform vec3  uHot;\nuniform vec3  uPigment;\nuniform float uStainFloor;\nuniform float uAlpha;\nuniform vec2  uLean;\nuniform float uLeanAmt;\n\nout vec4 fragColor;\n\n${glsl(\"stain\", \"composite\")}\n\nconst int STEPS = ${STEPS};\nconst float SIGMA = 1.05;\nconst float SOAK = 0.32;\n\nfloat hash13(vec3 p) {\n  p = fract(p * 0.1031);\n  p += dot(p, p.yzx + 33.33);\n  return fract((p.x + p.y) * p.z);\n}\n\nfloat vnoise(vec3 x) {\n  vec3 i = floor(x);\n  vec3 f = fract(x);\n  f = f * f * (3.0 - 2.0 * f);\n  float n000 = hash13(i + vec3(0.0, 0.0, 0.0));\n  float n100 = hash13(i + vec3(1.0, 0.0, 0.0));\n  float n010 = hash13(i + vec3(0.0, 1.0, 0.0));\n  float n110 = hash13(i + vec3(1.0, 1.0, 0.0));\n  float n001 = hash13(i + vec3(0.0, 0.0, 1.0));\n  float n101 = hash13(i + vec3(1.0, 0.0, 1.0));\n  float n011 = hash13(i + vec3(0.0, 1.0, 1.0));\n  float n111 = hash13(i + vec3(1.0, 1.0, 1.0));\n  return mix(\n    mix(mix(n000, n100, f.x), mix(n010, n110, f.x), f.y),\n    mix(mix(n001, n101, f.x), mix(n011, n111, f.x), f.y),\n    f.z\n  );\n}\n\nfloat fbm(vec3 p) {\n  float a = 0.5;\n  float s = 0.0;\n  for (int i = 0; i < 3; i++) {\n    s += a * vnoise(p);\n    p = p * 2.02 + vec3(11.3, 7.7, 3.1);\n    a *= 0.5;\n  }\n  return s;\n}\n\nfloat smoother(float t) {\n  t = clamp(t, 0.0, 1.0);\n  return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);\n}\n\nvec3 ramp(float d) {\n  float t = clamp(d, 0.0, 1.0);\n  vec3 lo = mix(uDeep, uMid, smoothstep(0.0, 0.55, t));\n  return mix(lo, uHot, smoothstep(0.55, 1.0, t));\n}\n\nvoid main() {\n  float m = min(uResolution.x, uResolution.y);\n  vec2 uv = (gl_FragCoord.xy * 2.0 - uResolution) / m;\n  uv -= uLean * uLeanAmt * 0.12;\n\n  float ym = exp(-(uv.y * uv.y) / max(uWispSigma, 1e-3));\n  float Rout = uRadius * (1.0 + uWispAmt * ym);\n\n  float rr2 = dot(uv, uv);\n  float core = uRadius * uRadius - rr2;\n  float zspan = core > 0.0 ? sqrt(core) : 0.0;\n\n  float wisp = uWispAmt * ym * uRadius * 0.6;\n  wisp *= 1.0 - smoothstep(uRadius * 0.4, Rout, length(uv));\n  zspan = max(zspan, wisp);\n\n  if (zspan <= 0.0) {\n    fragColor = vec4(0.0);\n    return;\n  }\n\n  float stepLen = (2.0 * zspan) / float(STEPS);\n  float dphase = uDrift * uTime;\n  vec3 col = vec3(0.0);\n  float T = 1.0;\n  float depth = 0.0;\n\n  for (int i = 0; i < STEPS; i++) {\n    float z = zspan - (float(i) + 0.5) * stepLen;\n    vec3 p = vec3(uv, z);\n\n    float ang = uSwirl * p.y + uOmega * uTime;\n    float s = sin(ang);\n    float c = cos(ang);\n    vec3 q = vec3(c * p.x - s * p.z, p.y, s * p.x + c * p.z);\n\n    vec3 qn = q;\n    qn.x *= mix(1.0, 0.65, ym);\n    qn.y *= mix(1.0, 1.35, ym);\n    qn += vec3(dphase * 0.5 + uWispDrift * uTime * ym, -dphase * 0.3, dphase);\n\n    float ax = p.x / (1.0 + uWispAmt * ym);\n    float rad = length(vec3(ax, p.y, p.z));\n    float shell = smoother(1.0 - rad / uRadius);\n\n    float n = fbm(qn * uNoiseFreq);\n    float density = max(shell * (uNoiseBase + uNoiseAmp * n), 0.0);\n    depth += density * stepLen;\n\n    float b = density * uBands;\n    float aa = fwidth(b) * 1.5 + 0.04;\n    float fb = fract(b);\n    float rim = smoothstep(0.5 - aa, 0.5, fb) * (1.0 - smoothstep(0.5, 0.5 + aa, fb));\n\n    float em = (rim * 1.6 + 0.18) * density;\n    col += T * em * ramp(density) * (1.0 / float(STEPS));\n    T *= exp(-density * uExtinction * stepLen);\n  }\n\n  col *= uExposure;\n  col = vec3(1.0) - exp(-col);\n  float peak = clamp(max(col.r, max(col.g, col.b)), 0.0, 1.0);\n  vec3 display = col / max(peak, 1e-4);\n  if (uAbsorb < 0.5) {\n    fragColor = composite(display, peak * uAlpha);\n    return;\n  }\n\n  vec3 absorbed = hold(uPigment, uStainFloor);\n  float alpha = clamp(soak(depth * SOAK, SIGMA) * uAlpha, 0.0, 1.0);\n  fragColor = vec4(absorbed * alpha, alpha);\n}\n`;\n"
    },
    {
      "path": "utu-uniforms.ts",
      "target": "components/ui/utu-uniforms.ts",
      "type": "registry:ui",
      "content": "import { MAX_STAIN } from \"./atmospheres-color\";\nimport {\n  setUniform,\n  type Uniforms,\n} from \"./atmospheres-gl\";\nimport type { UtuColors, UtuParams } from \"./utu-field\";\n\n/** The per-frame values: everything else is set on change, not per frame. */\nexport interface UtuFrame {\n  /** Seconds since start. */\n  time: number;\n  /** Breathing radius for this frame. */\n  radius: number;\n  /** Breathing band count for this frame. */\n  bands: number;\n  /** Eased pointer offset in normalized units; [0,0] when not interacting. */\n  lean: [number, number];\n  /** Fades the lean in and out, 0..1. */\n  leanAmt: number;\n  alpha: number;\n  /** The house law: 1 stains the ground, 0 emits into it. */\n  absorb: number;\n}\n\nexport function utuUniforms(colors: UtuColors, params: UtuParams): Uniforms {\n  return {\n    uTime: { value: 0 },\n    uResolution: { value: [1, 1] },\n    uRadius: { value: params.radius },\n    uBands: { value: params.bands },\n    uSwirl: { value: params.swirl },\n    uOmega: { value: params.omega },\n    uNoiseFreq: { value: params.noiseFreq },\n    uNoiseAmp: { value: params.noiseAmp },\n    uNoiseBase: { value: params.noiseBase },\n    uDrift: { value: params.drift },\n    uWispSigma: { value: params.wispSigma },\n    uWispAmt: { value: params.wispAmt },\n    uWispDrift: { value: params.wispDrift },\n    uExtinction: { value: params.absorb },\n    uAbsorb: { value: 0 },\n    uExposure: { value: params.exposure },\n    uDeep: { value: colors.deep },\n    uMid: { value: colors.mid },\n    uHot: { value: colors.hot },\n    uPigment: { value: colors.pigment },\n    uStainFloor: { value: MAX_STAIN },\n    uAlpha: { value: 1 },\n    uLean: { value: [0, 0] },\n    uLeanAmt: { value: 0 },\n  };\n}\n\nexport function setUtuColors(u: Uniforms, colors: UtuColors): void {\n  setUniform(u, \"uDeep\", colors.deep);\n  setUniform(u, \"uMid\", colors.mid);\n  setUniform(u, \"uHot\", colors.hot);\n  setUniform(u, \"uPigment\", colors.pigment);\n}\n\nexport function setUtuParams(u: Uniforms, params: UtuParams): void {\n  setUniform(u, \"uSwirl\", params.swirl);\n  setUniform(u, \"uOmega\", params.omega);\n  setUniform(u, \"uNoiseFreq\", params.noiseFreq);\n  setUniform(u, \"uNoiseAmp\", params.noiseAmp);\n  setUniform(u, \"uNoiseBase\", params.noiseBase);\n  setUniform(u, \"uDrift\", params.drift);\n  setUniform(u, \"uWispSigma\", params.wispSigma);\n  setUniform(u, \"uWispAmt\", params.wispAmt);\n  setUniform(u, \"uWispDrift\", params.wispDrift);\n  setUniform(u, \"uExtinction\", params.absorb);\n  setUniform(u, \"uExposure\", params.exposure);\n}\n\nexport function setUtuFrame(u: Uniforms, frame: UtuFrame): void {\n  setUniform(u, \"uTime\", frame.time);\n  setUniform(u, \"uRadius\", frame.radius);\n  setUniform(u, \"uBands\", frame.bands);\n  setUniform(u, \"uLean\", frame.lean);\n  setUniform(u, \"uLeanAmt\", frame.leanAmt);\n  setUniform(u, \"uAlpha\", frame.alpha);\n  setUniform(u, \"uAbsorb\", frame.absorb);\n}\n"
    },
    {
      "path": "utu.tsx",
      "target": "components/ui/utu.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  type BlendMode,\n  blendStyleFor,\n  blendUniform,\n  pigmentFor,\n  resolveBlendMode,\n  resolveColor,\n} from \"./atmospheres-color\";\nimport { useGlCanvas } from \"./use-gl-canvas\";\nimport {\n  useThemeVersion,\n  useTokenColors,\n} from \"./use-token-colors\";\nimport {\n  approach,\n  breathe,\n  buildRamp,\n  LEAN_EASE,\n  monoRamp,\n  resolveParams,\n  type UtuColors,\n  type UtuEmissionColors,\n  type UtuParams,\n} from \"./utu-field\";\nimport { utuFragmentShader } from \"./utu-shader\";\nimport {\n  setUtuColors,\n  setUtuFrame,\n  setUtuParams,\n  utuUniforms,\n} from \"./utu-uniforms\";\n\nconst ROLES = [\"ink\"] as const;\n\nexport interface UtuProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Rotation and breath rate multiplier; higher turns faster. Defaults to 1. */\n  speed?: number;\n  /** When on, the volume leans toward the eased cursor. Defaults to false. */\n  interactive?: boolean;\n  /** Contour count: how many glowing shells stack through the body. */\n  bands?: number;\n  /** Collapse the dawn gradient to a single brand colour instead. */\n  accentColor?: string;\n  /** Override any dawn gradient stop with a CSS colour. Omitted stops keep the\n   * dawn default (violet valleys, magenta body, warm-gold cores). */\n  colors?: { deep?: string; mid?: string; hot?: string };\n  /** Overall opacity of the fog, 0..1. Lower lets more of the page through.\n   * Defaults to 1. */\n  opacity?: number;\n  /** Force the material. Defaults to fog on a dark ground and damp pigment on\n   * a light one. */\n  mode?: BlendMode;\n  /** Escape hatch for the field parameters, for tuning demos. */\n  params?: Partial<UtuParams>;\n  children?: React.ReactNode;\n}\n\ninterface ColorOverrides {\n  accentColor?: string;\n  deep?: string;\n  mid?: string;\n  hot?: string;\n}\n\nfunction readColors(overrides: ColorOverrides): UtuEmissionColors {\n  const stop = (c?: string) => (c ? resolveColor(c) : undefined);\n  const stops = {\n    deep: stop(overrides.deep),\n    mid: stop(overrides.mid),\n    hot: stop(overrides.hot),\n  };\n  if (overrides.accentColor) {\n    const base = monoRamp(resolveColor(overrides.accentColor));\n    return {\n      deep: stops.deep ?? base.deep,\n      mid: stops.mid ?? base.mid,\n      hot: stops.hot ?? base.hot,\n    };\n  }\n  return buildRamp(stops);\n}\n\nexport const Utu = React.forwardRef<HTMLDivElement, UtuProps>(\n  (\n    {\n      speed = 1,\n      interactive = false,\n      bands,\n      accentColor,\n      colors,\n      opacity = 1,\n      mode,\n      params,\n      className,\n      children,\n      ...props\n    },\n    forwardedRef,\n  ) => {\n    const cDeep = colors?.deep;\n    const cMid = colors?.mid;\n    const cHot = colors?.hot;\n\n    /* These tune what the loop draws, not the GL context, so they ride refs: a\n     * live prop change must never tear the context down and rebuild it on the\n     * same canvas, which would race a scheduled frame. */\n    const speedRef = React.useRef(speed);\n    speedRef.current = speed;\n    const interactiveRef = React.useRef(interactive);\n    interactiveRef.current = interactive;\n    const opacityRef = React.useRef(opacity);\n    opacityRef.current = opacity;\n\n    const paramsRef = React.useRef<UtuParams>(\n      resolveParams({ ...params, ...(bands !== undefined ? { bands } : {}) }),\n    );\n    paramsRef.current = resolveParams({\n      ...params,\n      ...(bands !== undefined ? { bands } : {}),\n    });\n\n    const themeVersion = useThemeVersion();\n    const scopeRef = React.useRef<HTMLDivElement | null>(null);\n    const tokens = useTokenColors(ROLES, { scopeRef });\n    const blend = resolveBlendMode(mode, tokens.bg);\n    // biome-ignore lint/correctness/useExhaustiveDependencies: a theme swap re-resolves the same colour strings to new channels.\n    const ramp = React.useMemo<UtuColors>(() => {\n      const emission = readColors({\n        accentColor,\n        deep: cDeep,\n        mid: cMid,\n        hot: cHot,\n      });\n      return {\n        ...emission,\n        pigment: pigmentFor(emission.mid, tokens.colors.ink),\n      };\n    }, [accentColor, cDeep, cMid, cHot, tokens, themeVersion]);\n    const rampRef = React.useRef(ramp);\n    rampRef.current = ramp;\n    const blendRef = React.useRef(blend);\n    blendRef.current = blend;\n\n    const lean = React.useRef<[number, number]>([0, 0]);\n\n    const canvas = useGlCanvas({\n      fragment: utuFragmentShader,\n      uniforms: () => utuUniforms(rampRef.current, paramsRef.current),\n      pointer: true,\n      pointerEase: LEAN_EASE,\n      maxDpr: 1.5,\n      renderScale: 0.8,\n      onFrame: (u, frame) => {\n        const p = paramsRef.current;\n        const elapsed = frame.time;\n        const radius = breathe(elapsed, p.radius, p.breathAmt, p.breathRate);\n        const bandsNow = breathe(\n          elapsed,\n          p.bands,\n          p.breathAmt * 0.6,\n          p.breathRate,\n        );\n        if (interactiveRef.current) {\n          const short = Math.min(frame.width, frame.height) || 1;\n          lean.current[0] = approach(\n            lean.current[0],\n            (frame.pointer.x / short) * 2,\n            LEAN_EASE,\n          );\n          lean.current[1] = approach(\n            lean.current[1],\n            (frame.pointer.y / short) * 2,\n            LEAN_EASE,\n          );\n        }\n        setUtuColors(u, rampRef.current);\n        setUtuParams(u, p);\n        setUtuFrame(u, {\n          time: elapsed * speedRef.current,\n          radius,\n          bands: bandsNow,\n          lean: lean.current,\n          leanAmt: interactiveRef.current ? frame.pointer.amount : 0,\n          alpha: opacityRef.current,\n          absorb: blendUniform(blendRef.current),\n        });\n      },\n    });\n\n    const { redraw } = canvas;\n    // biome-ignore lint/correctness/useExhaustiveDependencies: the still frame must repaint when the ramp changes.\n    React.useEffect(() => {\n      redraw();\n    }, [redraw, ramp, blend]);\n\n    const sphereOn = canvas.active;\n\n    return (\n      <div\n        ref={(node) => {\n          canvas.containerRef.current = node;\n          scopeRef.current = node;\n          if (typeof forwardedRef === \"function\") forwardedRef(node);\n          else if (forwardedRef) forwardedRef.current = node;\n        }}\n        data-fluid={sphereOn ? \"on\" : \"off\"}\n        data-blend={blend}\n        className={cn(\"relative isolate overflow-hidden\", className)}\n        {...props}\n      >\n        {sphereOn ? (\n          <div\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute inset-0 -z-10\"\n          >\n            <canvas\n              ref={canvas.canvasRef}\n              className=\"block h-full w-full\"\n              style={blendStyleFor(blend)}\n            />\n          </div>\n        ) : null}\n        {children}\n      </div>\n    );\n  },\n);\nUtu.displayName = \"Utu\";\n"
    }
  ]
}