{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "routa",
  "type": "registry:ui",
  "dependencies": [
    "clsx",
    "motion",
    "ogl",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "https://usva.build/r/atmospheres-core.json"
  ],
  "files": [
    {
      "path": "routa-field.ts",
      "target": "components/ui/routa-field.ts",
      "type": "registry:ui",
      "content": "import {\n  type BlendMode,\n  pigmentFor,\n  type Rgb,\n} from \"./atmospheres-color\";\n\nexport interface RoutaParams {\n  /** Frost cells across one short-side unit. */\n  cellScale: number;\n  /** Height of the heaved cell interiors. */\n  heave: number;\n  /** Width of the fissure at each cell wall. */\n  crackWidth: number;\n  /** How far the cell walls fall below the heaved surface. */\n  crackDepth: number;\n  /** Spatial frequency of the broad unevenness under the frost. */\n  unevenScale: number;\n  /** How differently neighbouring cells lift. */\n  uneven: number;\n  /** Domain travel per second. Deliberately close to still. */\n  drift: number;\n  /** One-shot fissure propagation per second. */\n  growthRate: number;\n  /** Height-gradient gain before the normal is built. */\n  slope: number;\n  /** Oren-Nayar roughness. */\n  rough: number;\n  /** Ambient fill under the raking key. */\n  ambient: number;\n  /** Strength of the raking key. */\n  key: number;\n  /** How much damp the clay holds in the lee of a heave. Light grounds only. */\n  relief: number;\n  /** Dither amplitude for dark-ground emission. */\n  dither: number;\n}\n\nexport const ROUTA_DEFAULTS: RoutaParams = {\n  cellScale: 3.8,\n  heave: 0.075,\n  crackWidth: 0.055,\n  crackDepth: 0.045,\n  unevenScale: 0.72,\n  uneven: 0.34,\n  drift: 0.0025,\n  growthRate: 0.028,\n  slope: 1.35,\n  rough: 0.92,\n  ambient: 0.32,\n  key: 0.88,\n  relief: 0.18,\n  dither: 0.005,\n};\n\nexport function resolveParams(\n  overrides: Partial<RoutaParams> = {},\n): RoutaParams {\n  const merged = { ...ROUTA_DEFAULTS, ...overrides };\n  return {\n    ...merged,\n    cellScale: Math.max(merged.cellScale, 0.5),\n    heave: Math.max(merged.heave, 0),\n    crackWidth: Math.min(Math.max(merged.crackWidth, 0.005), 0.2),\n    crackDepth: Math.max(merged.crackDepth, 0),\n    uneven: Math.min(Math.max(merged.uneven, 0), 0.8),\n    // Past about a third the damp stops reading as a lit surface and starts\n    // competing with the fissures, which are the subject on clay.\n    relief: Math.min(Math.max(merged.relief, 0), 0.35),\n    drift: Math.max(merged.drift, 0),\n    growthRate: Math.max(merged.growthRate, 0),\n    rough: Math.min(Math.max(merged.rough, 0), 1),\n  };\n}\n\nexport const ROUTA_ROLES = [\"bg\", \"ink\", \"accent\", \"accent-alt\"] as const;\n\nexport type RoutaRole = (typeof ROUTA_ROLES)[number];\n\nexport interface RoutaColors {\n  /** The dark pigment held in the fissures on clay. */\n  pigment: Rgb;\n  /** The cold raking key caught by heaved crests on a dark ground. */\n  emission: Rgb;\n  /** The frozen surface itself on a dark ground. */\n  body: Rgb;\n  /** Under the surface: the fissures, and ground the key never reaches. */\n  fissure: Rgb;\n}\n\nfunction mix(a: Rgb, b: Rgb, t: number): Rgb {\n  return [\n    a[0] + (b[0] - a[0]) * t,\n    a[1] + (b[1] - a[1]) * t,\n    a[2] + (b[2] - a[2]) * t,\n  ];\n}\n\n/**\n * The theme swap changes the material, not the shader, the way kynnös already\n * does it. On clay the fissures hold pigment and the frost is what is left of\n * the ground. On a dark ground it is black ice: a surface that is always there,\n * a grazing key that only the heaved crests catch, and fissures cut below it.\n *\n * The body is the whole point. Without one the shader painted crests and left\n * everything between them transparent, so the relief had nothing to sit on and\n * read as lumps of glow rather than frozen earth.\n */\nexport function buildColors(\n  roles: Record<RoutaRole, Rgb>,\n  mode: BlendMode,\n  keyColor?: Rgb,\n): RoutaColors {\n  const hue = mode === \"absorptive\" ? roles.accent : roles[\"accent-alt\"];\n  return {\n    pigment: pigmentFor(hue, roles.ink),\n    emission: keyColor ?? mix(roles.accent, roles.ink, 0.55),\n    body: mix(roles.bg, roles.ink, 0.09),\n    fissure: mix(roles.bg, [0, 0, 0], 0.55),\n  };\n}\n\n/** A low raking key, cold and long across the broken ground. */\nexport const DEFAULT_LIGHT: [number, number, number] = [-0.68, 0.46, 0.38];\n"
    },
    {
      "path": "routa-shader.ts",
      "target": "components/ui/routa-shader.ts",
      "type": "registry:ui",
      "content": "import { glsl } from \"./atmospheres-glsl\";\n\nexport const routaVertexShader = /* glsl */ `#version 300 es\nin vec2 position;\nvoid main() {\n  gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nexport const routaFragmentShader = /* glsl */ `#version 300 es\nprecision highp float;\n\nuniform float uTime;\nuniform vec2  uResolution;\nuniform float uCellScale;\nuniform float uHeave;\nuniform float uCrackWidth;\nuniform float uCrackDepth;\nuniform float uUnevenScale;\nuniform float uUneven;\nuniform float uDrift;\nuniform float uGrowthRate;\nuniform float uSlope;\nuniform float uRough;\nuniform float uAmbient;\nuniform float uKey;\nuniform float uRelief;\nuniform float uDither;\nuniform float uAlpha;\nuniform float uAbsorb;\nuniform vec3  uLightDir;\nuniform vec3  uPigment;\nuniform vec3  uEmission;\nuniform vec3  uBody;\nuniform vec3  uFissure;\nuniform float uStainFloor;\n\nout vec4 fragColor;\n\n${glsl(\"fbm\", \"worley\", \"dither\", \"stain\", \"composite\")}\n\nconst vec3 VIEW = vec3(0.0, 0.0, 1.0);\nconst float SIGMA = 1.12;\nconst float SOAK = 0.52;\n/** How far in from a fissure the plate has finished rising, in wall units. */\nconst float PLATE_EDGE = 0.2;\n\nvec2 frostAt(vec2 p) {\n  vec2 travel = vec2(uTime * uDrift, -uTime * uDrift * 0.37);\n  vec2 q = p + travel;\n  float wall = craquelure(q * uCellScale);\n  float fissure = 1.0 - smoothstep(0.0, uCrackWidth, wall);\n  float arrival = fbm2(\n    q * uCellScale * 0.55 + vec2(7.3, 1.9),\n    2\n  ) / 0.75;\n  float progress = clamp(0.1 + uTime * uGrowthRate, 0.0, 1.0);\n  fissure *= smoothstep(arrival - 0.08, arrival + 0.08, progress);\n  /* A heaved plate is flat, and flatness is also what makes it drawable.\n     craquelure is F2 - F1, whose slope kinks along the medial axis of every\n     cell, where the second-nearest cell changes. The normal is built by\n     differencing the height, so a slope kink becomes a hard seam straight\n     across each plate. Doming the interior put a gradient there for the kink\n     to bend; a flat interior gives it nothing, and the shape lands where it\n     belongs, on the shoulder falling into the seam. */\n  float dome = smoothstep(uCrackWidth, uCrackWidth + PLATE_EDGE, wall);\n  dome = dome * dome * (3.0 - 2.0 * dome);\n  float broad = fbm2(q * uUnevenScale + vec2(13.1, -7.4), 3) / 0.875;\n  float lift = dome * mix(1.0 - uUneven, 1.0 + uUneven, broad);\n  return vec2(uHeave * lift - uCrackDepth * fissure, fissure);\n}\n\nfloat heightAt(vec2 p) {\n  return frostAt(p).x;\n}\n\nfloat orenNayar(vec3 n, vec3 l, float rough) {\n  float s2 = rough * rough;\n  float a = 1.0 - 0.5 * s2 / (s2 + 0.33);\n  float b = 0.45 * s2 / (s2 + 0.09);\n  float ndl = dot(n, l);\n  float ndv = dot(n, VIEW);\n  float thetaL = acos(clamp(ndl, -1.0, 1.0));\n  float thetaV = acos(clamp(ndv, -1.0, 1.0));\n  float alpha = max(thetaL, thetaV);\n  float beta = min(thetaL, thetaV);\n  vec3 lp = l - n * ndl;\n  vec3 vp = VIEW - n * ndv;\n  float cosPhi = 0.0;\n  if (length(lp) > 1e-4 && length(vp) > 1e-4) {\n    cosPhi = max(dot(normalize(lp), normalize(vp)), 0.0);\n  }\n  return max(ndl, 0.0) * (a + b * cosPhi * sin(alpha) * tan(min(beta, 1.5)));\n}\n\nvoid main() {\n  float unit = max(min(uResolution.x, uResolution.y), 1.0);\n  vec2 p = (gl_FragCoord.xy - 0.5 * uResolution) / unit;\n  vec2 field = frostAt(p);\n  float height = field.x;\n\n  float eps = 1.5 / unit;\n  float heightX = heightAt(p + vec2(eps, 0.0));\n  float heightY = heightAt(p + vec2(0.0, eps));\n  vec3 normal = normalize(vec3(\n    (height - heightX) * uSlope,\n    (height - heightY) * uSlope,\n    eps * 3.0\n  ));\n\n  vec3 light = normalize(uLightDir);\n  float diffuse = orenNayar(normal, light, uRough);\n  float lit = clamp(uAmbient + uKey * diffuse, 0.0, 1.2);\n\n  // Black ice. The ground is opaque and always painted, and the key only picks\n  // out the crests: that surface is what separates frozen earth from a handful\n  // of lit lumps floating on whatever happens to be behind the canvas.\n  if (uAbsorb < 0.5) {\n    float crest = smoothstep(0.0, uHeave, max(height, 0.0));\n    float rake = smoothstep(0.36, 1.0, lit);\n    vec3 ground = mix(uFissure, uBody, smoothstep(0.18, 0.72, lit));\n    vec3 col = mix(ground, uEmission, crest * rake * 0.85);\n    col = mix(col, uFissure, field.y * 0.9);\n    col = dither(col, gl_FragCoord.xy, uDither);\n    fragColor = composite(col, uAlpha);\n    return;\n  }\n\n  /* Clay carries the heave as damp, never as a darker multiply: the multiply\n     composite is inert under isolate, so a light ground can only show relief by\n     holding more pigment. Wet in the lee of a plate, dry where the key lands.\n     Kept out of the fissures so it never doubles with the pigment already\n     there, and an order under them so the seams stay the subject. */\n  float relief = smoothstep(0.62, 0.18, lit) * (1.0 - field.y) * uRelief;\n  float fissure = field.y * mix(1.0, 0.68, diffuse);\n  vec3 absorbed = hold(uPigment, uStainFloor);\n  float alpha = clamp(soak(fissure * SOAK + relief, SIGMA) * uAlpha, 0.0, 1.0);\n  fragColor = vec4(absorbed * alpha, alpha);\n}\n`;\n"
    },
    {
      "path": "routa-uniforms.ts",
      "target": "components/ui/routa-uniforms.ts",
      "type": "registry:ui",
      "content": "import { MAX_STAIN } from \"./atmospheres-color\";\nimport {\n  setUniform,\n  type Uniforms,\n} from \"./atmospheres-gl\";\nimport type { RoutaColors, RoutaParams } from \"./routa-field\";\n\nexport interface RoutaFrame {\n  time: number;\n  alpha: number;\n  absorb: number;\n  light: [number, number, number];\n}\n\nexport function routaUniforms(\n  colors: RoutaColors,\n  params: RoutaParams,\n  light: [number, number, number],\n): Uniforms {\n  return {\n    uTime: { value: 0 },\n    uResolution: { value: [1, 1] },\n    uCellScale: { value: params.cellScale },\n    uHeave: { value: params.heave },\n    uCrackWidth: { value: params.crackWidth },\n    uCrackDepth: { value: params.crackDepth },\n    uUnevenScale: { value: params.unevenScale },\n    uUneven: { value: params.uneven },\n    uDrift: { value: params.drift },\n    uGrowthRate: { value: params.growthRate },\n    uSlope: { value: params.slope },\n    uRough: { value: params.rough },\n    uAmbient: { value: params.ambient },\n    uKey: { value: params.key },\n    uRelief: { value: params.relief },\n    uDither: { value: params.dither },\n    uAlpha: { value: 1 },\n    uAbsorb: { value: 1 },\n    uLightDir: { value: [...light] },\n    uPigment: { value: [...colors.pigment] },\n    uEmission: { value: [...colors.emission] },\n    uBody: { value: [...colors.body] },\n    uFissure: { value: [...colors.fissure] },\n    uStainFloor: { value: MAX_STAIN },\n  };\n}\n\nexport function setRoutaColors(u: Uniforms, colors: RoutaColors): void {\n  setUniform(u, \"uPigment\", [...colors.pigment]);\n  setUniform(u, \"uEmission\", [...colors.emission]);\n  setUniform(u, \"uBody\", [...colors.body]);\n  setUniform(u, \"uFissure\", [...colors.fissure]);\n}\n\nexport function setRoutaParams(u: Uniforms, params: RoutaParams): void {\n  setUniform(u, \"uCellScale\", params.cellScale);\n  setUniform(u, \"uHeave\", params.heave);\n  setUniform(u, \"uCrackWidth\", params.crackWidth);\n  setUniform(u, \"uCrackDepth\", params.crackDepth);\n  setUniform(u, \"uUnevenScale\", params.unevenScale);\n  setUniform(u, \"uUneven\", params.uneven);\n  setUniform(u, \"uDrift\", params.drift);\n  setUniform(u, \"uGrowthRate\", params.growthRate);\n  setUniform(u, \"uSlope\", params.slope);\n  setUniform(u, \"uRough\", params.rough);\n  setUniform(u, \"uAmbient\", params.ambient);\n  setUniform(u, \"uKey\", params.key);\n  setUniform(u, \"uRelief\", params.relief);\n  setUniform(u, \"uDither\", params.dither);\n}\n\nexport function setRoutaFrame(u: Uniforms, frame: RoutaFrame): void {\n  setUniform(u, \"uTime\", frame.time);\n  setUniform(u, \"uAlpha\", frame.alpha);\n  setUniform(u, \"uAbsorb\", frame.absorb);\n  setUniform(u, \"uLightDir\", [...frame.light]);\n}\n"
    },
    {
      "path": "routa.tsx",
      "target": "components/ui/routa.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 { useGlCanvas } from \"./use-gl-canvas\";\nimport { useTokenColors } from \"./use-token-colors\";\nimport {\n  buildColors,\n  DEFAULT_LIGHT,\n  ROUTA_ROLES,\n  type RoutaParams,\n  resolveParams,\n} from \"./routa-field\";\nimport { routaFragmentShader } from \"./routa-shader\";\nimport {\n  routaUniforms,\n  setRoutaColors,\n  setRoutaFrame,\n  setRoutaParams,\n} from \"./routa-uniforms\";\n\nexport interface RoutaLight {\n  /** Keep z low: the heave only reads when the key rakes across it. */\n  direction?: [number, number, number];\n  /** Optional CSS colour for the dark-ground key. */\n  color?: string;\n}\n\nexport interface RoutaProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Domain drift multiplier. Defaults to 1 and remains barely perceptible. */\n  speed?: number;\n  /** Force the material. By default the ground chooses frost-light or stain. */\n  mode?: BlendMode;\n  /** Raking light direction and optional dark-ground colour. */\n  light?: RoutaLight;\n  /** Overall opacity, 0..1. Defaults to 1. */\n  opacity?: number;\n  /** Escape hatch for frost cell, fissure, and relief parameters. */\n  params?: Partial<RoutaParams>;\n  children?: React.ReactNode;\n}\n\nconst STILL_TIME = 27;\n\nexport const Routa = React.forwardRef<HTMLDivElement, RoutaProps>(\n  (\n    {\n      speed = 1,\n      mode,\n      light,\n      opacity = 1,\n      params,\n      className,\n      children,\n      ...props\n    },\n    forwardedRef,\n  ) => {\n    const lightColor = light?.color;\n    const direction = light?.direction ?? DEFAULT_LIGHT;\n\n    const speedRef = React.useRef(speed);\n    speedRef.current = speed;\n    const opacityRef = React.useRef(opacity);\n    opacityRef.current = opacity;\n    const lightRef = React.useRef(direction);\n    lightRef.current = direction;\n\n    const scopeRef = React.useRef<HTMLDivElement | null>(null);\n    const tokens = useTokenColors(ROUTA_ROLES, { scopeRef });\n    const blend = resolveBlendMode(mode, tokens.bg);\n\n    const colors = React.useMemo(\n      () =>\n        buildColors(\n          tokens.colors,\n          blend,\n          lightColor ? resolveColor(lightColor) : undefined,\n        ),\n      [tokens.colors, blend, lightColor],\n    );\n    const colorsRef = React.useRef(colors);\n    colorsRef.current = colors;\n\n    const resolved = resolveParams(params);\n    const paramsRef = React.useRef(resolved);\n    paramsRef.current = resolved;\n\n    const blendRef = React.useRef(blend);\n    blendRef.current = blend;\n\n    const canvas = useGlCanvas({\n      fragment: routaFragmentShader,\n      stillTime: STILL_TIME,\n      maxDpr: 1.5,\n      uniforms: () =>\n        routaUniforms(colorsRef.current, paramsRef.current, lightRef.current),\n      onFrame: (u, frame) => {\n        setRoutaColors(u, colorsRef.current);\n        setRoutaParams(u, paramsRef.current);\n        setRoutaFrame(u, {\n          time: frame.time * speedRef.current,\n          alpha: opacityRef.current,\n          absorb: blendUniform(blendRef.current),\n          light: lightRef.current,\n        });\n      },\n    });\n\n    const { redraw } = canvas;\n    // biome-ignore lint/correctness/useExhaustiveDependencies: the still frame must repaint when the material, key, or ground changes.\n    React.useEffect(() => {\n      redraw();\n    }, [redraw, colors, direction, 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);\nRouta.displayName = \"Routa\";\n"
    }
  ]
}