{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "contour",
  "type": "registry:ui",
  "title": "Contour",
  "description": "Living topographic map: marching-squares contour lines over an evolving 3D value-noise terrain, thin minor strokes with a bold accent index line every fifth level, over soft posterized elevation bands. Cool cartographic default. Canvas 2D, seeded, offscreen-paused, DPR-clamped, reduced-motion-safe.",
  "dependencies": [],
  "registryDependencies": [
    "https://crucibleui.com/r/use-visibility-pause.json",
    "https://crucibleui.com/r/use-reduced-motion.json"
  ],
  "files": [
    {
      "path": "registry/default/backgrounds/contour/contour.tsx",
      "type": "registry:ui",
      "target": "components/crucible/contour.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\nimport { useVisibilityPause } from \"@/hooks/use-visibility-pause\";\n\nexport interface ContourProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  /**\n   * Two tones `[line, accent]`. `line` colors the thin minor contour lines;\n   * `accent` colors the bolder index line (every fifth contour) and tints the\n   * soft elevation bands toward the peaks. Defaults to a cool cartographic\n   * slate-on-ice. Pass warm tones for a topographic-heat look.\n   * @default [\"#64788f\", \"#dbe9fb\"]\n   */\n  colors?: [string, string];\n  /** Morph-speed multiplier — how fast the terrain evolves. 1 = default. @default 1 */\n  speed?: number;\n  /** Line brightness / opacity, 0–2. @default 1 */\n  intensity?: number;\n  /**\n   * Spacing between contour lines in CSS px — the elevation interval. Smaller =\n   * denser lines (steep terrain read), larger = sparse. Clamped 12–80.\n   * @default 28\n   */\n  lineGap?: number;\n  /** devicePixelRatio ceiling. Clamped 1–2. @default 2 */\n  dpr?: number;\n  /** Freeze the map at a static, fully-drawn topographic frame. @default false */\n  paused?: boolean;\n}\n\nconst DEFAULT_COLORS: [string, string] = [\"#64788f\", \"#dbe9fb\"];\n\n/** Cool near-black valley floor the map sits on — never flat #000. */\nconst BG_RGB: [number, number, number] = [6, 9, 14];\n\n/** Spatial size of one terrain \"hill\" in CSS px — sets feature scale. */\nconst FEATURE_PX = 300;\n/** Target grid cell size in CSS px — finer = smoother contours, more cost. */\nconst CELL_PX = 14;\n/** fBm octaves — 3 gives ridged terrain without going heavy. */\nconst OCTAVES = 3;\n/** How fast the noise field morphs through its third (time) dimension. */\nconst Z_RATE = 0.12;\n/** A slow horizontal drift so the map also creeps, not only morphs. */\nconst DRIFT_RATE = 0.015;\n/** Frozen z-slice for the reduced-motion / paused frame (a handsome terrain). */\nconst Z_STATIC = 3.7;\n/** Every Nth contour is promoted to a bold accent index line. */\nconst INDEX_EVERY = 5;\n\nfunction hexToRgb(hex: string): [number, number, number] {\n  let h = hex.replace(\"#\", \"\").trim();\n  if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];\n  const int = parseInt(h, 16);\n  if (Number.isNaN(int) || h.length !== 6) return [100, 120, 143];\n  return [(int >> 16) & 255, (int >> 8) & 255, int & 255];\n}\n\n/** Integer hash → [0,1), stable across SSR intent and client. */\nfunction hash3(ix: number, iy: number, iz: number, seed: number): number {\n  let h = seed >>> 0;\n  h = Math.imul(h ^ (ix | 0), 0x27d4eb2f);\n  h = Math.imul(h ^ (iy | 0), 0x85ebca6b);\n  h = Math.imul(h ^ (iz | 0), 0xc2b2ae35);\n  h ^= h >>> 15;\n  return (h >>> 0) / 4294967296;\n}\n\nfunction fade(t: number): number {\n  return t * t * t * (t * (t * 6 - 15) + 10);\n}\n\nfunction lerp(a: number, b: number, t: number): number {\n  return a + (b - a) * t;\n}\n\n/** Trilinear value noise in 3D — smooth, seeded, morphs along z. */\nfunction vnoise3(x: number, y: number, z: number, seed: number): number {\n  const ix = Math.floor(x);\n  const iy = Math.floor(y);\n  const iz = Math.floor(z);\n  const ux = fade(x - ix);\n  const uy = fade(y - iy);\n  const uz = fade(z - iz);\n\n  const c000 = hash3(ix, iy, iz, seed);\n  const c100 = hash3(ix + 1, iy, iz, seed);\n  const c010 = hash3(ix, iy + 1, iz, seed);\n  const c110 = hash3(ix + 1, iy + 1, iz, seed);\n  const c001 = hash3(ix, iy, iz + 1, seed);\n  const c101 = hash3(ix + 1, iy, iz + 1, seed);\n  const c011 = hash3(ix, iy + 1, iz + 1, seed);\n  const c111 = hash3(ix + 1, iy + 1, iz + 1, seed);\n\n  const x00 = lerp(c000, c100, ux);\n  const x10 = lerp(c010, c110, ux);\n  const x01 = lerp(c001, c101, ux);\n  const x11 = lerp(c011, c111, ux);\n  const y0 = lerp(x00, x10, uy);\n  const y1 = lerp(x01, x11, uy);\n  return lerp(y0, y1, uz);\n}\n\n/** Fractal value noise, normalized to ~[0,1]. */\nfunction fbm(x: number, y: number, z: number, seed: number): number {\n  let amp = 0.5;\n  let freq = 1;\n  let sum = 0;\n  let norm = 0;\n  for (let o = 0; o < OCTAVES; o++) {\n    sum += amp * vnoise3(x * freq, y * freq, z * freq + o * 13.7, seed + o * 101);\n    norm += amp;\n    amp *= 0.5;\n    freq *= 2;\n  }\n  return sum / norm;\n}\n\ninterface Internals {\n  ctx: CanvasRenderingContext2D;\n  width: number;\n  height: number;\n  gx: number;\n  gy: number;\n  cw: number;\n  ch: number;\n  stepX: number;\n  stepY: number;\n  field: Float32Array;\n  cellMin: Float32Array;\n  cellMax: Float32Array;\n  fieldCanvas: HTMLCanvasElement;\n  fieldCtx: CanvasRenderingContext2D;\n  imgData: ImageData;\n  levels: number[];\n  lineRgb: [number, number, number];\n  accentRgb: [number, number, number];\n  peakRgb: [number, number, number];\n  intensity: number;\n  speed: number;\n  seed: number;\n  /** Speed-scaled terrain clock, accumulated per frame so speed changes never jump. */\n  phase: number;\n  lastElapsed: number;\n}\n\n/** Elevation range the contour levels span (trims dead edges of the field). */\nconst LO = 0.08;\nconst HI = 0.92;\n\nfunction buildLevels(lineGap: number): number[] {\n  // px spacing → contour count. 28px → ~13 lines; larger gap → fewer.\n  const count = Math.min(48, Math.max(5, Math.round(360 / lineGap)));\n  const span = HI - LO;\n  const out: number[] = [];\n  for (let k = 0; k < count; k++) out.push(LO + ((k + 0.5) / count) * span);\n  return out;\n}\n\n/** Recompute the scalar terrain field for the whole grid at time-slice z. */\nfunction computeField(it: Internals, z: number, driftX: number) {\n  const { field, gx, gy, stepX, stepY, seed } = it;\n  const cols = gx + 1;\n  for (let j = 0; j <= gy; j++) {\n    const wy = j * stepY;\n    const row = j * cols;\n    for (let i = 0; i <= gx; i++) {\n      field[row + i] = fbm(i * stepX + driftX, wy, z, seed);\n    }\n  }\n  // Per-cell min/max so the contour pass can cheaply skip cells no level crosses.\n  const { cellMin, cellMax } = it;\n  for (let j = 0; j < gy; j++) {\n    const r0 = j * cols;\n    const r1 = r0 + cols;\n    for (let i = 0; i < gx; i++) {\n      const a = field[r0 + i];\n      const b = field[r0 + i + 1];\n      const c = field[r1 + i + 1];\n      const d = field[r1 + i];\n      let mn = a;\n      let mx = a;\n      if (b < mn) mn = b;\n      else if (b > mx) mx = b;\n      if (c < mn) mn = c;\n      else if (c > mx) mx = c;\n      if (d < mn) mn = d;\n      else if (d > mx) mx = d;\n      const ci = j * gx + i;\n      cellMin[ci] = mn;\n      cellMax[ci] = mx;\n    }\n  }\n}\n\n/** Paint the soft, posterized elevation bands beneath the lines. */\nfunction paintBands(it: Internals) {\n  const { field, gx, gy, imgData, peakRgb } = it;\n  const cols = gx + 1;\n  const data = imgData.data;\n  const span = HI - LO;\n  // Band edges land on every ~3rd contour, so plateaus read as \"filled bands\".\n  const bands = Math.max(3, Math.round(it.levels.length / 3));\n  const [pr, pg, pb] = peakRgb;\n  const [br, bg, bb] = BG_RGB;\n  for (let j = 0; j <= gy; j++) {\n    const row = j * cols;\n    for (let i = 0; i <= gx; i++) {\n      const v = field[row + i];\n      // Normalize into the drawn elevation range, posterize into bands.\n      let t = (v - LO) / span;\n      t = t < 0 ? 0 : t > 1 ? 1 : t;\n      const shade = Math.floor(t * bands) / bands;\n      const p = (row + i) * 4;\n      data[p] = br + (pr - br) * shade;\n      data[p + 1] = bg + (pg - bg) * shade;\n      data[p + 2] = bb + (pb - bb) * shade;\n      data[p + 3] = 255;\n    }\n  }\n  it.fieldCtx.putImageData(imgData, 0, 0);\n  it.ctx.imageSmoothingEnabled = true;\n  it.ctx.drawImage(it.fieldCanvas, 0, 0, it.width, it.height);\n}\n\n/** Add the marching-squares segment(s) for one cell at one level to the path. */\nfunction addCellSegments(\n  path: Path2D,\n  L: number,\n  x0: number,\n  y0: number,\n  cw: number,\n  ch: number,\n  vtl: number,\n  vtr: number,\n  vbr: number,\n  vbl: number\n) {\n  const x1 = x0 + cw;\n  const y1 = y0 + ch;\n  // Edge crossing helpers (only the ones a case needs are evaluated).\n  const top = () => {\n    const d = vtr - vtl;\n    return x0 + cw * (Math.abs(d) < 1e-6 ? 0.5 : (L - vtl) / d);\n  };\n  const bottom = () => {\n    const d = vbr - vbl;\n    return x0 + cw * (Math.abs(d) < 1e-6 ? 0.5 : (L - vbl) / d);\n  };\n  const left = () => {\n    const d = vbl - vtl;\n    return y0 + ch * (Math.abs(d) < 1e-6 ? 0.5 : (L - vtl) / d);\n  };\n  const right = () => {\n    const d = vbr - vtr;\n    return y0 + ch * (Math.abs(d) < 1e-6 ? 0.5 : (L - vtr) / d);\n  };\n\n  const code =\n    (vtl > L ? 8 : 0) | (vtr > L ? 4 : 0) | (vbr > L ? 2 : 0) | (vbl > L ? 1 : 0);\n\n  switch (code) {\n    case 1: // BL\n    case 14: // ~BL\n      path.moveTo(x0, left());\n      path.lineTo(bottom(), y1);\n      break;\n    case 2: // BR\n    case 13: // ~BR\n      path.moveTo(bottom(), y1);\n      path.lineTo(x1, right());\n      break;\n    case 3: // BL+BR\n    case 12: // TL+TR\n      path.moveTo(x0, left());\n      path.lineTo(x1, right());\n      break;\n    case 4: // TR\n    case 11: // ~TR\n      path.moveTo(top(), y0);\n      path.lineTo(x1, right());\n      break;\n    case 6: // TR+BR\n    case 9: // TL+BL\n      path.moveTo(top(), y0);\n      path.lineTo(bottom(), y1);\n      break;\n    case 7: // ~TL\n    case 8: // TL\n      path.moveTo(top(), y0);\n      path.lineTo(x0, left());\n      break;\n    case 5: // TR+BL saddle\n      path.moveTo(top(), y0);\n      path.lineTo(x0, left());\n      path.moveTo(bottom(), y1);\n      path.lineTo(x1, right());\n      break;\n    case 10: // TL+BR saddle\n      path.moveTo(top(), y0);\n      path.lineTo(x1, right());\n      path.moveTo(x0, left());\n      path.lineTo(bottom(), y1);\n      break;\n    default: // 0 or 15 — no crossing\n      break;\n  }\n}\n\n/** One frame. `useStatic` renders the frozen z-slice with no drift. */\nfunction draw(it: Internals, useStatic: boolean) {\n  const z = useStatic ? Z_STATIC : it.phase * Z_RATE;\n  const driftX = useStatic ? 0 : it.phase * DRIFT_RATE;\n\n  computeField(it, z, driftX);\n\n  // Elevation bands first (opaque base), then the crisp lines on top.\n  paintBands(it);\n\n  const { ctx, field, gx, gy, cw, ch, cellMin, cellMax, levels } = it;\n  const cols = gx + 1;\n  const gInt = Math.min(Math.max(it.intensity, 0), 2);\n  ctx.imageSmoothingEnabled = true;\n  ctx.lineJoin = \"round\";\n  ctx.lineCap = \"round\";\n\n  for (let li = 0; li < levels.length; li++) {\n    const L = levels[li];\n    const isIndex = li % INDEX_EVERY === Math.floor(INDEX_EVERY / 2);\n    const rgb = isIndex ? it.accentRgb : it.lineRgb;\n    const alpha = Math.min((isIndex ? 0.92 : 0.5) * gInt, 1);\n    if (alpha < 0.01) continue;\n\n    const path = new Path2D();\n    for (let j = 0; j < gy; j++) {\n      const r0 = j * cols;\n      const r1 = r0 + cols;\n      const y0 = j * ch;\n      for (let i = 0; i < gx; i++) {\n        const ci = j * gx + i;\n        if (L < cellMin[ci] || L > cellMax[ci]) continue;\n        addCellSegments(\n          path,\n          L,\n          i * cw,\n          y0,\n          cw,\n          ch,\n          field[r0 + i],\n          field[r0 + i + 1],\n          field[r1 + i + 1],\n          field[r1 + i]\n        );\n      }\n    }\n    ctx.strokeStyle = `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${alpha})`;\n    ctx.lineWidth = isIndex ? 1.5 : 1;\n    ctx.stroke(path);\n  }\n}\n\n/**\n * Contour — a living topographic map. Elevation contour lines traced by\n * marching-squares over an evolving 3D value-noise terrain: thin minor lines\n * with a bolder accent index line every fifth level, laid over soft posterized\n * elevation bands that read as filled plateaus. The whole field slowly morphs\n * and drifts like a survey map redrawing itself. Canvas 2D: seeded and\n * SSR-safe, paused offscreen and on hidden tabs, DPR-clamped, reduced-motion-\n * safe (a static, fully-drawn terrain). Drop it inside any `relative` container.\n */\nexport function Contour({\n  colors = DEFAULT_COLORS,\n  speed = 1,\n  intensity = 1,\n  lineGap = 28,\n  dpr = 2,\n  paused = false,\n  className,\n  ...props\n}: ContourProps) {\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const internalsRef = React.useRef<Internals | null>(null);\n  const reducedMotion = useReducedMotion();\n  const staticFrame = paused || reducedMotion;\n  const staticFrameRef = React.useRef(staticFrame);\n  staticFrameRef.current = staticFrame;\n\n  const gap = Math.min(Math.max(lineGap, 12), 80);\n  const colorsKey = colors.join(\",\");\n\n  const tick = React.useCallback((elapsed: number) => {\n    const it = internalsRef.current;\n    if (!it) return;\n    let dt = elapsed - it.lastElapsed;\n    it.lastElapsed = elapsed;\n    if (dt > 0.1) dt = 0.1; // clamp tab-restore jumps\n    if (dt < 0) dt = 0;\n    it.phase += dt * it.speed;\n    draw(it, staticFrameRef.current);\n  }, []);\n\n  const containerRef = useVisibilityPause<HTMLDivElement>(tick, { paused: staticFrame });\n\n  React.useEffect(() => {\n    const canvas = canvasRef.current;\n    const container = containerRef.current;\n    if (!canvas || !container) return;\n\n    const ctx = canvas.getContext(\"2d\", { alpha: false });\n    if (!ctx) return;\n\n    const effectiveDpr = Math.min(\n      typeof window !== \"undefined\" ? window.devicePixelRatio || 1 : 1,\n      Math.min(Math.max(dpr, 1), 2)\n    );\n\n    const lineRgb = hexToRgb(colors[0]);\n    const accentRgb = hexToRgb(colors[1]);\n    // Sunlit-peak tint: a dim wash of the accent hue over the valley floor.\n    const peakRgb: [number, number, number] = [\n      Math.min(255, (BG_RGB[0] + accentRgb[0] * 0.15) | 0),\n      Math.min(255, (BG_RGB[1] + accentRgb[1] * 0.15) | 0),\n      Math.min(255, (BG_RGB[2] + accentRgb[2] * 0.15) | 0),\n    ];\n\n    const fieldCanvas = document.createElement(\"canvas\");\n    const fieldCtx = fieldCanvas.getContext(\"2d\", { alpha: false });\n    if (!fieldCtx) return;\n\n    const it: Internals = {\n      ctx,\n      width: 0,\n      height: 0,\n      gx: 0,\n      gy: 0,\n      cw: 0,\n      ch: 0,\n      stepX: 0,\n      stepY: 0,\n      field: new Float32Array(0),\n      cellMin: new Float32Array(0),\n      cellMax: new Float32Array(0),\n      fieldCanvas,\n      fieldCtx,\n      imgData: fieldCtx.createImageData(1, 1),\n      levels: buildLevels(gap),\n      lineRgb,\n      accentRgb,\n      peakRgb,\n      intensity,\n      speed,\n      seed: 0x1234,\n      phase: internalsRef.current?.phase ?? 0,\n      lastElapsed: internalsRef.current?.lastElapsed ?? 0,\n    };\n    internalsRef.current = it;\n\n    const onContextLost = (e: Event) => e.preventDefault();\n    canvas.addEventListener(\"contextlost\", onContextLost);\n\n    const resize = () => {\n      const w = Math.max(container.clientWidth, 1);\n      const h = Math.max(container.clientHeight, 1);\n      canvas.width = Math.round(w * effectiveDpr);\n      canvas.height = Math.round(h * effectiveDpr);\n      canvas.style.width = \"100%\";\n      canvas.style.height = \"100%\";\n      ctx.setTransform(effectiveDpr, 0, 0, effectiveDpr, 0, 0);\n\n      const gx = Math.min(200, Math.max(8, Math.round(w / CELL_PX)));\n      const gy = Math.min(200, Math.max(8, Math.round(h / CELL_PX)));\n      it.width = w;\n      it.height = h;\n      it.gx = gx;\n      it.gy = gy;\n      it.cw = w / gx;\n      it.ch = h / gy;\n      // Isotropic feature scale regardless of grid resolution.\n      it.stepX = (w / FEATURE_PX) / gx;\n      it.stepY = (h / FEATURE_PX) / gy;\n      it.field = new Float32Array((gx + 1) * (gy + 1));\n      it.cellMin = new Float32Array(gx * gy);\n      it.cellMax = new Float32Array(gx * gy);\n      fieldCanvas.width = gx + 1;\n      fieldCanvas.height = gy + 1;\n      it.imgData = fieldCtx.createImageData(gx + 1, gy + 1);\n\n      // Never flash empty — paint one frame immediately.\n      draw(it, staticFrameRef.current);\n    };\n\n    const ro = new ResizeObserver(resize);\n    ro.observe(container);\n    resize();\n\n    return () => {\n      ro.disconnect();\n      canvas.removeEventListener(\"contextlost\", onContextLost);\n      internalsRef.current = null;\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [dpr, gap, colorsKey]);\n\n  // Live-sync speed / intensity without rebuilding the map.\n  React.useEffect(() => {\n    const it = internalsRef.current;\n    if (!it) return;\n    it.speed = speed;\n    it.intensity = intensity;\n    if (staticFrameRef.current) draw(it, true);\n  }, [speed, intensity]);\n\n  // Re-render the frozen frame in place when paused / reduced-motion toggles.\n  React.useEffect(() => {\n    const it = internalsRef.current;\n    if (!it || !staticFrame) return;\n    draw(it, true);\n  }, [staticFrame]);\n\n  return (\n    <div\n      ref={containerRef}\n      aria-hidden\n      data-crucible=\"contour\"\n      className={cn(\"absolute inset-0 -z-10 overflow-hidden bg-neutral-950\", className)}\n      {...props}\n    >\n      <canvas ref={canvasRef} className=\"block h-full w-full\" />\n      {/* Soft vignette so the map sinks into the frame edges. */}\n      <div\n        className=\"pointer-events-none absolute inset-0\"\n        style={{\n          background:\n            \"radial-gradient(120% 120% at 50% 42%, transparent 58%, rgba(0,0,0,0.5) 100%)\",\n        }}\n      />\n    </div>\n  );\n}\n"
    }
  ]
}