{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "topo-lines",
  "title": "Topo Lines",
  "description": "Topographic contour lines around a few seeded peaks breathe and shift in a seamless loop, one contour picked out in the accent color.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/topo-lines/index.tsx",
      "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport {\n  AbsoluteFill,\n  random,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nexport type TopoLinesProps = {\n  /** Number of contour levels. */\n  levels?: number;\n  /** Number of peaks in the height field (1 to 3 reads best). */\n  peaks?: number;\n  /** Contour drawn in the accent, counted from the lowest; -1 disables. */\n  highlight?: number;\n  /** Every n-th contour is a heavier index line; 0 disables. */\n  indexEvery?: number;\n  /** Sampling cells across the short side; higher is smoother and slower. */\n  resolution?: number;\n  /** Frames per seamless loop of the breathing field. */\n  loopFrames?: number;\n  seed?: number;\n  speed?: number;\n  background?: string;\n  color?: string;\n  accent?: string;\n};\n\nexport const topoLinesDefaults: Required<TopoLinesProps> = {\n  levels: 16,\n  peaks: 3,\n  highlight: 9,\n  indexEvery: 4,\n  resolution: 90,\n  loopFrames: 300,\n  seed: 2,\n  speed: 1,\n  background: \"#111111\",\n  color: \"#FFFFFF\",\n  accent: \"#E5322D\",\n};\n\nconst TAU = Math.PI * 2;\n\n// Cone height falls from 1 to 0 over this distance (short side = 1), so\n// contours are evenly spaced like a real survey map.\nconst REACH = 0.95;\n// Smooth-max softness: peaks merge into shared outer rings without creases.\nconst SOFT = 0.045;\n\ntype Peak = {\n  x: number;\n  y: number;\n  amp: number;\n  phase: number;\n  orbit: number;\n  lobes: [number, number];\n};\ntype Ripple = { fx: number; fy: number; phase: number };\n\nfunction makeTerrain(seed: number, peaks: number, w: number, h: number) {\n  const r = (k: string) => random(`topo-lines-${seed}-${k}`);\n  const m = Math.max(1, Math.round(peaks));\n  const turn = r(\"turn\") * TAU;\n  const list: Peak[] = Array.from({ length: m }, (_, k) => {\n    // Spread peaks around the centre so each keeps its own nest of rings.\n    const a = turn + (k * TAU) / m + (r(`j${k}`) - 0.5) * 0.8;\n    const spread = m === 1 ? 0.06 : 0.24 + r(`d${k}`) * 0.06;\n    return {\n      x: w / 2 + Math.cos(a) * spread * w,\n      y: h / 2 + Math.sin(a) * spread * h * 1.1,\n      amp: k === 0 ? 1 : 0.7 + r(`a${k}`) * 0.22,\n      phase: r(`p${k}`) * TAU,\n      orbit: 0.025 + r(`o${k}`) * 0.025,\n      lobes: [r(`l2${k}`) * TAU, r(`l3${k}`) * TAU],\n    };\n  });\n  const ripples: Ripple[] = Array.from({ length: 3 }, (_, k) => ({\n    fx: (r(`rx${k}`) * 2 - 1) * (3 + k * 2),\n    fy: (r(`ry${k}`) * 2 - 1) * (3 + k * 2),\n    phase: r(`rp${k}`) * TAU,\n  }));\n  // Height at (x, y). `tau` walks once around a circle per loop: peaks drift\n  // on small orbits, breathe, and their low harmonics rotate a whole number\n  // of times, so the last frame matches the first.\n  return (x: number, y: number, tau: number) => {\n    let sum = 0;\n    for (const p of list) {\n      const px = p.x + Math.cos(tau + p.phase) * p.orbit;\n      const py = p.y + Math.sin(tau + p.phase) * p.orbit;\n      const dx = x - px;\n      const dy = y - py;\n      const theta = Math.atan2(dy, dx);\n      const warp =\n        1 +\n        0.14 * Math.sin(2 * theta + p.lobes[0] + tau) +\n        0.08 * Math.sin(3 * theta + p.lobes[1] - tau);\n      const amp = p.amp * (1 + 0.05 * Math.sin(tau + p.phase));\n      const v = amp - (Math.hypot(dx, dy) * warp) / REACH;\n      sum += Math.exp(v / SOFT);\n    }\n    let v = SOFT * Math.log(sum);\n    for (const q of ripples) {\n      v +=\n        0.03 *\n        Math.sin(q.fx * x + q.fy * y + q.phase + Math.sin(tau + q.phase) * 1.4);\n    }\n    return v;\n  };\n}\n\nconst fmt = (n: number) => Math.round(n * 10) / 10;\n\nexport function TopoLines({\n  levels = topoLinesDefaults.levels,\n  peaks = topoLinesDefaults.peaks,\n  highlight = topoLinesDefaults.highlight,\n  indexEvery = topoLinesDefaults.indexEvery,\n  resolution = topoLinesDefaults.resolution,\n  loopFrames = topoLinesDefaults.loopFrames,\n  seed = topoLinesDefaults.seed,\n  speed = topoLinesDefaults.speed,\n  background = topoLinesDefaults.background,\n  color = topoLinesDefaults.color,\n  accent = topoLinesDefaults.accent,\n}: TopoLinesProps) {\n  const frame = useCurrentFrame();\n  const { width, height } = useVideoConfig();\n  const unit = Math.min(width, height);\n  const w = width / unit;\n  const h = height / unit;\n  const terrain = useMemo(\n    () => makeTerrain(seed, peaks, w, h),\n    [seed, peaks, w, h],\n  );\n  const tau = (TAU * frame * speed) / Math.max(1, loopFrames);\n\n  const step = 1 / Math.max(20, resolution);\n  const nx = Math.ceil(w / step) + 1;\n  const ny = Math.ceil(h / step) + 1;\n  const field = new Float32Array(nx * ny);\n  for (let j = 0; j < ny; j++) {\n    for (let i = 0; i < nx; i++) {\n      field[j * nx + i] = terrain(i * step, j * step, tau);\n    }\n  }\n\n  const count = Math.max(1, Math.round(levels));\n  const thresholds = Array.from(\n    { length: count },\n    (_, l) => 0.04 + (l * 0.9) / count,\n  );\n  const paths = { plain: \"\", index: \"\", accent: \"\" };\n  const s = step * unit;\n  const X = (i: number) => fmt(i * s);\n  const Y = (j: number) => fmt(j * s);\n\n  // Marching squares. Segments that share a cell edge land on the same\n  // interpolated point, so round caps make them read as continuous curves.\n  for (let j = 0; j < ny - 1; j++) {\n    for (let i = 0; i < nx - 1; i++) {\n      const a = field[j * nx + i];\n      const b = field[j * nx + i + 1];\n      const c = field[(j + 1) * nx + i + 1];\n      const d = field[(j + 1) * nx + i];\n      const lo = Math.min(a, b, c, d);\n      const hi = Math.max(a, b, c, d);\n      for (let l = 0; l < count; l++) {\n        const t = thresholds[l];\n        if (t < lo || t >= hi) continue;\n        const code =\n          (a > t ? 8 : 0) | (b > t ? 4 : 0) | (c > t ? 2 : 0) | (d > t ? 1 : 0);\n        const top = () => `${X(i + (t - a) / (b - a))} ${Y(j)}`;\n        const right = () => `${X(i + 1)} ${Y(j + (t - b) / (c - b))}`;\n        const bottom = () => `${X(i + (t - d) / (c - d))} ${Y(j + 1)}`;\n        const left = () => `${X(i)} ${Y(j + (t - a) / (d - a))}`;\n        let seg = \"\";\n        switch (code) {\n          case 1:\n          case 14:\n            seg = `M ${left()} L ${bottom()} `;\n            break;\n          case 2:\n          case 13:\n            seg = `M ${bottom()} L ${right()} `;\n            break;\n          case 3:\n          case 12:\n            seg = `M ${left()} L ${right()} `;\n            break;\n          case 4:\n          case 11:\n            seg = `M ${top()} L ${right()} `;\n            break;\n          case 6:\n          case 9:\n            seg = `M ${top()} L ${bottom()} `;\n            break;\n          case 7:\n          case 8:\n            seg = `M ${left()} L ${top()} `;\n            break;\n          case 5:\n            seg = `M ${left()} L ${top()} M ${bottom()} L ${right()} `;\n            break;\n          case 10:\n            seg = `M ${top()} L ${right()} M ${left()} L ${bottom()} `;\n            break;\n        }\n        if (l === highlight) paths.accent += seg;\n        else if (indexEvery > 0 && (l + 1) % indexEvery === 0)\n          paths.index += seg;\n        else paths.plain += seg;\n      }\n    }\n  }\n\n  const px = unit / 720;\n  return (\n    <AbsoluteFill style={{ backgroundColor: background }}>\n      <svg\n        width=\"100%\"\n        height=\"100%\"\n        viewBox={`0 0 ${width} ${height}`}\n        role=\"img\"\n        aria-label=\"Topographic contour lines\"\n        style={{ display: \"block\" }}\n      >\n        <g fill=\"none\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n          <path\n            d={paths.plain}\n            stroke={color}\n            strokeOpacity={0.38}\n            strokeWidth={1.1 * px}\n          />\n          <path\n            d={paths.index}\n            stroke={color}\n            strokeOpacity={0.85}\n            strokeWidth={1.8 * px}\n          />\n          <path d={paths.accent} stroke={accent} strokeWidth={2.6 * px} />\n        </g>\n      </svg>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/topo-lines.tsx"
    }
  ],
  "type": "registry:component"
}
