{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "loimu",
  "type": "registry:ui",
  "dependencies": [
    "clsx",
    "motion",
    "ogl",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "https://usva.build/r/atmospheres-core.json"
  ],
  "files": [
    {
      "path": "loimu-field.ts",
      "target": "components/ui/loimu-field.ts",
      "type": "registry:ui",
      "content": "import type { Rgb } from \"./atmospheres-color\";\n\n/**\n * loimu is a blaze: a light sheet hanging in space, lit from a source the size\n * of a weather system sitting just past the top right corner. Nothing here is a\n * vignette. The source is a real point in front of the sheet, the streamers are\n * anisotropic noise advected by a divergence-free curl field, and the hue rides\n * the distance already travelled along the streamline.\n */\nexport interface LoimuParams {\n  /** Camera focal length. Larger flattens the perspective. */\n  focal: number;\n  /** Distance from the eye to the sheet plane along its normal. */\n  sheetDist: number;\n  /** Half depth of the marched neighbourhood around the sheet. */\n  sheetSpan: number;\n  /** Gaussian thickness of the sheet. */\n  sigma: number;\n  /** How far the low-frequency fold bends the sheet out of plane. */\n  fold: number;\n  foldScale: number;\n  /** Sheet plane normal, normalised in the shader. */\n  normal: [number, number, number];\n  /** Flow axis. Projected onto the sheet plane in the shader. */\n  flow: [number, number, number];\n  /** Emission source, in half-height units of screen space, x scaled by aspect. */\n  source: [number, number];\n  noiseFreq: number;\n  /** Domain stretch along the flow axis. Below ~4 this stops reading as aurora. */\n  stretch: number;\n  curlScale: number;\n  curlAmt: number;\n  flowSpeed: number;\n  /** Strength of the pointer vortex added to the curl field. */\n  omega: number;\n  threshold: number;\n  sharpen: number;\n  /** Inverse-square arrival from the source. Higher decays to void sooner. */\n  falloff: number;\n  gain: number;\n  /** Weight of the thin leading line where one streamer edge slides over another. */\n  edge: number;\n  edgeBands: number;\n  /** Distance along the flow over which the hue completes its ramp. */\n  flowLength: number;\n}\n\nexport const LOIMU_DEFAULTS: LoimuParams = {\n  focal: 1.5,\n  sheetDist: 3,\n  sheetSpan: 1.6,\n  sigma: 0.8,\n  fold: 0.55,\n  foldScale: 0.22,\n  normal: [0.3, 0.4, 0.87],\n  flow: [-0.86, -0.5, 0],\n  source: [1.28, 0.92],\n  noiseFreq: 1.5,\n  stretch: 12,\n  curlScale: 0.35,\n  curlAmt: 0.7,\n  flowSpeed: 0.28,\n  omega: 1.6,\n  threshold: 0.33,\n  sharpen: 2.2,\n  falloff: 0.045,\n  gain: 14,\n  edge: 0.3,\n  edgeBands: 2,\n  flowLength: 7,\n};\n\nexport interface LoimuColors {\n  /** The body of the sheet. */\n  body: Rgb;\n  /** Where the light is oldest and thinnest. */\n  deep: Rgb;\n  /** The thin line on a streamer's leading edge. */\n  edge: Rgb;\n}\n\nexport const POINTER_EASE = 0.05;\n\nexport function approach(\n  current: number,\n  target: number,\n  ease: number,\n): number {\n  return current + (target - current) * ease;\n}\n\nexport function resolveParams(overrides?: Partial<LoimuParams>): LoimuParams {\n  return { ...LOIMU_DEFAULTS, ...overrides };\n}\n"
    },
    {
      "path": "loimu-shader.ts",
      "target": "components/ui/loimu-shader.ts",
      "type": "registry:ui",
      "content": "import { glsl } from \"./atmospheres-glsl\";\n\n/** Eight taps. The sheet is thin, so a long march would only oversample void. */\nconst TAPS = 8;\n\nexport const loimuFragmentShader = /* glsl */ `#version 300 es\nprecision highp float;\n\nuniform float uTime;\nuniform vec2  uResolution;\nuniform vec2  uMouse;\nuniform float uPointer;\nuniform float uFocal;\nuniform float uSheetDist;\nuniform float uSheetSpan;\nuniform float uSigma;\nuniform float uFold;\nuniform float uFoldScale;\nuniform vec3  uNormal;\nuniform vec3  uFlow;\nuniform vec2  uSource;\nuniform float uNoiseFreq;\nuniform float uStretch;\nuniform float uCurlScale;\nuniform float uCurlAmt;\nuniform float uFlowSpeed;\nuniform float uOmega;\nuniform float uThreshold;\nuniform float uSharpen;\nuniform float uFalloff;\nuniform float uGain;\nuniform float uEdge;\nuniform float uEdgeBands;\nuniform float uFlowLength;\nuniform float uAlpha;\nuniform float uAbsorb;\nuniform vec3  uBody;\nuniform vec3  uDeep;\nuniform vec3  uEdgeColor;\n\nout vec4 fragColor;\n\n${glsl(\"fbm\", \"curl\", \"isoband\", \"dither\", \"tonemap\", \"composite\")}\n\n/** Where a ray leaving the eye crosses the sheet plane. Grazing rays would run\n * to infinity, so the denominator is floored rather than discarded: the arrival\n * falloff has already taken those pixels to zero anyway. */\nfloat sheetHit(vec3 dir, vec3 nrm) {\n  return uSheetDist / max(dot(dir, nrm), 0.08);\n}\n\nvoid main() {\n  float aspect = uResolution.x / max(uResolution.y, 1.0);\n  vec2 ndc = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;\n\n  vec3 dir = normalize(vec3(ndc, uFocal));\n  vec3 nrm = normalize(uNormal);\n  vec3 flowAxis = normalize(uFlow - nrm * dot(uFlow, nrm));\n  vec3 crossAxis = cross(nrm, flowAxis);\n\n  vec3 srcDir = normalize(vec3(uSource.x * aspect, uSource.y, uFocal));\n  vec3 source = srcDir * sheetHit(srcDir, nrm);\n  vec3 mouseDir = normalize(vec3(uMouse, uFocal));\n  vec3 mouse = mouseDir * sheetHit(mouseDir, nrm);\n\n  float hit = sheetHit(dir, nrm);\n  float stepLen = (2.0 * uSheetSpan) / float(${TAPS});\n  float jitter = ign(gl_FragCoord.xy);\n\n  vec3 col = vec3(0.0);\n  float acc = 0.0;\n\n  vec3 sheetPoint = dir * hit;\n  vec3 vel = curl(sheetPoint * uCurlScale + vec3(0.0, 0.0, uTime * 0.05), 0.35);\n  vec2 baseVel = vec2(dot(vel, flowAxis), dot(vel, crossAxis));\n\n  for (int i = 0; i < ${TAPS}; i++) {\n    float t = hit - uSheetSpan + (float(i) + jitter) * stepLen;\n    vec3 p = dir * t;\n\n    float bend = fbm(p * uFoldScale + vec3(0.0, 0.0, uTime * 0.02), 2) - 0.5;\n    float s = dot(p, nrm) - uSheetDist - bend * uFold;\n    float shell = exp(-(s * s) / (uSigma * uSigma));\n\n    if (shell < 0.002) continue;\n\n    vec3 toMouse = p - mouse;\n    vec2 mAB = vec2(dot(toMouse, flowAxis), dot(toMouse, crossAxis));\n    vec2 sheetVel = baseVel + clamp(\n      uPointer * uOmega * vec2(-mAB.y, mAB.x) / (1.0 + dot(mAB, mAB)),\n      vec2(-0.65),\n      vec2(0.65)\n    );\n\n    vec2 disp = sheetVel * uCurlAmt;\n    vec3 q = p + disp.x * flowAxis + disp.y * crossAxis\n           - flowAxis * (uFlowSpeed * uTime);\n\n    vec3 domain = vec3(\n      dot(q, flowAxis) / uStretch,\n      dot(q, crossAxis),\n      dot(q, nrm) * 0.6\n    ) * uNoiseFreq;\n\n    float fb = fbm(domain, 3);\n    float along = domain.x;\n    float across = domain.y;\n    float tear = (fbm(vec3(along * 0.32, across * 1.8, domain.z), 3) - 0.5) * 0.6;\n    float d0 = across + 0.62 + tear;\n    float d1 = across - 0.05 + tear * 0.7;\n    float d2 = across - 0.72 - tear * 0.5;\n    float lanes = exp(-5.0 * d0 * d0)\n      + 0.85 * exp(-7.0 * d1 * d1)\n      + 0.65 * exp(-6.0 * d2 * d2);\n    float body = pow(smoothstep(uThreshold, 0.9, fb), uSharpen);\n    float rayWave = 0.5 + 0.5 * sin(along * 18.0 - uTime * 1.4);\n    float rays = 0.72 + 0.28 * rayWave * rayWave * rayWave;\n    float density = shell * (0.15 + 0.85 * lanes) * (0.08 + 0.92 * body) * rays;\n\n    float flow = clamp(dot(q - source, flowAxis) / uFlowLength, 0.0, 1.0);\n    vec3 hue = mix(uBody, uDeep, flow);\n    hue += uEdgeColor * isobands(fb, uEdgeBands) * uEdge;\n\n    float reach = length(p - source);\n    float alongReach = dot(p - source, flowAxis);\n    float launched = smoothstep(-1.2, 0.8, alongReach);\n    float arrival = launched / (1.0 + uFalloff * reach * reach);\n\n    col += hue * density * arrival * stepLen;\n    acc += density * arrival * stepLen;\n  }\n\n  col *= uGain;\n  float amount = clamp(acc * uGain, 0.0, 1.0);\n\n  // Tonemapped on luminance, not per channel, so a bright pass saturates\n  // toward the accent instead of washing to white. sisu must not bloom.\n  float lum = max(col.r, max(col.g, col.b));\n  float peak = 1.0 - exp(-lum);\n  vec3 tint = col / max(lum, 1e-4);\n\n  vec3 rgb = dither(mix(tint, tint * 0.78, uAbsorb), gl_FragCoord.xy, 0.006);\n  float alpha = mix(peak, amount * 0.85, uAbsorb) * uAlpha;\n  alpha = ditherAlpha(alpha, gl_FragCoord.xy, 0.004);\n\n  fragColor = composite(rgb, alpha);\n}\n`;\n"
    },
    {
      "path": "loimu-uniforms.ts",
      "target": "components/ui/loimu-uniforms.ts",
      "type": "registry:ui",
      "content": "import {\n  setUniform,\n  type Uniforms,\n} from \"./atmospheres-gl\";\nimport type { LoimuColors, LoimuParams } from \"./loimu-field\";\n\nexport interface LoimuFrame {\n  time: number;\n  /** Eased pointer in half-height screen units, y up. */\n  mouse: [number, number];\n  /** Fades the vortex in and out, 0..1. */\n  pointer: number;\n  alpha: number;\n  /** The house law: 1 stains the ground, 0 emits into it. */\n  absorb: number;\n}\n\nexport function loimuUniforms(\n  colors: LoimuColors,\n  params: LoimuParams,\n): Uniforms {\n  return {\n    uTime: { value: 0 },\n    uResolution: { value: [1, 1] },\n    uMouse: { value: [0, 0] },\n    uPointer: { value: 0 },\n    uFocal: { value: params.focal },\n    uSheetDist: { value: params.sheetDist },\n    uSheetSpan: { value: params.sheetSpan },\n    uSigma: { value: params.sigma },\n    uFold: { value: params.fold },\n    uFoldScale: { value: params.foldScale },\n    uNormal: { value: [...params.normal] },\n    uFlow: { value: [...params.flow] },\n    uSource: { value: [...params.source] },\n    uNoiseFreq: { value: params.noiseFreq },\n    uStretch: { value: params.stretch },\n    uCurlScale: { value: params.curlScale },\n    uCurlAmt: { value: params.curlAmt },\n    uFlowSpeed: { value: params.flowSpeed },\n    uOmega: { value: params.omega },\n    uThreshold: { value: params.threshold },\n    uSharpen: { value: params.sharpen },\n    uFalloff: { value: params.falloff },\n    uGain: { value: params.gain },\n    uEdge: { value: params.edge },\n    uEdgeBands: { value: params.edgeBands },\n    uFlowLength: { value: params.flowLength },\n    uAlpha: { value: 1 },\n    uAbsorb: { value: 0 },\n    uBody: { value: [...colors.body] },\n    uDeep: { value: [...colors.deep] },\n    uEdgeColor: { value: [...colors.edge] },\n  };\n}\n\nexport function setLoimuColors(u: Uniforms, colors: LoimuColors): void {\n  setUniform(u, \"uBody\", [...colors.body]);\n  setUniform(u, \"uDeep\", [...colors.deep]);\n  setUniform(u, \"uEdgeColor\", [...colors.edge]);\n}\n\nexport function setLoimuParams(u: Uniforms, params: LoimuParams): void {\n  setUniform(u, \"uFocal\", params.focal);\n  setUniform(u, \"uSheetDist\", params.sheetDist);\n  setUniform(u, \"uSheetSpan\", params.sheetSpan);\n  setUniform(u, \"uSigma\", params.sigma);\n  setUniform(u, \"uFold\", params.fold);\n  setUniform(u, \"uFoldScale\", params.foldScale);\n  setUniform(u, \"uNormal\", [...params.normal]);\n  setUniform(u, \"uFlow\", [...params.flow]);\n  setUniform(u, \"uSource\", [...params.source]);\n  setUniform(u, \"uNoiseFreq\", params.noiseFreq);\n  setUniform(u, \"uStretch\", params.stretch);\n  setUniform(u, \"uCurlScale\", params.curlScale);\n  setUniform(u, \"uCurlAmt\", params.curlAmt);\n  setUniform(u, \"uFlowSpeed\", params.flowSpeed);\n  setUniform(u, \"uOmega\", params.omega);\n  setUniform(u, \"uThreshold\", params.threshold);\n  setUniform(u, \"uSharpen\", params.sharpen);\n  setUniform(u, \"uFalloff\", params.falloff);\n  setUniform(u, \"uGain\", params.gain);\n  setUniform(u, \"uEdge\", params.edge);\n  setUniform(u, \"uEdgeBands\", params.edgeBands);\n  setUniform(u, \"uFlowLength\", params.flowLength);\n}\n\nexport function setLoimuFrame(u: Uniforms, frame: LoimuFrame): void {\n  setUniform(u, \"uTime\", frame.time);\n  setUniform(u, \"uMouse\", [...frame.mouse]);\n  setUniform(u, \"uPointer\", frame.pointer);\n  setUniform(u, \"uAlpha\", frame.alpha);\n  setUniform(u, \"uAbsorb\", frame.absorb);\n}\n"
    },
    {
      "path": "loimu.tsx",
      "target": "components/ui/loimu.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  type Rgb,\n  resolveBlendMode,\n  resolveColor,\n} from \"./atmospheres-color\";\nimport { hiddenOnGround } from \"./atmospheres-ground\";\nimport { useGlCanvas } from \"./use-gl-canvas\";\nimport {\n  useThemeVersion,\n  useTokenColors,\n} from \"./use-token-colors\";\nimport {\n  approach,\n  type LoimuColors,\n  type LoimuParams,\n  POINTER_EASE,\n  resolveParams,\n} from \"./loimu-field\";\nimport { loimuFragmentShader } from \"./loimu-shader\";\nimport {\n  loimuUniforms,\n  setLoimuColors,\n  setLoimuFrame,\n  setLoimuParams,\n} from \"./loimu-uniforms\";\n\nconst ROLES = [\"accent\", \"accent-2\", \"accent-alt\"] as const;\n\n/** Far enough in that the still frame shows a developed sheet, not a seed. */\nconst STILL_TIME = 14;\n\nexport interface LoimuProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Flow and fold rate multiplier. Defaults to 1. */\n  speed?: number;\n  /** When on, the field swirls toward the eased cursor. Defaults to true. */\n  interactive?: boolean;\n  /** Overall opacity of the sheet, 0..1. Defaults to 1. */\n  opacity?: number;\n  /** Force the blend. Defaults to emissive on a dark ground, absorptive on a\n   * light one, which is the only way this survives a light theme. */\n  mode?: BlendMode;\n  /** Override any hue stop with a CSS colour. Omitted stops read their token. */\n  colors?: { body?: string; deep?: string; edge?: string };\n  /** Escape hatch for the field parameters, for tuning demos. */\n  params?: Partial<LoimuParams>;\n  children?: React.ReactNode;\n}\n\nexport const Loimu = React.forwardRef<HTMLDivElement, LoimuProps>(\n  (\n    {\n      speed = 1,\n      interactive = true,\n      opacity = 1,\n      mode,\n      colors,\n      params,\n      className,\n      children,\n      ...props\n    },\n    forwardedRef,\n  ) => {\n    const cBody = colors?.body;\n    const cDeep = colors?.deep;\n    const cEdge = colors?.edge;\n\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<LoimuParams>(resolveParams(params));\n    paramsRef.current = resolveParams(params);\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\n    // biome-ignore lint/correctness/useExhaustiveDependencies: a theme swap re-resolves the same colour strings to new channels.\n    const ramp = React.useMemo<LoimuColors>(() => {\n      const stop = (value: string | undefined, fallback: Rgb): Rgb =>\n        value ? resolveColor(value) : fallback;\n      return {\n        body: stop(cBody, tokens.colors.accent),\n        deep: stop(cDeep, tokens.colors[\"accent-2\"]),\n        edge: stop(cEdge, tokens.colors[\"accent-alt\"]),\n      };\n    }, [cBody, cDeep, cEdge, tokens, themeVersion]);\n\n    const rampRef = React.useRef(ramp);\n    rampRef.current = ramp;\n    const blendRef = React.useRef(blend);\n    blendRef.current = blend;\n\n    const mouse = React.useRef<[number, number]>([0, 0]);\n\n    const canvas = useGlCanvas({\n      fragment: loimuFragmentShader,\n      uniforms: () => loimuUniforms(rampRef.current, paramsRef.current),\n      enabled: !hiddenOnGround(\"loimu\", blend),\n      pointer: true,\n      pointerEase: POINTER_EASE,\n      stillTime: STILL_TIME,\n      maxDpr: 1.5,\n      renderScale: 0.7,\n      onFrame: (u, frame) => {\n        const half = Math.max(frame.height, 1) / 2;\n        if (interactiveRef.current) {\n          mouse.current[0] = approach(\n            mouse.current[0],\n            frame.pointer.x / half,\n            POINTER_EASE,\n          );\n          mouse.current[1] = approach(\n            mouse.current[1],\n            frame.pointer.y / half,\n            POINTER_EASE,\n          );\n        }\n        setLoimuColors(u, rampRef.current);\n        setLoimuParams(u, paramsRef.current);\n        setLoimuFrame(u, {\n          time: frame.time * speedRef.current,\n          mouse: mouse.current,\n          pointer: 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 or the blend changes.\n    React.useEffect(() => {\n      redraw();\n    }, [redraw, ramp, blend]);\n\n    const on = 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-blend={blend}\n        className={cn(\"relative isolate overflow-hidden\", className)}\n        {...props}\n      >\n        {on ? (\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);\nLoimu.displayName = \"Loimu\";\n"
    }
  ]
}