{
  "name": "topo-background",
  "type": "registry:component",
  "dependencies": [],
  "files": [
    {
      "type": "registry:component",
      "content": "// biome-ignore-all lint/suspicious/noBitwiseOperators: the noise hash and the marching-squares case index are bit arithmetic.\n\"use client\";\n\n/**\n * @author: @dorianbaffier\n * @description: Topo Background\n * @version: 1.1.0\n * @date: 2026-10-14\n * @license: MIT\n * @website: https://kokonutui.com\n * @github: https://github.com/kokonut-labs/kokonutui\n */\n\nimport { type ReactNode, useEffect, useRef } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface TopoBackgroundProps {\n  /**\n   * Extra classes for the root. The map draws on a transparent canvas, so\n   * `className=\"bg-transparent\"` lays it over your own page background.\n   */\n  className?: string;\n  /** Content shown over the map. Pass `null` for a bare background. */\n  children?: ReactNode;\n  /** Height difference between two contour lines. Lower draws more lines. */\n  interval?: number;\n  /** How fast the terrain drifts. 0 holds it still. */\n  speed?: number;\n  /** Raise a hill under the cursor and send a ripple on click. */\n  interactive?: boolean;\n  /** Seed for the terrain, so a page always opens on the same map. */\n  seed?: number;\n  /** Print elevations on the index contours, as on a survey map. */\n  labels?: boolean;\n}\n\n// Distance between field samples, in CSS px. Contours are traced through\n// this grid, so it trades smoothness for work per frame.\nconst CELL = 12;\n// Width of a hill in the terrain, in px.\nconst TERRAIN_SCALE = 360;\n// Every fifth line is an index contour, drawn heavier, as on a real map.\nconst INDEX_EVERY = 5;\n// The hill that follows the cursor: height in field units, and spread (px).\nconst HILL_HEIGHT = 0.5;\nconst HILL_SPREAD = 120;\n// How quickly the hill catches up with the cursor (per second) and how\n// quickly it rises or sinks when the cursor arrives or leaves.\nconst HILL_FOLLOW = 9;\nconst HILL_RISE = 5;\n// A click sends a ring outwards through the terrain.\nconst RIPPLE_MS = 1800;\nconst RIPPLE_SPEED = 0.24; // px per ms\nconst RIPPLE_WIDTH = 42;\nconst RIPPLE_HEIGHT = 0.2;\n// Distance between the crests of the wave train, in px.\nconst RIPPLE_WAVELENGTH = 96;\n// When nothing is moving but the drift, a frame every 33ms is plenty.\nconst IDLE_FRAME_MS = 33;\n// Elevation labels: one per slot of this size at most (px), riding along\n// with the terrain. Elevation of level n is BASE + n * STEP metres, so the\n// index contours read as round hundreds.\nconst LABEL_SPACING = 210;\nconst LABEL_BASE = 500;\nconst LABEL_STEP = 20;\n// Labels keep clear of the faded top and bottom edges.\nconst LABEL_MARGIN = 0.16;\nconst MAX_LABEL_ANGLE = Math.PI / 3;\nconst LABEL_FONT =\n  \"500 10px ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif\";\n\n/** The best spot found so far in each label slot. */\ninterface LabelSlots {\n  drift: number;\n  width: number;\n  height: number;\n  best: Map<number, Label>;\n  /** Areas covered by the content, where a number would read as noise. */\n  avoid: { x0: number; y0: number; x1: number; y1: number }[];\n}\n\ninterface Label {\n  x: number;\n  y: number;\n  angle: number;\n  level: number;\n  /** Squared distance to the slot's centre; the closest candidate wins. */\n  score: number;\n}\n\ninterface Ripple {\n  x: number;\n  y: number;\n  start: number;\n}\n\n/** Seeded 3D gradient noise (Ken Perlin's improved noise), about -1 to 1. */\nfunction createNoise(seed: number) {\n  const p = Array.from({ length: 256 }, (_, i) => i);\n  let state = seed >>> 0 || 1;\n  const random = () => {\n    state = (state * 1_664_525 + 1_013_904_223) >>> 0;\n    return state / 4_294_967_296;\n  };\n  for (let i = 255; i > 0; i--) {\n    const j = Math.floor(random() * (i + 1));\n    [p[i], p[j]] = [p[j], p[i]];\n  }\n  const perm = new Uint8Array(512);\n  for (let i = 0; i < 512; i++) {\n    perm[i] = p[i & 255];\n  }\n  const fade = (t: number) => t * t * t * (t * (t * 6 - 15) + 10);\n  const lerp = (a: number, b: number, t: number) => a + (b - a) * t;\n  const grad = (hash: number, x: number, y: number, z: number) => {\n    const h = hash & 15;\n    const u = h < 8 ? x : y;\n    let v = z;\n    if (h < 4) {\n      v = y;\n    } else if (h === 12 || h === 14) {\n      v = x;\n    }\n    return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);\n  };\n\n  return (x: number, y: number, z: number) => {\n    const X = Math.floor(x) & 255;\n    const Y = Math.floor(y) & 255;\n    const Z = Math.floor(z) & 255;\n    const fx = x - Math.floor(x);\n    const fy = y - Math.floor(y);\n    const fz = z - Math.floor(z);\n    const u = fade(fx);\n    const v = fade(fy);\n    const w = fade(fz);\n    const A = perm[X] + Y;\n    const AA = perm[A] + Z;\n    const AB = perm[A + 1] + Z;\n    const B = perm[X + 1] + Y;\n    const BA = perm[B] + Z;\n    const BB = perm[B + 1] + Z;\n    return lerp(\n      lerp(\n        lerp(grad(perm[AA], fx, fy, fz), grad(perm[BA], fx - 1, fy, fz), u),\n        lerp(\n          grad(perm[AB], fx, fy - 1, fz),\n          grad(perm[BB], fx - 1, fy - 1, fz),\n          u\n        ),\n        v\n      ),\n      lerp(\n        lerp(\n          grad(perm[AA + 1], fx, fy, fz - 1),\n          grad(perm[BA + 1], fx - 1, fy, fz - 1),\n          u\n        ),\n        lerp(\n          grad(perm[AB + 1], fx, fy - 1, fz - 1),\n          grad(perm[BB + 1], fx - 1, fy - 1, fz - 1),\n          u\n        ),\n        v\n      ),\n      w\n    );\n  };\n}\n\n/**\n * The segment a contour draws through one grid cell, given which corners\n * are above the level (bits: top-left 8, top-right 4, bottom-right 2,\n * bottom-left 1) and where it crosses each edge.\n */\nfunction cellSegments(\n  path: Path2D,\n  index: number,\n  box: { x0: number; y0: number; x1: number; y1: number },\n  cross: { top: number; right: number; bottom: number; left: number }\n) {\n  const { x0, y0, x1, y1 } = box;\n  const { top, right, bottom, left } = cross;\n  const line = (ax: number, ay: number, bx: number, by: number) => {\n    path.moveTo(ax, ay);\n    path.lineTo(bx, by);\n  };\n  switch (index) {\n    case 1:\n    case 14:\n      line(x0, left, bottom, y1);\n      break;\n    case 2:\n    case 13:\n      line(bottom, y1, x1, right);\n      break;\n    case 3:\n    case 12:\n      line(x0, left, x1, right);\n      break;\n    case 4:\n    case 11:\n      line(top, y0, x1, right);\n      break;\n    case 6:\n    case 9:\n      line(top, y0, bottom, y1);\n      break;\n    case 7:\n    case 8:\n      line(x0, left, top, y0);\n      break;\n    // The two saddles: opposite corners are high, so the level crosses\n    // the cell twice.\n    case 5:\n      line(x0, left, top, y0);\n      line(bottom, y1, x1, right);\n      break;\n    case 10:\n      line(top, y0, x1, right);\n      line(x0, left, bottom, y1);\n      break;\n    default:\n      break;\n  }\n}\n\n/**\n * Offer a point on an index contour as a label spot. Slots are laid out in\n * terrain space (they drift with the map) and every other row is offset by\n * half a slot, so labels scatter instead of lining up in a grid.\n */\nfunction offerLabel(slots: LabelSlots, label: Omit<Label, \"score\">) {\n  const { x, y } = label;\n  if (\n    y < slots.height * LABEL_MARGIN ||\n    y > slots.height * (1 - LABEL_MARGIN) ||\n    x < 24 ||\n    x > slots.width - 24 ||\n    slots.avoid.some((r) => x > r.x0 && x < r.x1 && y > r.y0 && y < r.y1)\n  ) {\n    return;\n  }\n  const row = Math.floor(y / LABEL_SPACING);\n  const shifted = x + slots.drift + (row % 2) * (LABEL_SPACING / 2);\n  const col = Math.floor(shifted / LABEL_SPACING);\n  const cx = (col + 0.5) * LABEL_SPACING;\n  const cy = (row + 0.5) * LABEL_SPACING;\n  const score = (shifted - cx) ** 2 + (y - cy) ** 2;\n  // Too far from the slot centre: a neighbouring label could sit right\n  // next to it.\n  if (score > (LABEL_SPACING * 0.32) ** 2) {\n    return;\n  }\n  const key = row * 100_000 + col;\n  const current = slots.best.get(key);\n  if (!current || score < current.score) {\n    slots.best.set(key, { ...label, score });\n  }\n}\n\n/** Offer the middle of a cell an index contour passes through. */\nfunction offerCellLabel(\n  slots: LabelSlots,\n  [a, b, c, d]: [number, number, number, number],\n  x0: number,\n  y0: number,\n  level: number,\n  t: number\n) {\n  // The contour runs across the slope; the cell's gradient gives a steadier\n  // direction than the short segment itself.\n  const gx = b - a + c - d;\n  const gy = d - a + c - b;\n  // Slide from the cell's centre along the slope onto the line itself.\n  const px = gx / (2 * CELL);\n  const py = gy / (2 * CELL);\n  const g2 = px * px + py * py;\n  const lift = g2 > 0 ? (t - (a + b + c + d) / 4) / g2 : 0;\n  const half = CELL / 2;\n  const ox = Math.max(-half, Math.min(half, lift * px));\n  const oy = Math.max(-half, Math.min(half, lift * py));\n  let angle = Math.atan2(gx, -gy);\n  // Keep text upright, and skip steep stretches: a number on its side is\n  // hard to read.\n  if (angle > Math.PI / 2) {\n    angle -= Math.PI;\n  } else if (angle < -Math.PI / 2) {\n    angle += Math.PI;\n  }\n  if (Math.abs(angle) > MAX_LABEL_ANGLE) {\n    return;\n  }\n  offerLabel(slots, { x: x0 + half + ox, y: y0 + half + oy, angle, level });\n}\n\n/** Every contour level that passes through one cell, usually none or one. */\nfunction traceCell(\n  field: Float32Array,\n  cols: number,\n  i: number,\n  j: number,\n  interval: number,\n  paths: { minor: Path2D; major: Path2D; slots: LabelSlots | null }\n) {\n  const a = field[j * cols + i];\n  const b = field[j * cols + i + 1];\n  const c = field[(j + 1) * cols + i + 1];\n  const d = field[(j + 1) * cols + i];\n  const x0 = i * CELL;\n  const y0 = j * CELL;\n  const first = Math.ceil(Math.min(a, b, c, d) / interval);\n  const last = Math.floor(Math.max(a, b, c, d) / interval);\n  if (first > last) {\n    return;\n  }\n  const box = { x0, y0, x1: x0 + CELL, y1: y0 + CELL };\n  for (let level = first; level <= last; level++) {\n    const t = level * interval;\n    const index =\n      (a > t ? 8 : 0) | (b > t ? 4 : 0) | (c > t ? 2 : 0) | (d > t ? 1 : 0);\n    if (index === 0 || index === 15) {\n      continue;\n    }\n    const isIndex = level % INDEX_EVERY === 0;\n    cellSegments(isIndex ? paths.major : paths.minor, index, box, {\n      top: x0 + CELL * ((t - a) / (b - a)),\n      right: y0 + CELL * ((t - b) / (c - b)),\n      bottom: x0 + CELL * ((t - d) / (c - d)),\n      left: y0 + CELL * ((t - a) / (d - a)),\n    });\n    // Saddles are poor label spots: the line turns sharply there.\n    if (isIndex && paths.slots && index !== 5 && index !== 10) {\n      offerCellLabel(paths.slots, [a, b, c, d], x0, y0, level, t);\n    }\n  }\n}\n\n/**\n * Marching squares: for each cell, find where each contour level crosses\n * its edges and join those points. Index contours go into their own path\n * so each group is stroked once.\n */\nfunction traceContours(\n  field: Float32Array,\n  cols: number,\n  rows: number,\n  interval: number,\n  minor: Path2D,\n  major: Path2D,\n  slots: LabelSlots | null\n) {\n  const paths = { minor, major, slots };\n  for (let j = 0; j < rows - 1; j++) {\n    for (let i = 0; i < cols - 1; i++) {\n      traceCell(field, cols, i, j, interval, paths);\n    }\n  }\n}\n\ninterface Hill {\n  x: number;\n  y: number;\n  targetX: number;\n  targetY: number;\n  amount: number;\n  on: boolean;\n}\n\n/** Ease the hill towards the cursor and up or down. True while it moves. */\nfunction stepHill(hill: Hill, dt: number) {\n  const follow = 1 - Math.exp(-HILL_FOLLOW * dt);\n  hill.x += (hill.targetX - hill.x) * follow;\n  hill.y += (hill.targetY - hill.y) * follow;\n  const goal = hill.on ? 1 : 0;\n  hill.amount += (goal - hill.amount) * (1 - Math.exp(-HILL_RISE * dt));\n  return (\n    Math.abs(hill.targetX - hill.x) > 0.5 ||\n    Math.abs(hill.targetY - hill.y) > 0.5 ||\n    Math.abs(goal - hill.amount) > 0.002\n  );\n}\n\nexport default function TopoBackground({\n  className,\n  children,\n  interval = 0.07,\n  speed = 1,\n  interactive = true,\n  seed = 26,\n  labels = true,\n}: TopoBackgroundProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n\n  useEffect(() => {\n    const root = rootRef.current;\n    const canvas = canvasRef.current;\n    const ctx = canvas?.getContext(\"2d\");\n    if (!(root && canvas && ctx)) {\n      return;\n    }\n\n    const reduceMotion = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches;\n    const noise = createNoise(seed);\n\n    let width = 0;\n    let height = 0;\n    let cols = 0;\n    let rows = 0;\n    let field = new Float32Array(0);\n    let color = \"\";\n\n    // The cursor hill, eased towards the pointer so it glides, not jumps.\n    const hill: Hill = {\n      x: 0,\n      y: 0,\n      targetX: 0,\n      targetY: 0,\n      amount: 0,\n      on: false,\n    };\n    const ripples: Ripple[] = [];\n    let time = 0;\n    let last = 0;\n    let lastPaint = 0;\n    let raf = 0;\n    let visible = true;\n\n    const resize = () => {\n      const rect = canvas.getBoundingClientRect();\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      width = rect.width;\n      height = rect.height;\n      canvas.width = Math.round(width * dpr);\n      canvas.height = Math.round(height * dpr);\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      cols = Math.ceil(width / CELL) + 1;\n      rows = Math.ceil(height / CELL) + 1;\n      field = new Float32Array(cols * rows);\n      color = getComputedStyle(canvas).color;\n      measureContent();\n    };\n\n    const sample = (now: number) => {\n      const z = time * 0.05;\n      const drift = time * 6;\n      const hillOn = hill.amount > 0.001;\n      const spread = 2 * HILL_SPREAD * HILL_SPREAD;\n      const ringWidth = 2 * RIPPLE_WIDTH * RIPPLE_WIDTH;\n      for (let j = 0; j < rows; j++) {\n        const y = j * CELL;\n        for (let i = 0; i < cols; i++) {\n          const x = i * CELL;\n          const nx = (x + drift) / TERRAIN_SCALE;\n          const ny = y / TERRAIN_SCALE;\n          let v =\n            noise(nx, ny, z) * 0.78 +\n            noise(nx * 2.3 + 17.1, ny * 2.3 + 4.7, z * 1.4) * 0.22;\n          if (hillOn) {\n            const dx = x - hill.x;\n            const dy = y - hill.y;\n            v +=\n              HILL_HEIGHT *\n              hill.amount *\n              Math.exp(-(dx * dx + dy * dy) / spread);\n          }\n          for (const ripple of ripples) {\n            const age = now - ripple.start;\n            const reach = age * RIPPLE_SPEED;\n            const d = Math.hypot(x - ripple.x, y - ripple.y) - reach;\n            const fadeOut = 1 - age / RIPPLE_MS;\n            // A short train of crests and troughs, like a stone dropped in\n            // water, so the contours bunch into rings as it passes.\n            v +=\n              RIPPLE_HEIGHT *\n              fadeOut *\n              fadeOut *\n              Math.exp(-(d * d) / ringWidth) *\n              Math.cos((d / RIPPLE_WAVELENGTH) * Math.PI * 2);\n          }\n          field[j * cols + i] = v;\n        }\n      }\n    };\n\n    const slots: LabelSlots = {\n      drift: 0,\n      width: 0,\n      height: 0,\n      best: new Map(),\n      avoid: [],\n    };\n    // Keep labels out from under the content laid over the map.\n    const measureContent = () => {\n      const origin = canvas.getBoundingClientRect();\n      slots.avoid = [];\n      for (const child of Array.from(root.children)) {\n        if (child.contains(canvas)) {\n          continue;\n        }\n        const r = child.getBoundingClientRect();\n        slots.avoid.push({\n          x0: r.left - origin.left - 16,\n          y0: r.top - origin.top - 12,\n          x1: r.right - origin.left + 16,\n          y1: r.bottom - origin.top + 12,\n        });\n      }\n    };\n\n    /**\n     * Knock the lines out under each label, then print the elevation, so\n     * the contour is broken where the number sits, as on a survey map.\n     */\n    const drawLabels = () => {\n      ctx.font = LABEL_FONT;\n      ctx.textAlign = \"center\";\n      ctx.textBaseline = \"middle\";\n      for (const label of slots.best.values()) {\n        const text = `${LABEL_BASE + label.level * LABEL_STEP}`;\n        const w = ctx.measureText(text).width + 8;\n        ctx.save();\n        ctx.translate(Math.round(label.x), Math.round(label.y));\n        ctx.rotate(label.angle);\n        ctx.globalCompositeOperation = \"destination-out\";\n        ctx.globalAlpha = 1;\n        ctx.fillRect(-w / 2, -6, w, 12);\n        ctx.globalCompositeOperation = \"source-over\";\n        ctx.globalAlpha = 0.5;\n        ctx.fillText(text, 0, 0.5);\n        ctx.restore();\n      }\n    };\n\n    const paint = (now: number) => {\n      sample(now);\n      const minor = new Path2D();\n      const major = new Path2D();\n      slots.best.clear();\n      slots.drift = time * 6;\n      slots.width = width;\n      slots.height = height;\n      traceContours(\n        field,\n        cols,\n        rows,\n        interval,\n        minor,\n        major,\n        labels ? slots : null\n      );\n      ctx.clearRect(0, 0, width, height);\n      ctx.strokeStyle = color;\n      ctx.fillStyle = color;\n      ctx.lineCap = \"round\";\n      ctx.globalAlpha = 0.16;\n      ctx.lineWidth = 0.75;\n      ctx.stroke(minor);\n      ctx.globalAlpha = 0.34;\n      ctx.lineWidth = 1.1;\n      ctx.stroke(major);\n      if (labels) {\n        drawLabels();\n      }\n      ctx.globalAlpha = 1;\n    };\n\n    const frame = (now: number) => {\n      raf = 0;\n      if (!visible) {\n        return;\n      }\n      const dt = last ? Math.min((now - last) / 1000, 0.05) : 0;\n      last = now;\n      const hillMoving = stepHill(hill, dt);\n      while (ripples.length > 0 && now - ripples[0].start > RIPPLE_MS) {\n        ripples.shift();\n      }\n      const settling = hillMoving || ripples.length > 0;\n\n      time += dt * speed;\n      // The drift alone is slow; half the frame rate looks the same and\n      // costs half as much. Anything following the hand gets every frame.\n      if (settling || now - lastPaint >= IDLE_FRAME_MS) {\n        paint(now);\n        lastPaint = now;\n      }\n      if (speed > 0 || settling) {\n        raf = requestAnimationFrame(frame);\n      }\n    };\n\n    const start = () => {\n      if (!raf && visible && !reduceMotion) {\n        last = 0;\n        raf = requestAnimationFrame(frame);\n      }\n    };\n\n    // Follow light and dark mode without asking the app for a theme. The\n    // check only runs while the map is on screen.\n    let colorTimer = 0;\n    const checkColor = () => {\n      const next = getComputedStyle(canvas).color;\n      if (next !== color) {\n        color = next;\n        if (reduceMotion || !raf) {\n          paint(performance.now());\n        }\n      }\n    };\n    const watchColor = () => {\n      if (visible && !colorTimer) {\n        checkColor();\n        colorTimer = window.setInterval(checkColor, 500);\n      } else if (!visible && colorTimer) {\n        window.clearInterval(colorTimer);\n        colorTimer = 0;\n      }\n    };\n\n    resize();\n    paint(performance.now());\n    canvas.style.opacity = \"1\";\n    start();\n\n    const resizeObserver = new ResizeObserver(() => {\n      resize();\n      paint(performance.now());\n    });\n    resizeObserver.observe(canvas);\n\n    // Stop drawing entirely while the map is scrolled away or the tab is\n    // in the background.\n    let onScreen = true;\n    const updateVisible = () => {\n      visible = onScreen && document.visibilityState === \"visible\";\n      watchColor();\n      if (visible) {\n        start();\n      }\n    };\n    const intersection = new IntersectionObserver(([entry]) => {\n      onScreen = entry.isIntersecting;\n      updateVisible();\n    });\n    intersection.observe(root);\n    document.addEventListener(\"visibilitychange\", updateVisible);\n    watchColor();\n\n    const local = (event: PointerEvent) => {\n      const rect = canvas.getBoundingClientRect();\n      return { x: event.clientX - rect.left, y: event.clientY - rect.top };\n    };\n    const onMove = (event: PointerEvent) => {\n      // Touch has no hover; a hill stuck where a finger lifted looks wrong.\n      if (event.pointerType !== \"mouse\") {\n        return;\n      }\n      const point = local(event);\n      if (!hill.on) {\n        // Rise where the cursor came in rather than sliding over from the\n        // last place it left.\n        hill.x = point.x;\n        hill.y = point.y;\n      }\n      hill.targetX = point.x;\n      hill.targetY = point.y;\n      hill.on = true;\n      start();\n    };\n    const onLeave = () => {\n      hill.on = false;\n      start();\n    };\n    const onDown = (event: PointerEvent) => {\n      // Clicks on the content (links, buttons) are not meant for the map.\n      if ((event.target as HTMLElement).closest(\"a, button, input, textarea\")) {\n        return;\n      }\n      const point = local(event);\n      ripples.push({ ...point, start: performance.now() });\n      if (ripples.length > 4) {\n        ripples.shift();\n      }\n      start();\n    };\n    if (interactive && !reduceMotion) {\n      root.addEventListener(\"pointermove\", onMove);\n      root.addEventListener(\"pointerleave\", onLeave);\n      root.addEventListener(\"pointerdown\", onDown);\n    }\n\n    return () => {\n      cancelAnimationFrame(raf);\n      resizeObserver.disconnect();\n      intersection.disconnect();\n      document.removeEventListener(\"visibilitychange\", updateVisible);\n      window.clearInterval(colorTimer);\n      root.removeEventListener(\"pointermove\", onMove);\n      root.removeEventListener(\"pointerleave\", onLeave);\n      root.removeEventListener(\"pointerdown\", onDown);\n    };\n  }, [interval, speed, interactive, seed, labels]);\n\n  return (\n    <div\n      className={cn(\n        \"relative flex min-h-[560px] w-full items-center justify-center overflow-hidden bg-white text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100\",\n        className\n      )}\n      ref={rootRef}\n    >\n      {/* Faded out top and bottom so the map sits into the page around it. */}\n      <div aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0\">\n        <canvas\n          className=\"h-full w-full opacity-0 transition-opacity duration-500 ease-out\"\n          ref={canvasRef}\n          style={{\n            maskImage:\n              \"linear-gradient(to bottom, transparent, #000 18%, #000 82%, transparent)\",\n          }}\n        />\n      </div>\n      {children === undefined ? <DefaultContent /> : children}\n    </div>\n  );\n}\n\nfunction DefaultContent() {\n  return (\n    <div className=\"relative flex max-w-xl flex-col items-center gap-4 px-6 text-center\">\n      {/* A plain paragraph: the page that uses this decides its own h1. */}\n      <p className=\"text-balance font-semibold text-4xl text-zinc-900 tracking-tight sm:text-5xl dark:text-zinc-100\">\n        Find your footing\n      </p>\n      <p className=\"max-w-sm text-base text-zinc-500 dark:text-zinc-400\">\n        A living contour map. Move the cursor to raise a hill, click to send a\n        ripple across it.\n      </p>\n    </div>\n  );\n}\n",
      "path": "/components/kokonutui/topo-background.tsx",
      "target": "components/kokonutui/topo-background.tsx"
    }
  ]
}