{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "kajastus",
  "type": "registry:ui",
  "dependencies": [
    "clsx",
    "motion",
    "ogl",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "https://usva.build/r/atmospheres-core.json"
  ],
  "files": [
    {
      "path": "kajastus-uniforms.ts",
      "target": "components/ui/kajastus-uniforms.ts",
      "type": "registry:ui",
      "content": "import type { Rgb } from \"./atmospheres-color\";\nimport {\n  setUniform,\n  type Uniforms,\n} from \"./atmospheres-gl\";\n\nexport interface KajastusColors {\n  /** Low and distant: oxygen green. */\n  low: Rgb;\n  /** Climbing the folds: nitrogen violet. */\n  high: Rgb;\n  /** The cold field behind. */\n  star: Rgb;\n}\n\nexport interface KajastusParams {\n  /** How far the eye tilts up, radians. */\n  pitch: number;\n  /** Curvature of the ceiling. Larger arches it down harder at the edges. */\n  curve: number;\n  /** How far the ribbon meanders across the sky. */\n  fold: number;\n  /** Wavelength of the meander; smaller is longer. */\n  foldScale: number;\n  /** One-axis domain warp, so folds stay folds instead of turning to soup. */\n  warp: number;\n  /** Where the ribbon sits relative to the eye. */\n  offset: number;\n  /** Thickness of the sheet. */\n  width: number;\n  /** Across-ribbon detail. The along-ribbon domain is 8x this. */\n  detail: number;\n  /** Cuts the noise floor away, which is most of what keeps the frame empty. */\n  threshold: number;\n  /** Lateral drift of the fine structure. */\n  drift: number;\n  /** Vertical wavelength of the streaming rays. */\n  rayFreq: number;\n  /** How fast the rays run up the ribbon. */\n  raySpeed: number;\n  /** Distance extinction. Also the anti-aliasing budget near the horizon. */\n  far: number;\n  exposure: number;\n  /** Star brightness. Zero for a clean sky. */\n  stars: number;\n  /** Depth of the corridor cut for the header type, 0..1. */\n  corridor: number;\n  /** Vertical centre of the corridor in NDC, -1 bottom to 1 top. */\n  corridorY: number;\n  /** Corridor height. */\n  corridorH: number;\n}\n\nexport const KAJASTUS_DEFAULTS: KajastusParams = {\n  pitch: 0.42,\n  curve: 0.018,\n  fold: 7,\n  foldScale: 0.045,\n  warp: 9,\n  offset: -6,\n  width: 1.8,\n  detail: 0.5,\n  threshold: 0.28,\n  drift: 0.06,\n  rayFreq: 2.4,\n  raySpeed: 0.9,\n  far: 0.025,\n  exposure: 11,\n  stars: 0.22,\n  corridor: 0.34,\n  corridorY: -0.25,\n  corridorH: 0.6,\n};\n\nexport function resolveKajastusParams(\n  overrides: Partial<KajastusParams> = {},\n): KajastusParams {\n  return { ...KAJASTUS_DEFAULTS, ...overrides };\n}\n\nexport interface KajastusFrame {\n  time: number;\n  alpha: number;\n  /** 1.0 stains a light ground as pigment, 0.0 emits into a dark one. */\n  blend: number;\n}\n\nexport function kajastusUniforms(\n  colors: KajastusColors,\n  params: KajastusParams,\n): Uniforms {\n  return {\n    uTime: { value: 0 },\n    uResolution: { value: [1, 1] },\n    uLow: { value: colors.low },\n    uHigh: { value: colors.high },\n    uStar: { value: colors.star },\n    uPitch: { value: params.pitch },\n    uCurve: { value: params.curve },\n    uFold: { value: params.fold },\n    uFoldScale: { value: params.foldScale },\n    uWarp: { value: params.warp },\n    uOffset: { value: params.offset },\n    uWidth: { value: params.width },\n    uDetail: { value: params.detail },\n    uThreshold: { value: params.threshold },\n    uDrift: { value: params.drift },\n    uRayFreq: { value: params.rayFreq },\n    uRaySpeed: { value: params.raySpeed },\n    uFar: { value: params.far },\n    uExposure: { value: params.exposure },\n    uStars: { value: params.stars },\n    uCorridor: { value: params.corridor },\n    uCorridorY: { value: params.corridorY },\n    uCorridorH: { value: params.corridorH },\n    uAlpha: { value: 1 },\n    uBlend: { value: 0 },\n  };\n}\n\nexport function setKajastusColors(u: Uniforms, colors: KajastusColors): void {\n  setUniform(u, \"uLow\", colors.low);\n  setUniform(u, \"uHigh\", colors.high);\n  setUniform(u, \"uStar\", colors.star);\n}\n\nexport function setKajastusParams(u: Uniforms, params: KajastusParams): void {\n  setUniform(u, \"uPitch\", params.pitch);\n  setUniform(u, \"uCurve\", params.curve);\n  setUniform(u, \"uFold\", params.fold);\n  setUniform(u, \"uFoldScale\", params.foldScale);\n  setUniform(u, \"uWarp\", params.warp);\n  setUniform(u, \"uOffset\", params.offset);\n  setUniform(u, \"uWidth\", params.width);\n  setUniform(u, \"uDetail\", params.detail);\n  setUniform(u, \"uThreshold\", params.threshold);\n  setUniform(u, \"uDrift\", params.drift);\n  setUniform(u, \"uRayFreq\", params.rayFreq);\n  setUniform(u, \"uRaySpeed\", params.raySpeed);\n  setUniform(u, \"uFar\", params.far);\n  setUniform(u, \"uExposure\", params.exposure);\n  setUniform(u, \"uStars\", params.stars);\n  setUniform(u, \"uCorridor\", params.corridor);\n  setUniform(u, \"uCorridorY\", params.corridorY);\n  setUniform(u, \"uCorridorH\", params.corridorH);\n}\n\nexport function setKajastusFrame(u: Uniforms, frame: KajastusFrame): void {\n  setUniform(u, \"uTime\", frame.time);\n  setUniform(u, \"uAlpha\", frame.alpha);\n  setUniform(u, \"uBlend\", frame.blend);\n}\n"
    },
    {
      "path": "kajastus-shader.ts",
      "target": "components/ui/kajastus-shader.ts",
      "type": "registry:ui",
      "content": "import { glsl } from \"./atmospheres-glsl\";\n\n/**\n * Kajastus is an aurora seen from underneath: a vault, not a curtain. Three things\n * carry it and none of them are negotiable.\n *\n * 1. The camera sits at the origin, so a point on a ray is just rd * t and the\n *    radial distance equals t. Altitude is measured in a paraboloid field,\n *    hh = y + curve * r^2, which is a large-radius sphere near its apex. Its\n *    level sets arch overhead and drop away toward the horizon, so the geometry\n *    converges above you with no fake perspective anywhere. hh is quadratic in\n *    t, so the entry and exit of the emitting layer are solved analytically and\n *    every one of the 48 steps lands inside the aurora rather than in vacuum.\n *\n * 2. The noise domain is stretched 8:1 along the field lines, which for an\n *    aurora run vertically. Long and thin along, sharply detailed across: that\n *    anisotropy is the whole difference between an aurora and a cloud, and\n *    isotropic noise here gives violet clouds on near-black.\n *\n * 3. Aurora is optically thin. There is no Beer-Lambert term: emission\n *    accumulates additively with zero absorption, and pow(density, 2.2) keeps\n *    the ribbon edges crisp instead of milky.\n *\n * Hue follows altitude because that is the physics: oxygen green low, nitrogen\n * violet above it. kajo's accent-alt is green and its accent is violet.\n */\n\n/** The march is jittered per pixel per frame, so the dither buys back the bands\n * a coarser step would otherwise show. 48 was paying for detail nobody could see. */\nconst STEPS = 32;\nconst H_LOW = 1.2;\nconst H_HIGH = 6.0;\nconst T_MAX = 90.0;\n\nexport const kajastusFragmentShader = /* glsl */ `#version 300 es\nprecision highp float;\n\nuniform float uTime;\nuniform vec2  uResolution;\nuniform vec3  uLow;\nuniform vec3  uHigh;\nuniform vec3  uStar;\nuniform float uPitch;\nuniform float uCurve;\nuniform float uFold;\nuniform float uFoldScale;\nuniform float uWarp;\nuniform float uOffset;\nuniform float uWidth;\nuniform float uDetail;\nuniform float uThreshold;\nuniform float uDrift;\nuniform float uRayFreq;\nuniform float uRaySpeed;\nuniform float uFar;\nuniform float uExposure;\nuniform float uStars;\nuniform float uCorridor;\nuniform float uCorridorY;\nuniform float uCorridorH;\nuniform float uAlpha;\nuniform float uBlend;\n\nout vec4 fragColor;\n\n${glsl(\"fbm\", \"dither\", \"tonemap\", \"composite\")}\n\nconst int STEPS = ${STEPS};\nconst float H_LOW = ${H_LOW.toFixed(2)};\nconst float H_HIGH = ${H_HIGH.toFixed(2)};\nconst float T_MAX = ${T_MAX.toFixed(1)};\n\n/** Where a ray crosses altitude h, given hh(t) = ry*t + a*t^2. Always one\n * positive root for h > 0, so rays that dip below the eye still find the vault\n * far out, which is how it reaches past the horizon. */\nfloat tAtHeight(float ry, float a, float h) {\n  if (a < 1e-5) return ry > 1e-4 ? h / ry : -1.0;\n  return (-ry + sqrt(ry * ry + 4.0 * a * h)) / (2.0 * a);\n}\n\n/** The ribbon centreline, meandering in z as a function of x. The warp is\n * applied on one axis only: that gives folds in the fabric, not turbulence. */\nfloat centreline(float x, float t) {\n  float warp = uWarp * (fbm2(vec2(x * uFoldScale * 0.35, t * 0.008), 2) - 0.5) * 2.0;\n  float u = x + warp;\n  return uOffset + uFold * (fbm2(vec2(u * uFoldScale, t * 0.011), 3) - 0.5) * 2.0;\n}\n\nfloat ribbon(float dz, float width) {\n  return exp(-(dz * dz) / max(width * width, 1e-4));\n}\n\nvoid main() {\n  vec2 uv = (gl_FragCoord.xy * 2.0 - uResolution) / uResolution.y;\n\n  float cp = cos(uPitch);\n  float sp = sin(uPitch);\n  vec3 rd = normalize(vec3(uv.x, uv.y * cp + 1.25 * sp, uv.y * sp - 1.25 * cp));\n\n  float ry = rd.y;\n  float aq = uCurve * (1.0 - ry * ry);\n  float t0 = tAtHeight(ry, aq, H_LOW);\n  float t1 = tAtHeight(ry, aq, H_HIGH);\n\n  vec3 acc = vec3(0.0);\n  float cover = 0.0;\n\n  if (t0 > 0.0 && t1 > t0 && t0 < T_MAX) {\n    t1 = min(t1, T_MAX);\n    float dt = (t1 - t0) / float(STEPS);\n    float jitter = ign(gl_FragCoord.xy + vec2(uTime * 61.7, uTime * 37.3));\n    float span = H_HIGH - H_LOW;\n\n    for (int i = 0; i < STEPS; i++) {\n      float t = t0 + (float(i) + jitter) * dt;\n      vec3 p = rd * t;\n      float hh = p.y + uCurve * dot(p.xz, p.xz);\n      float alt = (hh - H_LOW) / span;\n      if (alt < 0.0 || alt > 1.0) continue;\n\n      float zc = centreline(p.x, uTime);\n      float mainSheet = ribbon(p.z - zc, uWidth);\n      float sideA = ribbon(p.z - zc - 3.8, uWidth * 0.78);\n      float sideB = ribbon(p.z - zc + 4.6, uWidth * 0.66);\n      float sheet = mainSheet + 0.64 * sideA + 0.46 * sideB;\n      if (sheet < 0.004) continue;\n\n      float n = fbm2(\n        vec2(p.x * uDetail + uDrift * uTime, hh * uDetail * 0.125),\n        3\n      );\n      float shaped = clamp(n * 1.7 - uThreshold, 0.0, 1.0);\n      float density = sheet * pow(shaped, 2.2);\n      if (density < 1e-4) continue;\n\n      float low = smoothstep(0.0, 0.08, alt);\n      float crown = mix(1.0, 0.24, smoothstep(0.2, 1.0, alt));\n      float phase = 6.2831853 * fbm2(vec2(p.x * 0.3, 4.7), 2);\n      float rayWave = 0.5 + 0.5 * sin(uRayFreq * hh - uRaySpeed * uTime + phase);\n      float rays = 0.58 + 0.42 * rayWave * rayWave * rayWave;\n\n      float em = density * low * crown * rays * exp(-t * uFar) * dt;\n      acc += em * mix(uLow, uHigh, smoothstep(0.02, 0.6, alt));\n      cover += em;\n    }\n  }\n\n  // pow() is undefined for a negative base in GLSL ES, and uv.y - uCorridorY is\n  // negative over most of the lower frame. Squaring by hand, never pow(x, 2.0).\n  float cd = (uv.y - uCorridorY) / max(uCorridorH, 1e-3);\n  float corridor = 1.0 - min(uCorridor, 0.38) * exp(-0.55 * cd * cd);\n  acc *= corridor;\n  cover *= corridor;\n\n  vec2 sky = vec2(atan(rd.z, rd.x), asin(clamp(rd.y, -1.0, 1.0))) * 26.0;\n  vec2 cell = floor(sky);\n  vec2 off = hash22(cell) - 0.5;\n  vec2 local = fract(sky) - 0.5 - off * 0.7;\n  float twinkle = step(0.962, hash12(cell + 3.1));\n  float star = twinkle * exp(-dot(local, local) * 90.0);\n  star *= smoothstep(-0.15, 0.06, rd.y) * exp(-cover * 3.0);\n\n  vec3 col = acc * uExposure + star * uStars * uStar * (1.0 - uBlend);\n\n  // Tonemapped on luminance, not per channel: the hot lobe of a fold stays\n  // violet instead of clipping to white, and only the very peak heats up.\n  //\n  // The shoulder is Reinhard, not 1 - exp(-lm). The centreline's meander swings\n  // the whole frame's brightness threefold over a few minutes, and at the top of\n  // that swing the exponential reached 1.0 across a fifth of the frame: flat,\n  // fully opaque, folds and corridor gone. Reinhard only approaches 1, so a\n  // surge stays a surge. uExposure is 11 to put the opening seconds back.\n  float lm = max(col.r, max(col.g, col.b));\n  float peak = lm / (1.0 + lm);\n  // Re-saturating exponent: rays that crossed both the green floor and the\n  // violet folds average out milky, and the pow pulls the hue back.\n  vec3 display = pow(col / max(lm, 1e-4), vec3(1.6));\n  float heat = peak * peak;\n  display = mix(display, vec3(1.0), heat * heat * 0.22);\n  display = mix(display, display * display, uBlend);\n  display = dither(display, gl_FragCoord.xy, 1.0 / 255.0);\n\n  fragColor = composite(display, peak * uAlpha);\n}\n`;\n"
    },
    {
      "path": "kajastus.tsx",
      "target": "components/ui/kajastus.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  resolveBlendMode,\n  resolveColor,\n} from \"./atmospheres-color\";\nimport { hiddenOnGround } from \"./atmospheres-ground\";\nimport { useGlCanvas } from \"./use-gl-canvas\";\nimport { useTokenColors } from \"./use-token-colors\";\nimport { kajastusFragmentShader } from \"./kajastus-shader\";\nimport {\n  type KajastusColors,\n  type KajastusParams,\n  kajastusUniforms,\n  resolveKajastusParams,\n  setKajastusColors,\n  setKajastusFrame,\n  setKajastusParams,\n} from \"./kajastus-uniforms\";\n\nconst ROLES = [\"accent\", \"accent-alt\", \"ink\"] as const;\n\nexport interface KajastusProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Rate of the streaming rays and the fold drift. Defaults to 1. */\n  speed?: number;\n  /** Overall strength, 0..1. Defaults to 1. */\n  opacity?: number;\n  /** Dark grounds emit, light grounds stain. Defaults to the resolved bg. */\n  mode?: BlendMode;\n  /** Override the altitude ramp. Green low, violet high, cold stars behind. */\n  colors?: { low?: string; high?: string; star?: string };\n  /** Escape hatch for the field parameters, for tuning demos. */\n  params?: Partial<KajastusParams>;\n  children?: React.ReactNode;\n}\n\n/** Seconds into the animation the reduced-motion still frame is taken from. */\nconst STILL_TIME = 8;\n\nexport const Kajastus = React.forwardRef<HTMLDivElement, KajastusProps>(\n  (\n    {\n      speed = 1,\n      opacity = 1,\n      mode,\n      colors,\n      params,\n      className,\n      children,\n      ...props\n    },\n    forwardedRef,\n  ) => {\n    const cLow = colors?.low;\n    const cHigh = colors?.high;\n    const cStar = colors?.star;\n\n    const scopeRef = React.useRef<HTMLDivElement | null>(null);\n    const tokens = useTokenColors(ROLES, { scopeRef });\n    const blend = resolveBlendMode(mode, tokens.bg);\n\n    const ramp = React.useMemo<KajastusColors>(\n      () => ({\n        low: cLow ? resolveColor(cLow) : tokens.colors[\"accent-alt\"],\n        high: cHigh ? resolveColor(cHigh) : tokens.colors.accent,\n        star: cStar ? resolveColor(cStar) : tokens.colors.ink,\n      }),\n      [cLow, cHigh, cStar, tokens],\n    );\n\n    const rampRef = React.useRef(ramp);\n    rampRef.current = ramp;\n    const speedRef = React.useRef(speed);\n    speedRef.current = speed;\n    const opacityRef = React.useRef(opacity);\n    opacityRef.current = opacity;\n    const blendRef = React.useRef(blend);\n    blendRef.current = blend;\n\n    const resolved = resolveKajastusParams(params);\n    const paramsRef = React.useRef(resolved);\n    paramsRef.current = resolved;\n\n    const canvas = useGlCanvas({\n      fragment: kajastusFragmentShader,\n      uniforms: () => kajastusUniforms(rampRef.current, paramsRef.current),\n      enabled: !hiddenOnGround(\"kajastus\", blend),\n      maxDpr: 1.5,\n      renderScale: 0.5,\n      stillTime: STILL_TIME,\n      onFrame: (u, frame) => {\n        setKajastusColors(u, rampRef.current);\n        setKajastusParams(u, paramsRef.current);\n        setKajastusFrame(u, {\n          time: frame.time * speedRef.current,\n          alpha: opacityRef.current,\n          blend: blendUniform(blendRef.current),\n        });\n      },\n    });\n\n    const { redraw } = canvas;\n    // biome-ignore lint/correctness/useExhaustiveDependencies: the still frame must repaint when the theme repaints the ramp.\n    React.useEffect(() => {\n      redraw();\n    }, [redraw, ramp, blend]);\n\n    const vaultOn = 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={vaultOn ? \"on\" : \"off\"}\n        className={cn(\"relative isolate overflow-hidden\", className)}\n        {...props}\n      >\n        {vaultOn ? (\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);\nKajastus.displayName = \"Kajastus\";\n"
    }
  ]
}