{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "vare",
  "type": "registry:ui",
  "dependencies": [
    "clsx",
    "motion",
    "ogl",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "https://usva.build/r/atmospheres-core.json"
  ],
  "files": [
    {
      "path": "vare-field.ts",
      "target": "components/ui/vare-field.ts",
      "type": "registry:ui",
      "content": "import type { Rgb } from \"./atmospheres-color\";\n\n/**\n * vare (väre, a ripple) emits from the iso-phase lines of a few broad travelling\n * wavefronts, never from the wave bodies. What you see is a set of drifting\n * luminous boundaries: the folded edges of a curtain, seen from very far away.\n *\n * The failure mode is moire. Every defence against it is a parameter here: the\n * domain warp, the per-front phase jitter, the wavenumber scatter and the\n * thickness noise. Turn them to zero and the interference snaps into a diagram.\n */\nexport interface VareParams {\n  /** Shared travel direction of the front, in radians. */\n  angle: number;\n  /** Half-width of the k-vector fan around that direction, in radians. */\n  spread: number;\n  /** Base wavenumber. Each front scatters around it, so no two bands match. */\n  wavenumber: number;\n  speed: number;\n  /** Amplitude of the FBM domain warp that bends the whole field. */\n  warp: number;\n  warpScale: number;\n  /** Per-front phase drift, in band periods. This is what stops two fronts from\n   * lining up, since the shared warp bends them all the same way. */\n  jitter: number;\n  /** Exponent of the cosine ridge. Higher is a narrower boundary; low melts\n   * the bands into one another. */\n  soft: number;\n  /** Frequency of the noise that varies band thickness along a front. */\n  detail: number;\n  /** Extra light where two fronts cross. */\n  node: number;\n  gain: number;\n  /** Off-frame origin of the fronts, in half-height units, x scaled by aspect. */\n  source: [number, number];\n  /** Inverse-square arrival from the source. Higher decays to void sooner. */\n  falloff: number;\n  /** Distance over which the hue completes its front-to-back ramp. */\n  span: number;\n  /** Strength of the pointer lens in phase space. */\n  lens: number;\n  lensSigma: number;\n}\n\nexport const VARE_DEFAULTS: VareParams = {\n  angle: 3.64,\n  spread: 0.62,\n  wavenumber: 3.8,\n  speed: 0.9,\n  warp: 0.7,\n  warpScale: 0.55,\n  jitter: 0.7,\n  soft: 6.5,\n  detail: 1.6,\n  node: 0.9,\n  gain: 1.15,\n  source: [1.15, 0.8],\n  falloff: 0.09,\n  span: 4,\n  lens: 2.2,\n  lensSigma: 0.9,\n};\n\nexport interface VareColors {\n  /** The body of the front. */\n  body: Rgb;\n  /** The oldest, furthest bands. */\n  deep: Rgb;\n  /** The freshest bands, closest to the source. */\n  edge: Rgb;\n  /** kosteus: the hue the clay takes where the trough holds water. */\n  pigment: Rgb;\n}\n\nexport const POINTER_EASE = 0.06;\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<VareParams>): VareParams {\n  return { ...VARE_DEFAULTS, ...overrides };\n}\n"
    },
    {
      "path": "vare-shader.ts",
      "target": "components/ui/vare-shader.ts",
      "type": "registry:ui",
      "content": "import { glsl } from \"./atmospheres-glsl\";\n\nconst WAVES = 4;\n\nexport const vareFragmentShader = /* glsl */ `#version 300 es\nprecision highp float;\n\nuniform float uTime;\nuniform vec2  uResolution;\nuniform vec2  uMouse;\nuniform float uPointer;\nuniform float uAngle;\nuniform float uSpread;\nuniform float uWavenumber;\nuniform float uSpeed;\nuniform float uWarp;\nuniform float uWarpScale;\nuniform float uJitter;\nuniform float uSoft;\nuniform float uDetail;\nuniform float uNode;\nuniform float uGain;\nuniform vec2  uSource;\nuniform float uFalloff;\nuniform float uSpan;\nuniform float uLens;\nuniform float uLensSigma;\nuniform float uAlpha;\nuniform float uAbsorb;\nuniform vec3  uBody;\nuniform vec3  uDeep;\nuniform vec3  uEdgeColor;\nuniform vec3  uPigment;\nuniform float uStainFloor;\n\nout vec4 fragColor;\n\n${glsl(\"fbm\", \"dither\", \"stain\", \"composite\")}\n\nconst float TAU = 6.28318530718;\n\n/** The raking key. Low and from the left, so a ridge one pixel high still casts. */\nconst vec3 KEY = vec3(-0.62, -0.44, 0.36);\n\n/** How hard the clay drinks. Tuned so a node is damp, not a bruise. */\nconst float SIGMA = 1.15;\n\n/** The wettest a ripple ever gets. Above this the ground stops being ground. */\nconst float SOAK = 0.45;\n\nfloat phaseLens(vec2 p) {\n  vec2 toMouse = p - uMouse;\n  return uPointer * uLens\n    * exp(-dot(toMouse, toMouse) / (uLensSigma * uLensSigma));\n}\n\nvec2 waves(vec2 p, vec2 source) {\n  float lens = phaseLens(p);\n  float bands = 0.0;\n  float squares = 0.0;\n  float basins = 0.0;\n  float basinSquares = 0.0;\n\n  for (int i = 0; i < ${WAVES}; i++) {\n    float fi = float(i);\n    float angle = uAngle + (hash11(fi * 3.17 + 0.5) - 0.5) * uSpread;\n    float k = uWavenumber * (0.7 + 1.1 * hash11(fi * 7.31 + 1.7));\n    float w = uSpeed * (0.6 + 0.8 * hash11(fi * 11.7 + 4.3));\n\n    vec2 dir = vec2(cos(angle), sin(angle));\n\n    // The shared domain warp bends every front identically, so on its own it\n    // cannot stop two fronts beating against each other. This one is per front.\n    float drift = fbm2(\n      p * uWarpScale * 1.7 + vec2(fi * 21.7, uTime * 0.018), 2\n    ) - 0.5;\n\n    vec2 bendAxis = normalize(vec2(\n      cos(angle + 1.5708 + fi * 0.31),\n      sin(angle + 1.5708 + fi * 0.31)\n    ));\n    float bend = (fbm2(\n      p * (uWarpScale * (0.7 + 0.18 * fi)) + vec2(fi * 7.1),\n      2\n    ) - 0.375) * (0.7 + 0.22 * fi);\n\n    float phase = dot(p - source + bendAxis * bend, dir) * k - w * uTime\n      + hash11(fi * 5.13 + 2.9) * TAU + drift * uJitter * TAU + lens;\n\n    float thickness = 0.5 + 0.5\n      * fbm2(p * uDetail + vec2(fi * 13.0, uTime * 0.02), 2);\n    float weight = fi < 2.0 ? 1.0 : mix(0.4, 0.6, hash11(fi + 8.4));\n    // A soft cosine ridge, not a hairline: the boundary glows and falls off,\n    // and a faint square-law fill keeps the space between from reading as a\n    // diagram on black.\n    float wave = 0.5 + 0.5 * cos(phase);\n    float exponent = uSoft * (0.8 + 0.4 * hash11(fi * 2.3 + 0.7));\n    float crest = pow(wave, exponent);\n    float trough = pow(1.0 - wave, exponent);\n    float band = (crest + 0.1 * wave * wave) * thickness * weight;\n    float basinBand = trough * thickness * weight;\n\n    bands += band;\n    squares += band * band;\n    basins += basinBand;\n    basinSquares += basinBand * basinBand;\n  }\n\n  float overlap = max(0.5 * (bands * bands - squares), 0.0);\n  float nodes = smoothstep(0.3, 1.0, overlap);\n  nodes *= nodes;\n  float emission = bands + nodes * uNode;\n\n  float basinOverlap = max(0.5 * (basins * basins - basinSquares), 0.0);\n  float basinNodes = smoothstep(0.3, 1.0, basinOverlap);\n  basinNodes *= basinNodes;\n  float basin = basins + basinNodes * uNode;\n\n  return vec2(emission, basin);\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  vec2 warp = vec2(\n    fbm2(ndc * uWarpScale + vec2(uTime * 0.03, 0.0), 2),\n    fbm2(ndc * uWarpScale + vec2(9.7, uTime * 0.024), 2)\n  ) - 0.5;\n  vec2 p = ndc + warp * uWarp;\n\n  vec2 source = vec2(uSource.x * aspect, uSource.y);\n  vec2 travel = vec2(cos(uAngle), sin(uAngle));\n\n  vec2 field = waves(p, source);\n  float emission = field.x;\n\n  float depth = clamp(dot(p - source, travel) / uSpan, 0.0, 1.0);\n  vec3 hue = mix(uBody, uDeep, depth);\n  hue = mix(uEdgeColor, hue, smoothstep(0.0, 0.3, depth));\n\n  float reach = length(p - source);\n  float arrival = 1.0 / (1.0 + uFalloff * reach * reach);\n\n  vec3 col = hue * emission * arrival * uGain;\n\n  // Tonemapped on luminance, not per channel, so a bright node 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 emissive = dither(tint, gl_FragCoord.xy, 0.006);\n  float emissiveAlpha = peak * uAlpha;\n\n  if (uAbsorb < 0.5) {\n    float alpha = ditherAlpha(emissiveAlpha, gl_FragCoord.xy, 0.004);\n    fragColor = composite(emissive, alpha);\n    return;\n  }\n\n  float eps = 2.0 / max(uResolution.y, 1.0);\n  float heightX = waves(p + vec2(eps, 0.0), source).x;\n  float heightY = waves(p + vec2(0.0, eps), source).x;\n  vec3 normal = normalize(vec3(\n    emission - heightX,\n    emission - heightY,\n    eps * 5.0\n  ));\n  float key = clamp(dot(normal, normalize(KEY)), 0.0, 1.0);\n  float amount = clamp(field.y * arrival * uGain, 0.0, 1.0);\n  float wet = amount * SOAK * (1.0 - 0.62 * key);\n\n  vec3 absorbed = hold(uPigment, uStainFloor);\n  float alpha = clamp(soak(wet, SIGMA) * uAlpha, 0.0, 1.0);\n  fragColor = vec4(absorbed * alpha, alpha);\n}\n`;\n"
    },
    {
      "path": "vare-uniforms.ts",
      "target": "components/ui/vare-uniforms.ts",
      "type": "registry:ui",
      "content": "import { MAX_STAIN } from \"./atmospheres-color\";\nimport {\n  setUniform,\n  type Uniforms,\n} from \"./atmospheres-gl\";\nimport type { VareColors, VareParams } from \"./vare-field\";\n\nexport interface VareFrame {\n  time: number;\n  /** Eased pointer in half-height screen units, y up. */\n  mouse: [number, number];\n  /** Fades the phase lens 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 vareUniforms(colors: VareColors, params: VareParams): Uniforms {\n  return {\n    uTime: { value: 0 },\n    uResolution: { value: [1, 1] },\n    uMouse: { value: [0, 0] },\n    uPointer: { value: 0 },\n    uAngle: { value: params.angle },\n    uSpread: { value: params.spread },\n    uWavenumber: { value: params.wavenumber },\n    uSpeed: { value: params.speed },\n    uWarp: { value: params.warp },\n    uWarpScale: { value: params.warpScale },\n    uJitter: { value: params.jitter },\n    uSoft: { value: params.soft },\n    uDetail: { value: params.detail },\n    uNode: { value: params.node },\n    uGain: { value: params.gain },\n    uSource: { value: [...params.source] },\n    uFalloff: { value: params.falloff },\n    uSpan: { value: params.span },\n    uLens: { value: params.lens },\n    uLensSigma: { value: params.lensSigma },\n    uAlpha: { value: 1 },\n    uAbsorb: { value: 0 },\n    uBody: { value: [...colors.body] },\n    uDeep: { value: [...colors.deep] },\n    uEdgeColor: { value: [...colors.edge] },\n    uPigment: { value: [...colors.pigment] },\n    uStainFloor: { value: MAX_STAIN },\n  };\n}\n\nexport function setVareColors(u: Uniforms, colors: VareColors): void {\n  setUniform(u, \"uBody\", [...colors.body]);\n  setUniform(u, \"uDeep\", [...colors.deep]);\n  setUniform(u, \"uEdgeColor\", [...colors.edge]);\n  setUniform(u, \"uPigment\", [...colors.pigment]);\n}\n\nexport function setVareParams(u: Uniforms, params: VareParams): void {\n  setUniform(u, \"uAngle\", params.angle);\n  setUniform(u, \"uSpread\", params.spread);\n  setUniform(u, \"uWavenumber\", params.wavenumber);\n  setUniform(u, \"uSpeed\", params.speed);\n  setUniform(u, \"uWarp\", params.warp);\n  setUniform(u, \"uWarpScale\", params.warpScale);\n  setUniform(u, \"uJitter\", params.jitter);\n  setUniform(u, \"uSoft\", params.soft);\n  setUniform(u, \"uDetail\", params.detail);\n  setUniform(u, \"uNode\", params.node);\n  setUniform(u, \"uGain\", params.gain);\n  setUniform(u, \"uSource\", [...params.source]);\n  setUniform(u, \"uFalloff\", params.falloff);\n  setUniform(u, \"uSpan\", params.span);\n  setUniform(u, \"uLens\", params.lens);\n  setUniform(u, \"uLensSigma\", params.lensSigma);\n}\n\nexport function setVareFrame(u: Uniforms, frame: VareFrame): 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": "vare.tsx",
      "target": "components/ui/vare.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  type Rgb,\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  POINTER_EASE,\n  resolveParams,\n  type VareColors,\n  type VareParams,\n} from \"./vare-field\";\nimport { vareFragmentShader } from \"./vare-shader\";\nimport {\n  setVareColors,\n  setVareFrame,\n  setVareParams,\n  vareUniforms,\n} from \"./vare-uniforms\";\n\nconst ROLES = [\"accent\", \"accent-2\", \"accent-alt\", \"ink\"] as const;\n\n/** Far enough in that the fronts have separated in the still frame. */\nconst STILL_TIME = 9;\n\nexport interface VareProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Wave rate multiplier. Defaults to 1. */\n  speed?: number;\n  /** When on, a phase lens follows the eased cursor and bends the bands.\n   * Defaults to true. */\n  interactive?: boolean;\n  /** Overall opacity of the bands, 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<VareParams>;\n  children?: React.ReactNode;\n}\n\nexport const Vare = React.forwardRef<HTMLDivElement, VareProps>(\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<VareParams>(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<VareColors>(() => {\n      const stop = (value: string | undefined, fallback: Rgb): Rgb =>\n        value ? resolveColor(value) : fallback;\n      const body = stop(cBody, tokens.colors.accent);\n      return {\n        body,\n        deep: stop(cDeep, tokens.colors[\"accent-2\"]),\n        edge: stop(cEdge, tokens.colors[\"accent-alt\"]),\n        pigment: pigmentFor(body, tokens.colors.ink),\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: vareFragmentShader,\n      uniforms: () => vareUniforms(rampRef.current, paramsRef.current),\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        setVareColors(u, rampRef.current);\n        setVareParams(u, paramsRef.current);\n        setVareFrame(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);\nVare.displayName = \"Vare\";\n"
    }
  ]
}