{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "kuulto",
  "type": "registry:ui",
  "dependencies": [
    "clsx",
    "motion",
    "ogl",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "https://usva.build/r/atmospheres-core.json"
  ],
  "files": [
    {
      "path": "kuulto-field.ts",
      "target": "components/ui/kuulto-field.ts",
      "type": "registry:ui",
      "content": "import type { Rgb } from \"./atmospheres-color\";\n\n/**\n * kuulto is translucency, the state of being dimly visible: something light passes\n * through or reflects softly. Here that is a vast sheet of silk, creased a few\n * times and lit from three sides. Nothing here samples a\n * palette. The colour is what the lighting does to the surface, so a fold turning\n * toward one lamp reads as that lamp's hue and the same fold rolling away catches\n * the next. The ground is black because no light reaches it, not because it was\n * painted black.\n */\nexport interface KuultoParams {\n  /** Fold size. Smaller values pull the drape further back from the eye. */\n  scale: number;\n  /** How steeply the normals turn across a fold. The biggest knob on the look. */\n  relief: number;\n  /** Depth of the pleats folded into the sheet. */\n  crease: number;\n  /** Gaussian width of a pleat. Wide pleats read as satin, narrow ones as foil. */\n  creaseWidth: number;\n  /** How far the pleats travel. */\n  drift: number;\n  /** Weight of the organic wander laid over the pleats. */\n  drape: number;\n  drapeScale: number;\n  /** Specular exponent. High is a tight glint, low is a broad sheen. */\n  sheen: number;\n  /** Weight of the specular term against the diffuse one. */\n  gloss: number;\n  /** Lambert wrap. At 0 the terminator is hard; at 1 light bleeds right round. */\n  wrap: number;\n  /** Power on the wrapped diffuse. High drives the unlit sheet to black. */\n  contrast: number;\n  /** Gamma on the normalised tint. Above 1 keeps overlapping lamps from\n   * greying each other out; 1 leaves the mix alone. */\n  purity: number;\n  /** Fill and rim directions, in eye space. The key is steered by the pointer. */\n  fill: [number, number, number];\n  rim: [number, number, number];\n  /** Key direction with the pointer at rest. */\n  key: [number, number, number];\n  /** How far the pointer swings the key light. */\n  tilt: number;\n  gain: number;\n}\n\nexport const KUULTO_DEFAULTS: KuultoParams = {\n  scale: 1.2,\n  relief: 3.0,\n  crease: 1.15,\n  creaseWidth: 1.0,\n  drift: 0.06,\n  drape: 0.45,\n  drapeScale: 0.35,\n  sheen: 36,\n  gloss: 1.0,\n  wrap: 0.08,\n  contrast: 4.5,\n  purity: 1.9,\n  key: [-0.8, 0.45, 0.3],\n  fill: [0.85, -0.35, 0.26],\n  rim: [0.1, 0.95, 0.22],\n  tilt: 0.55,\n  gain: 1.35,\n};\n\nexport interface KuultoColors {\n  /** The key lamp. Whatever faces it takes this hue. */\n  key: Rgb;\n  /** The fill lamp, opposite the key, holding the shadow side off black. */\n  fill: Rgb;\n  /** The rim lamp, grazing the sheet so only fold crests catch it. */\n  rim: Rgb;\n}\n\nexport const POINTER_EASE = 0.045;\n\n/** The number of pleats folded into the sheet. Three read as cloth; more read as\n * corrugation, which is the object the background must never become. */\nexport const CREASES = 3;\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<KuultoParams>): KuultoParams {\n  return { ...KUULTO_DEFAULTS, ...overrides };\n}\n\nfunction normalize(v: [number, number, number]): [number, number, number] {\n  const len = Math.hypot(v[0], v[1], v[2]);\n  if (len < 1e-6) return [0, 0, 1];\n  return [v[0] / len, v[1] / len, v[2] / len];\n}\n\n/**\n * The key lamp swings with the eased pointer, so the cursor turns the silk in the\n * light rather than dragging a glow across it. The z term is floored: a lamp that\n * swings past the horizon would light the sheet from behind and the folds would\n * flip inside out.\n */\nexport function keyLight(\n  params: KuultoParams,\n  mouse: [number, number],\n  amount: number,\n): [number, number, number] {\n  const swing = params.tilt * amount;\n  return normalize([\n    params.key[0] + mouse[0] * swing,\n    params.key[1] + mouse[1] * swing,\n    Math.max(params.key[2], 0.25),\n  ]);\n}\n"
    },
    {
      "path": "kuulto-shader.ts",
      "target": "components/ui/kuulto-shader.ts",
      "type": "registry:ui",
      "content": "import { glsl } from \"./atmospheres-glsl\";\nimport { CREASES } from \"./kuulto-field\";\n\n/** The gradient is taken across a fixed fraction of a fold rather than a pixel:\n * sampling the height field a pixel apart would resolve the fbm's own grain and\n * the sheet would come back sandpapered. */\nconst GRAD_EPS = 0.035;\n\nexport const kuultoFragmentShader = /* glsl */ `#version 300 es\nprecision highp float;\n\nuniform float uTime;\nuniform vec2  uResolution;\nuniform float uScale;\nuniform float uRelief;\nuniform float uCrease;\nuniform float uCreaseWidth;\nuniform float uDrift;\nuniform float uDrape;\nuniform float uDrapeScale;\nuniform float uSheen;\nuniform float uGloss;\nuniform float uWrap;\nuniform float uContrast;\nuniform float uPurity;\nuniform vec3  uKey;\nuniform vec3  uFill;\nuniform vec3  uRim;\nuniform float uGain;\nuniform float uAlpha;\nuniform float uAbsorb;\nuniform vec3  uKeyColor;\nuniform vec3  uFillColor;\nuniform vec3  uRimColor;\n\nout vec4 fragColor;\n\n${glsl(\"fbm\", \"dither\", \"composite\")}\n\n/** One pleat. Odd in the signed distance to its axis, so the sheet lifts on one\n * side of the crease and drops on the other the way real cloth does. A Gaussian\n * bump would only ever make a welt. */\nfloat pleat(vec2 p, float angle, vec2 centre, float width) {\n  vec2 axis = vec2(cos(angle), sin(angle));\n  float d = dot(p - centre, axis) / max(width, 1e-3);\n  return d * exp(-d * d);\n}\n\n/** The drape, as a height. Three pleats on slow lissajous paths, plus one gentle\n * fbm so the cloth wanders and the eye can never trace a pleat back to a line. */\nfloat height(vec2 p, float t) {\n  float h = 0.0;\n\n  for (int i = 0; i < ${CREASES}; i++) {\n    float fi = float(i);\n    // Golden-angle spacing: any rational fraction of a turn would let the pleats\n    // line up into a corrugation every few seconds.\n    float angle = 2.39996 * fi + t * uDrift * (0.6 + 0.2 * fi);\n    vec2 centre = vec2(\n      sin(t * uDrift * (1.3 + 0.4 * fi) + fi * 2.1),\n      cos(t * uDrift * (0.9 + 0.5 * fi) + fi * 1.7)\n    ) * 1.15;\n    h += pleat(p, angle, centre, uCreaseWidth) * uCrease;\n  }\n\n  h += (fbm(vec3(p * uDrapeScale, t * 0.035), 3) - 0.5) * uDrape * 2.0;\n  return h;\n}\n\n/** Central differences. The sheet is a height field, so the normal is just the\n * gradient stood up: a fold's whole appearance is decided here. */\nvec3 surfaceNormal(vec2 p, float t) {\n  vec2 e = vec2(${GRAD_EPS.toFixed(3)}, 0.0);\n  float hx = height(p + e.xy, t) - height(p - e.xy, t);\n  float hy = height(p + e.yx, t) - height(p - e.yx, t);\n  vec2 grad = vec2(hx, hy) / (2.0 * e.x);\n  return normalize(vec3(-grad * uRelief, 1.0));\n}\n\n/** Wrapped Lambert raised to a contrast power. The wrap softens the terminator\n * into cloth; the power drives everything that does not face a lamp down to\n * black, which is where the ground between the folds comes from. */\nfloat lambert(vec3 n, vec3 l) {\n  float d = (dot(n, l) + uWrap) / (1.0 + uWrap);\n  return pow(max(d, 0.0), uContrast);\n}\n\n/** Blinn-Phong against a viewer straight down the z axis. This is the sheen that\n * a band function cannot have, and the whole reason the folds read as silk. */\nfloat sheen(vec3 n, vec3 l) {\n  vec3 h = normalize(l + vec3(0.0, 0.0, 1.0));\n  return pow(max(dot(n, h), 0.0), uSheen);\n}\n\nvec3 lamp(vec3 n, vec3 dir, vec3 hue, float weight) {\n  return hue * (lambert(n, dir) * weight + sheen(n, dir) * uGloss);\n}\n\nvoid main() {\n  vec2 ndc = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;\n  vec2 p = ndc * uScale;\n\n  vec3 n = surfaceNormal(p, uTime);\n\n  vec3 col = lamp(n, normalize(uKey), uKeyColor, 1.0)\n           + lamp(n, normalize(uFill), uFillColor, 0.85)\n           + lamp(n, normalize(uRim), uRimColor, 0.55);\n\n  col *= uGain;\n\n  // Tonemapped on luminance, not per channel, so a lit crest saturates toward its\n  // lamp instead of washing to white.\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  // A gamma on the normalised tint. Where two lamps overlap the mix drifts\n  // grey; this deepens the minor channels so the dominant lamp keeps its hue,\n  // and a pure lamp colour passes through untouched.\n  tint = pow(tint, vec3(uPurity));\n\n  vec3 rgb = dither(mix(tint, tint * 0.65, uAbsorb), gl_FragCoord.xy, 0.006);\n  float alpha = peak * uAlpha;\n  alpha = ditherAlpha(alpha, gl_FragCoord.xy, 0.004);\n\n  fragColor = composite(rgb, alpha);\n}\n`;\n"
    },
    {
      "path": "kuulto-uniforms.ts",
      "target": "components/ui/kuulto-uniforms.ts",
      "type": "registry:ui",
      "content": "import {\n  setUniform,\n  type Uniforms,\n} from \"./atmospheres-gl\";\nimport type { KuultoColors, KuultoParams } from \"./kuulto-field\";\n\nexport interface KuultoFrame {\n  time: number;\n  /** The key lamp direction after the pointer has swung it. */\n  key: [number, number, number];\n  alpha: number;\n  /** The house law: 1 stains the ground, 0 emits into it. */\n  absorb: number;\n}\n\nexport function kuultoUniforms(\n  colors: KuultoColors,\n  params: KuultoParams,\n): Uniforms {\n  return {\n    uTime: { value: 0 },\n    uResolution: { value: [1, 1] },\n    uScale: { value: params.scale },\n    uRelief: { value: params.relief },\n    uCrease: { value: params.crease },\n    uCreaseWidth: { value: params.creaseWidth },\n    uDrift: { value: params.drift },\n    uDrape: { value: params.drape },\n    uDrapeScale: { value: params.drapeScale },\n    uSheen: { value: params.sheen },\n    uGloss: { value: params.gloss },\n    uWrap: { value: params.wrap },\n    uContrast: { value: params.contrast },\n    uPurity: { value: params.purity },\n    uKey: { value: [...params.key] },\n    uFill: { value: [...params.fill] },\n    uRim: { value: [...params.rim] },\n    uGain: { value: params.gain },\n    uAlpha: { value: 1 },\n    uAbsorb: { value: 0 },\n    uKeyColor: { value: [...colors.key] },\n    uFillColor: { value: [...colors.fill] },\n    uRimColor: { value: [...colors.rim] },\n  };\n}\n\nexport function setKuultoColors(u: Uniforms, colors: KuultoColors): void {\n  setUniform(u, \"uKeyColor\", [...colors.key]);\n  setUniform(u, \"uFillColor\", [...colors.fill]);\n  setUniform(u, \"uRimColor\", [...colors.rim]);\n}\n\nexport function setKuultoParams(u: Uniforms, params: KuultoParams): void {\n  setUniform(u, \"uScale\", params.scale);\n  setUniform(u, \"uRelief\", params.relief);\n  setUniform(u, \"uCrease\", params.crease);\n  setUniform(u, \"uCreaseWidth\", params.creaseWidth);\n  setUniform(u, \"uDrift\", params.drift);\n  setUniform(u, \"uDrape\", params.drape);\n  setUniform(u, \"uDrapeScale\", params.drapeScale);\n  setUniform(u, \"uSheen\", params.sheen);\n  setUniform(u, \"uGloss\", params.gloss);\n  setUniform(u, \"uWrap\", params.wrap);\n  setUniform(u, \"uContrast\", params.contrast);\n  setUniform(u, \"uPurity\", params.purity);\n  setUniform(u, \"uFill\", [...params.fill]);\n  setUniform(u, \"uRim\", [...params.rim]);\n  setUniform(u, \"uGain\", params.gain);\n}\n\nexport function setKuultoFrame(u: Uniforms, frame: KuultoFrame): void {\n  setUniform(u, \"uTime\", frame.time);\n  setUniform(u, \"uKey\", [...frame.key]);\n  setUniform(u, \"uAlpha\", frame.alpha);\n  setUniform(u, \"uAbsorb\", frame.absorb);\n}\n"
    },
    {
      "path": "kuulto.tsx",
      "target": "components/ui/kuulto.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 KuultoColors,\n  type KuultoParams,\n  keyLight,\n  POINTER_EASE,\n  resolveParams,\n} from \"./kuulto-field\";\nimport { kuultoFragmentShader } from \"./kuulto-shader\";\nimport {\n  kuultoUniforms,\n  setKuultoColors,\n  setKuultoFrame,\n  setKuultoParams,\n} from \"./kuulto-uniforms\";\n\nconst ROLES = [\"accent\", \"accent-2\", \"accent-alt\"] as const;\n\n/** Far enough in that the pleats have travelled off their seed positions. */\nconst STILL_TIME = 22;\n\nexport interface KuultoProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Drift and drape rate multiplier. Defaults to 1. */\n  speed?: number;\n  /** When on, the cursor swings the key lamp and the folds re-catch it.\n   * 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 lamp with a CSS colour. Omitted lamps read their token. */\n  colors?: { key?: string; fill?: string; rim?: string };\n  /** Escape hatch for the drape parameters, for tuning demos. */\n  params?: Partial<KuultoParams>;\n  children?: React.ReactNode;\n}\n\nexport const Kuulto = React.forwardRef<HTMLDivElement, KuultoProps>(\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 cKey = colors?.key;\n    const cFill = colors?.fill;\n    const cRim = colors?.rim;\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<KuultoParams>(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 lamps = React.useMemo<KuultoColors>(() => {\n      const lamp = (value: string | undefined, fallback: Rgb): Rgb =>\n        value ? resolveColor(value) : fallback;\n      return {\n        key: lamp(cKey, tokens.colors.accent),\n        fill: lamp(cFill, tokens.colors[\"accent-2\"]),\n        rim: lamp(cRim, tokens.colors[\"accent-alt\"]),\n      };\n    }, [cKey, cFill, cRim, tokens, themeVersion]);\n\n    const lampsRef = React.useRef(lamps);\n    lampsRef.current = lamps;\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: kuultoFragmentShader,\n      uniforms: () => kuultoUniforms(lampsRef.current, paramsRef.current),\n      enabled: !hiddenOnGround(\"kuulto\", blend),\n      pointer: true,\n      pointerEase: POINTER_EASE,\n      stillTime: STILL_TIME,\n      maxDpr: 1.5,\n      renderScale: 0.8,\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        setKuultoColors(u, lampsRef.current);\n        setKuultoParams(u, paramsRef.current);\n        setKuultoFrame(u, {\n          time: frame.time * speedRef.current,\n          key: keyLight(\n            paramsRef.current,\n            mouse.current,\n            interactiveRef.current ? frame.pointer.amount : 0,\n          ),\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 lamps or the blend change.\n    React.useEffect(() => {\n      redraw();\n    }, [redraw, lamps, 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);\nKuulto.displayName = \"Kuulto\";\n"
    }
  ]
}