{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "groundwork-background",
  "title": "Groundwork Background",
  "description": "Seeded architectural SVG overlays: evolving survey terrain, terraced cut-and-fill, a 3D survey mesh and a connected material-exchange network. Anchored by default, optional bobbing, extended fields, camera tours and reduced-motion fallbacks. Explore /patterns/living-background.",
  "registryDependencies": [
    "@nx-ui/use-background-clock",
    "@nx-ui/utils"
  ],
  "files": [
    {
      "path": "components/blocks/groundwork-background/groundwork-background.tsx",
      "content": "\"use client\"\n\nimport { useId, useMemo, type CSSProperties } from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { useBackgroundClock } from \"@/hooks/use-background-clock\"\nimport { clamp, contours, createTerrain, heightAt, polygon, projector, randomAt, xy, siteLinks, smoothContours, type Point, type Terrain } from \"./terrain\"\n\nexport type GroundworkVariant = \"survey\" | \"cut-fill\" | \"mesh\" | \"exchange\"\n\nexport interface GroundworkBackgroundProps {\n  /** survey: natural contours; cut-fill: terraced excavation; mesh: ground scan; exchange: connected sites. */\n  variant?: GroundworkVariant\n  /** Reproducible terrain or site layout. Integer, 1–9999. */\n  seed?: number\n  /** Contribution of fine terrain detail, 0–1. */\n  ruggedness?: number\n  /** Contour intervals, mesh resolution or number of sites; 6–24. */\n  density?: number\n  /** Vertical exaggeration of the 3D terrain, 0–260. */\n  relief?: number\n  /** Orthographic camera bearing in degrees, 15–75. */\n  bearing?: number\n  /** Number of stepped elevation bands in cut-and-fill, 3–16. */\n  terraces?: number\n  /** Design elevation: ground above this level is cut and hatched, 0.2–1. */\n  datum?: number\n  /** Nearest-neighbour links per site, 1–4; a spanning tree keeps all sites connected. */\n  connections?: number\n  /** Material-exchange site glyph size, 0.5–2; independent of field extent. */\n  siteSize?: number\n  /** False renders the same geometry without motion. */\n  animated?: boolean\n  /** Freeze the current frame without losing animation progress. */\n  paused?: boolean\n  /** Cycle duration in seconds, 2–120. */\n  duration?: number\n  /** Playback multiplier, 0.25–6; cycles never run faster than two seconds. */\n  speed?: number\n  /** Drift distance, 0–120; independent from scans and terrain evolution. */\n  amplitude?: number\n  /** Optional vertical bobbing for the 3D studies; off by default. */\n  floating?: boolean\n  /** Survey evolution changes the underlying noise field. Default: evolve. */\n  surveyMotion?: \"evolve\" | \"drift\" | \"still\"\n  /** Noise-field excursion during an evolution cycle, 0–2. */\n  evolutionAmount?: number\n  /** Terrain feature frequency, 1–8. */\n  terrainFrequency?: number\n  /** Ridge/valley distortion, 0–2. */\n  terrainWarp?: number\n  /** Model on a finite plate, or an extended field cropped to the viewport. */\n  extent?: \"tile\" | \"field\"\n  /** Line weight multiplier, 0.4–2.5; preserves major/minor line hierarchy. */\n  lineWeight?: number\n  /** Independent scan/signal speed multiplier, 0.25–4. */\n  effectSpeed?: number\n  /** Composition zoom, clamped to 0.75–2. */\n  scale?: number\n  /** Overall line and face opacity, clamped to 0–1. */\n  opacity?: number\n  /** Any CSS colour; defaults to the container's text colour. */\n  color?: string\n  /** Scan, excavation hatch and material signal colour. Defaults to currentColor. */\n  accentColor?: string\n  /** Travelling survey line or mesh scan highlights. */\n  showScan?: boolean\n  /** Reference grid and base plate. */\n  showGrid?: boolean\n  /** Survey markers, elevation probes and site beacons. */\n  showMarkers?: boolean\n  /** Filled terrain faces; false leaves a wireframe. */\n  showShading?: boolean\n  /** Excavation footprint hatching. */\n  showHatching?: boolean\n  /** Dashed design elevation plane. */\n  showDatum?: boolean\n  /** Travelling material signals. */\n  showSignals?: boolean\n  /** Site-to-site route lines. */\n  showRoutes?: boolean\n  className?: string\n}\n\nconst styles = `\n[data-groundwork] .nx-ground-motion { transition: none; }\n${[\".65\", \".8\", \"0.8\", \"1\", \"1.5\", \"1.7\", \"2.5\", \"36\"].map(weight => `[data-groundwork][stroke-width=\"${weight}\"], [data-groundwork] [stroke-width=\"${weight}\"] { stroke-width: calc(${Number(weight)}px * var(--nx-ground-weight)); }`).join(\"\\n\")}\n@keyframes nx-ground-float {\n  0%,100% { transform: translateY(0); }\n  50% { transform: translateY(calc(var(--nx-ground-travel) * -.5)); }\n}\n@keyframes nx-ground-scan {\n  0% { transform: translateX(-140px); opacity: 0; }\n  8%,92% { opacity: .75; }\n  100% { transform: translateX(1340px); opacity: 0; }\n}\n@keyframes nx-ground-row {\n  0%,28%,100% { opacity: .08; }\n  12% { opacity: .9; }\n}\n@keyframes nx-ground-signal { to { stroke-dashoffset: -100; } }\n@keyframes nx-ground-beacon { 0%,100% { opacity: .12; } 50% { opacity: .65; } }\n[data-groundwork] .nx-ground-scanner { opacity: 0; }\n${[\"scan\", \"grid\", \"markers\", \"hatching\", \"datum\", \"signals\", \"routes\"].map(layer => `[data-groundwork][data-show-${layer}=\"false\"] [data-groundwork-layer=\"${layer}\"] { display: none; }`).join(\"\\n\")}\n[data-groundwork][data-show-shading=\"false\"] [data-groundwork-shading] { fill-opacity: 0; }\n@media (prefers-reduced-motion: no-preference) {\n  [data-groundwork][data-animated=\"true\"] .nx-ground-motion {\n    animation: var(--nx-ground-effect, nx-ground-float) var(--nx-ground-duration) var(--nx-ground-easing, ease-in-out) infinite;\n    animation-delay: var(--nx-ground-delay, 0s);\n    animation-play-state: var(--nx-ground-play);\n  }\n  [data-groundwork] .nx-ground-bob { animation-duration: var(--nx-ground-drift-duration); }\n  [data-groundwork][data-floating=\"false\"] .nx-ground-bob { animation: none; }\n}\n`\n\nconst required = <T,>(items: readonly T[], index: number): T => {\n  const item = items[index]\n  if (item === undefined) throw new RangeError(`Missing generated geometry at ${index}`)\n  return item\n}\n\ntype Projection = ReturnType<typeof projector>\nconst linePath = (segments: [Point, Point][], project: (p: Point) => Point) =>\n  segments.map(([a, b]) => `M${xy(project(a))}L${xy(project(b))}`).join(\" \")\n\nfunction Survey({ terrain, count, accent }: { terrain: Terrain; count: number; accent: string }) {\n  const lines = useMemo(() => Array.from({ length: count }, (_, i) => ({\n    level: (i + 1) / (count + 1),\n    d: smoothContours(contours(terrain, (i + 1) / (count + 1)), ([x, y]) => [-100 + x * 1400, -100 + y * 920]),\n  })), [terrain, count])\n  return <>\n    <g data-groundwork-layer=\"grid\" strokeOpacity=\".18\" strokeDasharray=\"2 6\">\n      {Array.from({ length: 11 }, (_, i) => <path key={i} d={`M${i * 120} -100V820 M-100 ${i * 100}H1300`} />)}\n    </g>\n    <g className=\"nx-ground-motion nx-ground-bob\">\n      {lines.map((line, i) => <path key={i} d={line.d} strokeWidth={i % 4 === 0 ? 1.7 : .8} strokeOpacity={i % 4 === 0 ? .95 : .5} strokeLinecap=\"round\" />)}\n      {([ [210, 180], [1020, 230], [750, 560], [320, 610] ] as const).map(([x, y], i) => <g data-groundwork-layer=\"markers\" key={i} opacity=\".7\">\n        <path d={`M${x - 7} ${y}h14 M${x} ${y - 7}v14`} />\n        <circle cx={x} cy={y} r=\"3\" />\n        <text x={x + 14} y={y + 4} fill=\"currentColor\" stroke=\"none\" fontSize=\"10\" fontFamily=\"monospace\">{(heightAt(terrain, x / 1200, y / 720) * 80 + 100).toFixed(1)}</text>\n      </g>)}\n    </g>\n    <g data-groundwork-layer=\"scan\" className=\"nx-ground-motion nx-ground-scanner\" stroke={accent} style={{ \"--nx-ground-effect\": \"nx-ground-scan\", \"--nx-ground-easing\": \"linear\" } as CSSProperties}>\n      <path d=\"M0 -100V820\" strokeWidth=\"1.5\" />\n      <path d=\"M-18 -100V820\" strokeWidth=\"36\" strokeOpacity=\".06\" />\n    </g>\n  </>\n}\n\nfunction BasePlate({ project }: { project: Projection }) {\n  return <g data-groundwork-layer=\"grid\" strokeOpacity=\".24\">\n    <path d={polygon(([[0, 0], [1, 0], [1, 1], [0, 1]] as const).map(([x, y]) => project(x, y, -.18)))} />\n    {Array.from({ length: 11 }, (_, i) => <path key={i} d={`M${xy(project(i / 10, 0, -.18))}L${xy(project(i / 10, 1, -.18))} M${xy(project(0, i / 10, -.18))}L${xy(project(1, i / 10, -.18))}`} strokeOpacity=\".28\" />)}\n    {([[0, 0], [1, 0], [1, 1], [0, 1]] as const).map(([x, y], i) => <path key={i} d={`M${xy(project(x, y, -.18))}L${xy(project(x, y, .2))}`} strokeDasharray=\"2 5\" />)}\n  </g>\n}\n\nfunction CutFill({ terrain, project, steps, datum, hatch, accent }: { terrain: Terrain; project: Projection; steps: number; datum: number; hatch: string; accent: string }) {\n  const cells = useMemo(() => {\n    const n = terrain.size\n    const h = (x: number, y: number) => x >= n || y >= n ? -.18 : Math.min(Math.round(heightAt(terrain, (x + .5) / n, (y + .5) / n) * steps) / steps, datum)\n    const result = []\n    for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) {\n      const z = h(x, y), a = x / n, b = y / n, c = (x + 1) / n, d = (y + 1) / n\n      const top = polygon([project(a, b, z), project(c, b, z), project(c, d, z), project(a, d, z)])\n      const right = z > h(x + 1, y) ? polygon([project(c, b, z), project(c, d, z), project(c, d, h(x + 1, y)), project(c, b, h(x + 1, y))]) : undefined\n      const front = z > h(x, y + 1) ? polygon([project(a, d, z), project(c, d, z), project(c, d, h(x, y + 1)), project(a, d, h(x, y + 1))]) : undefined\n      result.push({ top, right, front, z, cut: Math.round(heightAt(terrain, (x + .5) / n, (y + .5) / n) * steps) / steps > datum, depth: project(a, b, 0)[1] })\n    }\n    return result.sort((a, b) => a.depth - b.depth)\n  }, [terrain, steps, datum, project])\n  return <g className=\"nx-ground-motion nx-ground-bob\">\n    <BasePlate project={project} />\n    {cells.map((c, i) => <g key={i} strokeWidth=\".65\" strokeOpacity=\".6\">\n      {c.right && <path data-groundwork-shading d={c.right} fill=\"currentColor\" fillOpacity=\".08\" />}\n      {c.front && <path data-groundwork-shading d={c.front} fill=\"currentColor\" fillOpacity=\".2\" />}\n      <path data-groundwork-shading d={c.top} fill=\"currentColor\" fillOpacity={.05 + c.z * .12} strokeOpacity=\".3\" />\n      {c.cut && <path data-groundwork-layer=\"hatching\" d={c.top} fill={`url(#${hatch})`} stroke=\"none\" />}\n    </g>)}\n    <path data-groundwork-layer=\"datum\" d={polygon(([[0, 0], [1, 0], [1, 1], [0, 1]] as const).map(([x, y]) => project(x, y, datum)))} stroke={accent} strokeOpacity=\".8\" strokeDasharray=\"5 7\" />\n    <path data-groundwork-layer=\"datum\" d={`M${xy(project(1, 1, -.18))}L${xy(project(1, 1, datum))}`} stroke={accent} />\n  </g>\n}\n\nfunction Mesh({ terrain, project, accent, duration }: { terrain: Terrain; project: Projection; accent: string; duration: number }) {\n  const geometry = useMemo(() => {\n    const n = terrain.size, triangles: { d: string; shade: number; depth: number }[] = [], rows: string[] = []\n    const p = (x: number, y: number) => project(x / n, y / n, required(required(terrain.heights, y), x))\n    for (let y = 0; y <= n; y++) rows.push(`M${Array.from({ length: n + 1 }, (_, x) => xy(p(x, y))).join(\" L\")}`)\n    for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) {\n      const points: [Point, Point, Point, Point] = [p(x, y), p(x + 1, y), p(x + 1, y + 1), p(x, y + 1)]\n      const slope = required(required(terrain.heights, y), x + 1) - required(required(terrain.heights, y + 1), x)\n      for (const indices of [[0, 1, 2], [0, 2, 3]]) triangles.push({ d: polygon(indices.map(i => required(points, i))), shade: Number(clamp(.09 + slope * 1.4, .015, .32, .1).toFixed(4)), depth: project(x / n, y / n, 0)[1] })\n    }\n    return { triangles: triangles.sort((a, b) => a.depth - b.depth), rows }\n  }, [terrain, project])\n  return <g className=\"nx-ground-motion nx-ground-bob\">\n    <BasePlate project={project} />\n    {geometry.triangles.map((t, i) => <path data-groundwork-shading key={i} d={t.d} fill=\"currentColor\" fillOpacity={t.shade} strokeWidth=\".65\" strokeOpacity=\".48\" />)}\n    {geometry.rows.map((d, i) => <path data-groundwork-layer=\"scan\" key={i} d={d} stroke={accent} strokeWidth=\"1.5\" opacity=\".25\" className=\"nx-ground-motion\" style={{ \"--nx-ground-effect\": \"nx-ground-row\", \"--nx-ground-delay\": `${-duration + i * duration * .65 / geometry.rows.length}s` } as CSSProperties} />)}\n    {[0, .25, .5, .75, 1].map((x, i) => <g data-groundwork-layer=\"markers\" key={i} opacity=\".7\">\n      <circle cx={project(x, .75, heightAt(terrain, x, .75))[0]} cy={project(x, .75, heightAt(terrain, x, .75))[1]} r=\"2.5\" fill={accent} stroke=\"none\" />\n      <path d={`M${xy(project(x, .75, -.18))}L${xy(project(x, .75, heightAt(terrain, x, .75)))}`} strokeDasharray=\"2 5\" strokeOpacity=\".5\" />\n    </g>)}\n  </g>\n}\n\nfunction Exchange({ terrain, project, count, seed, connections, accent, duration, siteSize }: { terrain: Terrain; project: Projection; count: number; seed: number; connections: number; accent: string; duration: number; siteSize: number }) {\n  const geometry = useMemo(() => {\n    const columns = Math.ceil(Math.sqrt(count * 1.2)), rows = Math.ceil(count / columns)\n    const sites: Point[] = Array.from({ length: count }, (_, i) => [\n      .1 + ((i % columns) + .2 + randomAt(i, 1, seed) * .6) / columns * .8,\n      .1 + (Math.floor(i / columns) + .2 + randomAt(i, 2, seed) * .6) / rows * .8,\n    ])\n    const centres = sites.map(([x, y], i) => project(x, y, .26 + randomAt(i, 3, seed) * .25))\n    const links = siteLinks(sites, connections).map(([a, b]) => {\n      const start = required(centres, a), end = required(centres, b), lift = Math.hypot(end[0] - start[0], end[1] - start[1]) * .18 + 24\n      return `M${xy(start)} C${start[0]},${start[1] - lift} ${end[0]},${end[1] - lift} ${xy(end)}`\n    })\n    return { sites, links, contours: [ .2, .35, .5, .65, .8 ].map(level => linePath(contours(terrain, level), ([x, y]) => project(x, y, -.18))) }\n  }, [terrain, project, count, seed, connections])\n  return <g className=\"nx-ground-motion nx-ground-bob\">\n    <BasePlate project={project} />\n    <g strokeOpacity=\".14\">{geometry.contours.map((d, i) => <path key={i} d={d} />)}</g>\n    {geometry.links.map((d, i) => <g key={i}>\n      <path data-groundwork-layer=\"routes\" d={d} strokeOpacity=\".32\" strokeWidth=\"1\" />\n      <path data-groundwork-layer=\"signals\" d={d} pathLength=\"100\" stroke={accent} strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeDasharray=\"2 98\" opacity=\".85\" className=\"nx-ground-motion\" style={{ \"--nx-ground-effect\": \"nx-ground-signal\", \"--nx-ground-easing\": \"linear\", \"--nx-ground-delay\": `${-i * duration / 7}s` } as CSSProperties} />\n    </g>)}\n    {geometry.sites.map(([x, y], i) => {\n      const z = .26 + randomAt(i, 3, seed) * .25, width = (.021 + randomAt(i, 4, seed) * .016) * siteSize\n      const plate = (height: number, size = width) => polygon(([[x - size, y - size], [x + size, y - size], [x + size, y + size], [x - size, y + size]] as const).map(([u, v]) => project(u, v, height)))\n      return <g key={i}>\n        <path data-groundwork-layer=\"markers\" d={plate(-.18, width * 2)} stroke={accent} opacity=\".25\" className=\"nx-ground-motion\" style={{ \"--nx-ground-effect\": \"nx-ground-beacon\", \"--nx-ground-delay\": `${-i * duration / count}s` } as CSSProperties} />\n        <path d={`M${xy(project(x, y, -.18))}L${xy(project(x, y, z))}`} strokeOpacity=\".3\" strokeDasharray=\"2 4\" />\n        {[0, 1, 2].map(layer => <path key={layer} d={plate(z - layer * .07)} fill=\"currentColor\" fillOpacity={layer ? .06 : .2} strokeOpacity={layer ? .35 : .9} />)}\n        <circle cx={project(x, y, z)[0]} cy={project(x, y, z)[1]} r=\"2.5\" fill={accent} stroke=\"none\" />\n      </g>\n    })}\n  </g>\n}\n\n/** Procedural architectural SVG overlays. Deterministic seeds, no network or\n * animation dependency. Survey evolution changes the height field; ambient effects\n * use CSS and useGroundworkMotion adds camera tours. Provide a pause control. */\nexport function GroundworkBackground({\n  variant = \"survey\", seed = 42, ruggedness = .55, density = 16, relief = 180,\n  bearing = 45, terraces = 7, datum = .62, connections = 2, animated = true,\n  paused = false, duration = 48, speed = 1, amplitude = 14, scale = 1, opacity = .4,\n  color = \"currentColor\", accentColor = \"currentColor\", className,\n  showScan = true, showGrid = true, showMarkers = true, showShading = true,\n  showHatching = true, showDatum = true, showSignals = true, showRoutes = true,\n  floating = false, surveyMotion = \"evolve\", evolutionAmount = .65,\n  terrainFrequency = 3.6, terrainWarp = .85, extent = \"tile\", lineWeight = 1, effectSpeed = 1,\n  siteSize = 1,\n}: GroundworkBackgroundProps) {\n  const id = useId().replace(/:/g, \"\")\n  const stableSeed = Math.round(clamp(seed, 1, 9999, 42))\n  const count = Math.round(clamp(density, 6, 24, 16))\n  const rough = clamp(ruggedness, 0, 1, .55)\n  const seconds = Math.max(2, clamp(duration, 2, 120, 48) / clamp(speed, .25, 6, 1))\n  const clock = useBackgroundClock({ playing: variant === \"survey\" && surveyMotion === \"evolve\" && animated && !paused && evolutionAmount > 0,\n    rate: 1 / seconds, end: 1, fps: 15, resetKey: String(stableSeed) })\n  const phase = animated && !clock.reducedMotion && surveyMotion === \"evolve\" ? clock.time * Math.PI * 2 : 0\n  const frequency = clamp(terrainFrequency, 1, 8, 3.6), warp = clamp(terrainWarp, 0, 2, .85)\n  const evolve = variant === \"survey\" ? clamp(evolutionAmount, 0, 2, .65) : 0\n  const terrain = useMemo(() => createTerrain(variant === \"survey\" ? 64 : variant === \"exchange\" ? 32 : count + 4, stableSeed, rough, frequency, warp, phase, evolve), [variant, count, stableSeed, rough, frequency, warp, phase, evolve])\n  const camera = clamp(bearing, 15, 75, 45), height = clamp(relief, 0, 260, 180)\n  const project = useMemo(() => {\n    const projection = projector(camera, height)\n    return extent === \"field\" ? (x: number, y: number, z: number) => projection((x - .5) * 3.4 + .5, (y - .5) * 3.4 + .5, z) : projection\n  }, [camera, height, extent])\n  const zoom = clamp(scale, .75, 2, 1), effectDuration = seconds / clamp(effectSpeed, .25, 4, 1)\n  const travel = clamp(amplitude, 0, 120, 14)\n  return <div ref={clock.viewportRef} className=\"pointer-events-none absolute inset-0\" data-survey-phase={phase.toFixed(4)}><svg aria-hidden=\"true\" focusable=\"false\" data-groundwork={variant} data-animated={animated}\n    data-floating={variant === \"survey\" ? surveyMotion === \"drift\" : floating} data-extent={extent}\n    data-show-scan={showScan} data-show-grid={showGrid} data-show-markers={showMarkers}\n    data-show-shading={showShading} data-show-hatching={showHatching} data-show-datum={showDatum}\n    data-show-signals={showSignals} data-show-routes={showRoutes}\n    className={cn(\"pointer-events-none absolute inset-0 size-full overflow-hidden\", className)}\n    viewBox={`${600 - 600 / zoom} ${360 - 360 / zoom} ${1200 / zoom} ${720 / zoom}`}\n    preserveAspectRatio={variant === \"survey\" || extent === \"field\" ? \"xMidYMid slice\" : \"xMidYMid meet\"}\n    fill=\"none\" stroke={color} strokeWidth=\"1\" style={{ color, opacity: clamp(opacity, 0, 1, .4),\n      \"--nx-ground-duration\": `${effectDuration}s`, \"--nx-ground-travel\": `${travel}px`,\n      \"--nx-ground-drift-duration\": `${seconds}s`,\n      \"--nx-ground-weight\": clamp(lineWeight, .4, 2.5, 1),\n      \"--nx-ground-play\": paused ? \"paused\" : \"running\",\n    } as CSSProperties}>\n    <style>{styles}</style>\n    <defs><pattern id={`${id}-hatch`} width=\"7\" height=\"7\" patternUnits=\"userSpaceOnUse\" patternTransform=\"rotate(30)\"><path d=\"M0 0V7\" stroke={accentColor} strokeWidth=\"1\" opacity=\".85\" /></pattern></defs>\n    {variant === \"survey\" && <Survey terrain={terrain} count={count} accent={accentColor} />}\n    {variant === \"cut-fill\" && <CutFill terrain={terrain} project={project} steps={Math.round(clamp(terraces, 3, 16, 7))} datum={clamp(datum, .2, 1, .62)} hatch={`${id}-hatch`} accent={accentColor} />}\n    {variant === \"mesh\" && <Mesh terrain={terrain} project={project} accent={accentColor} duration={effectDuration} />}\n    {variant === \"exchange\" && <Exchange terrain={terrain} project={project} count={extent === \"field\" ? count * 3 : count} seed={stableSeed} connections={Math.round(clamp(connections, 1, 4, 2))} accent={accentColor} duration={effectDuration} siteSize={clamp(siteSize, .5, 2, 1) / (extent === \"field\" ? 3.4 : 1)} />}\n  </svg></div>\n}\n",
      "type": "registry:component",
      "target": "components/blocks/groundwork-background/groundwork-background.tsx"
    },
    {
      "path": "components/blocks/groundwork-background/motion-presets.ts",
      "content": "import { clamp } from \"./terrain\"\n\nexport type GroundworkMotionMode = \"ambient\" | \"orbit\" | \"emerge\" | \"excavate\" | \"hover\" | \"drag\"\nexport interface GroundworkPose {\n  bearing: number\n  relief: number\n  ruggedness: number\n  scale: number\n  datum: number\n}\n\nexport function constrainPose(pose: GroundworkPose): GroundworkPose {\n  return {\n    bearing: clamp(pose.bearing, 15, 75, 45), relief: clamp(pose.relief, 0, 260, 180),\n    ruggedness: clamp(pose.ruggedness, 0, 1, .55), scale: clamp(pose.scale, .75, 2, 1), datum: clamp(pose.datum, .2, 1, .62),\n  }\n}\n\nexport function blendPose(from: GroundworkPose, to: GroundworkPose, amount: number): GroundworkPose {\n  const result = { ...from }\n  for (const key of Object.keys(from) as (keyof GroundworkPose)[]) result[key] += (to[key] - from[key]) * amount\n  return constrainPose(result)\n}\n\n/** Closed periodic curves, with matching endpoint derivatives: no jump at the loop seam.\n * This orthographic model sweeps a 60-degree arc rather than showing hidden back faces. */\nexport function sampleGroundworkTour(mode: GroundworkMotionMode, progress: number, base: GroundworkPose, strength = 1): GroundworkPose {\n  const phase = ((progress % 1) + 1) % 1 * Math.PI * 2\n  const rise = (1 - Math.cos(phase)) / 2, swing = Math.sin(phase)\n  let target = { ...base }\n  if (mode === \"orbit\") target = { ...base, bearing: base.bearing + 28 * swing, scale: base.scale * (1 + .18 * rise), relief: base.relief * (1 + .18 * swing) }\n  if (mode === \"emerge\") target = {\n    ...base, bearing: 32 + 28 * rise, relief: base.relief * rise,\n    ruggedness: .02 + (Math.max(.2, base.ruggedness) - .02) * rise,\n    scale: base.scale * (.9 + .23 * rise),\n  }\n  if (mode === \"excavate\") target = { ...base, bearing: base.bearing + 24 * swing, datum: .98 - .65 * rise, scale: base.scale * (.94 + .18 * rise) }\n  return blendPose(base, target, clamp(strength, 0, 2, 1))\n}\n\nexport function pointerGroundworkPose(base: GroundworkPose, x: number, y: number, strength: number): GroundworkPose {\n  const amount = clamp(strength, 0, 2, 1)\n  return constrainPose({ ...base, bearing: base.bearing + clamp(x, -1, 1, 0) * 30 * amount,\n    relief: base.relief - clamp(y, -1, 1, 0) * 100 * amount,\n    scale: base.scale - clamp(y, -1, 1, 0) * .15 * amount })\n}\n",
      "type": "registry:component",
      "target": "components/blocks/groundwork-background/motion-presets.ts"
    },
    {
      "path": "components/blocks/groundwork-background/terrain.ts",
      "content": "/** Deterministic synthetic terrain, contour extraction and orthographic projection.\n * Kept independent of React so geometry can be tested and computed only on changes. */\nexport type Point = [number, number]\nexport type Terrain = { size: number; heights: number[][] }\n\n/** A terrain grid and its generated topology have known bounds after construction. */\nconst required = <T,>(items: readonly T[], index: number): T => {\n  const item = items[index]\n  if (item === undefined) throw new RangeError(`Missing terrain element at ${index}`)\n  return item\n}\n\nexport const clamp = (n: number, min: number, max: number, fallback: number) =>\n  Number.isFinite(n) ? Math.max(min, Math.min(max, n)) : fallback\n\nexport function randomAt(x: number, y: number, seed: number) {\n  let n = Math.imul(x, 374761393) ^ Math.imul(y, 668265263) ^ Math.imul(seed, 1442695041)\n  n = Math.imul(n ^ (n >>> 13), 1274126177)\n  return ((n ^ (n >>> 16)) >>> 0) / 4294967295\n}\n\nconst smooth = (n: number) => n * n * n * (n * (n * 6 - 15) + 10)\nconst lerp = (a: number, b: number, t: number) => a + (b - a) * t\n\nfunction noise(x: number, y: number, seed: number) {\n  const ix = Math.floor(x), iy = Math.floor(y)\n  const u = smooth(x - ix), v = smooth(y - iy)\n  return lerp(\n    lerp(randomAt(ix, iy, seed), randomAt(ix + 1, iy, seed), u),\n    lerp(randomAt(ix, iy + 1, seed), randomAt(ix + 1, iy + 1, seed), u), v,\n  )\n}\n\nexport function createTerrain(size: number, seed: number, ruggedness: number, frequency = 3.6, warp = .85, phase = 0, evolution = 0): Terrain {\n  let lo = Infinity, hi = -Infinity\n  const heights = Array.from({ length: size + 1 }, (_, y) =>\n    Array.from({ length: size + 1 }, (_, x) => {\n      // A closed orbit through the continuous noise field changes the terrain\n      // itself, not the position of the finished drawing. Phase zero is the base.\n      const u = x / size * frequency + Math.sin(phase) * evolution\n      const v = y / size * frequency + (Math.cos(phase) - 1) * evolution\n      // Domain warping bends ridges and valleys before the finer octaves are added.\n      const wx = u + warp * noise(u * .7, v * .7, seed + 19)\n      const wy = v + warp * noise(u * .7, v * .7, seed + 47)\n      let value = 0, weight = 1, total = 0\n      for (let octave = 0; octave < 5; octave++) {\n        const frequency = 2 ** octave\n        value += noise(wx * frequency, wy * frequency, seed + octave * 73) * weight\n        total += weight\n        weight *= .24 + ruggedness * .4\n      }\n      value = value / total + .13 * Math.sin(u * 1.7 + v * .8)\n      lo = Math.min(lo, value); hi = Math.max(hi, value)\n      return value\n    }),\n  )\n  // JS engines can disagree in the last bits of transcendental calculations.\n  // Quantise the sampled field before it reaches markup or topology decisions.\n  return { size, heights: heights.map(row => row.map(h => Number(((h - lo) / (hi - lo || 1)).toFixed(8)))) }\n}\n\nexport function heightAt(terrain: Terrain, x: number, y: number) {\n  const u = clamp(x, 0, 1, .5) * terrain.size\n  const v = clamp(y, 0, 1, .5) * terrain.size\n  const ix = Math.min(terrain.size - 1, Math.floor(u))\n  const iy = Math.min(terrain.size - 1, Math.floor(v))\n  const h = terrain.heights\n  return lerp(lerp(required(required(h, iy), ix), required(required(h, iy), ix + 1), u - ix), lerp(required(required(h, iy + 1), ix), required(required(h, iy + 1), ix + 1), u - ix), v - iy)\n}\n\n/** Marching squares, with a bilinear asymptotic decider for saddle cells.\n * Every edge is interpolated from shared samples, so adjoining cells meet. */\nexport function contours(terrain: Terrain, level: number): [Point, Point][] {\n  const { size, heights } = terrain\n  const segments: [Point, Point][] = []\n  for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {\n    const corners: Point[] = [[x, y], [x + 1, y], [x + 1, y + 1], [x, y + 1]]\n    const values = [required(required(heights, y), x), required(required(heights, y), x + 1), required(required(heights, y + 1), x + 1), required(required(heights, y + 1), x)]\n    const edges: Point[] = []\n    for (let i = 0; i < 4; i++) {\n      const j = (i + 1) % 4\n      if ((required(values, i) >= level) === (required(values, j) >= level)) continue\n      const t = (level - required(values, i)) / (required(values, j) - required(values, i))\n      edges.push([lerp(required(corners, i)[0], required(corners, j)[0], t) / size, lerp(required(corners, i)[1], required(corners, j)[1], t) / size])\n    }\n    if (edges.length === 2) segments.push([required(edges, 0), required(edges, 1)])\n    if (edges.length === 4) {\n      const q = (required(values, 0) - level) * (required(values, 2) - level) - (required(values, 1) - level) * (required(values, 3) - level)\n      if (q >= 0) segments.push([required(edges, 0), required(edges, 1)], [required(edges, 2), required(edges, 3)])\n      else segments.push([required(edges, 0), required(edges, 3)], [required(edges, 1), required(edges, 2)])\n    }\n  }\n  return segments\n}\n\nexport const xy = (p: Point) => `${p[0].toFixed(2)},${p[1].toFixed(2)}`\nexport const polygon = (points: Point[]) => `M${points.map(xy).join(\" L\")} Z`\n\n/** Join shared cell edges into contour chains, then use quadratic midpoint\n * interpolation to soften sampling corners without inventing circular rings. */\nexport function smoothContours(segments: [Point, Point][], project: (p: Point) => Point) {\n  const key = (p: Point) => p.map(n => n.toFixed(8)).join(\":\")\n  const adjacent = new Map<string, number[]>()\n  segments.forEach((segment, i) => segment.forEach(p => {\n    const k = key(p)\n    adjacent.set(k, [...(adjacent.get(k) ?? []), i])\n  }))\n  const used = new Set<number>(), paths: string[] = []\n  segments.forEach(([a, b], index) => {\n    if (used.has(index)) return\n    const chain = [a, b]\n    used.add(index)\n    for (const reverse of [false, true]) {\n      for (;;) {\n        const end = required(chain, reverse ? 0 : chain.length - 1)\n        const next = adjacent.get(key(end))?.find(i => !used.has(i))\n        if (next === undefined) break\n        used.add(next)\n        const segment = required(segments, next)\n        const point = key(segment[0]) === key(end) ? segment[1] : segment[0]\n        if (reverse) chain.unshift(point)\n        else chain.push(point)\n      }\n    }\n    const closed = key(required(chain, 0)) === key(required(chain, chain.length - 1))\n    if (closed) chain.pop()\n    const points = chain.map(project)\n    const midpoint = (p: Point, q: Point): Point => [(p[0] + q[0]) / 2, (p[1] + q[1]) / 2]\n    if (closed) {\n      paths.push(`M${xy(midpoint(required(points, points.length - 1), required(points, 0)))} ` + points.map((p, i) => `Q${xy(p)} ${xy(midpoint(p, required(points, (i + 1) % points.length)))}`).join(\" \") + \" Z\")\n    } else {\n      paths.push(`M${xy(required(points, 0))} ` + points.slice(1, -1).map((p, i) => `Q${xy(p)} ${xy(midpoint(p, required(points, i + 2)))}`).join(\" \") + ` L${xy(required(points, points.length - 1))}`)\n    }\n  })\n  return paths.join(\" \")\n}\n\nexport function projector(bearing: number, relief: number) {\n  const angle = bearing * Math.PI / 180\n  return (x: number, y: number, h: number): Point => {\n    const u = (x - .5) * 2, v = (y - .5) * 2\n    return [Number((600 + (u * Math.cos(angle) - v * Math.sin(angle)) * 365).toFixed(4)),\n      Number((390 + (u * Math.sin(angle) + v * Math.cos(angle)) * 175 - (h - .25) * relief).toFixed(4))]\n  }\n}\n\n/** A spanning tree guarantees every site is reachable. Extra nearest-neighbour\n * edges add redundancy without drawing an unreadable complete graph. */\nexport function siteLinks(points: Point[], connections: number) {\n  const distance = (a: number, b: number) => (required(points, a)[0] - required(points, b)[0]) ** 2 + (required(points, a)[1] - required(points, b)[1]) ** 2\n  const edges = new Map<string, [number, number]>()\n  const add = (a: number, b: number) => edges.set(`${Math.min(a, b)}:${Math.max(a, b)}`, [Math.min(a, b), Math.max(a, b)])\n  const reached = new Set([0])\n  while (reached.size < points.length) {\n    let best = Infinity, pair: [number, number] = [0, 0]\n    for (const a of reached) for (let b = 0; b < points.length; b++) {\n      if (!reached.has(b) && distance(a, b) < best) { best = distance(a, b); pair = [a, b] }\n    }\n    add(...pair); reached.add(pair[1])\n  }\n  points.forEach((_, a) => {\n    points.map((_, b) => b).filter(b => b !== a).sort((b, c) => distance(a, b) - distance(a, c))\n      .slice(0, connections).forEach(b => add(a, b))\n  })\n  return [...edges.values()]\n}\n",
      "type": "registry:component",
      "target": "components/blocks/groundwork-background/terrain.ts"
    },
    {
      "path": "components/blocks/groundwork-background/use-groundwork-motion.ts",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState, useSyncExternalStore, type HTMLAttributes, type RefObject } from \"react\"\nimport { blendPose, constrainPose, pointerGroundworkPose, sampleGroundworkTour, type GroundworkMotionMode, type GroundworkPose } from \"./motion-presets\"\nimport { clamp } from \"./terrain\"\n\nfunction subscribeMotion(listener: () => void) {\n  const query = matchMedia(\"(prefers-reduced-motion: reduce)\")\n  query.addEventListener(\"change\", listener)\n  return () => query.removeEventListener(\"change\", listener)\n}\nconst motionSnapshot = () => matchMedia(\"(prefers-reduced-motion: reduce)\").matches\nconst serverMotionSnapshot = () => true\n\nexport interface GroundworkMotionOptions {\n  base: GroundworkPose\n  mode?: GroundworkMotionMode\n  /** False restores the base pose and stops tours and pointer response. */\n  enabled?: boolean\n  paused?: boolean\n  /** Seconds per tour at 1× speed. */\n  duration?: number\n  speed?: number\n  /** Camera/terrain excursion, 0–2. */\n  strength?: number\n  /** Pointer response, 0 (soft) to 1 (direct). */\n  responsiveness?: number\n}\n\n/** Attach interactionProps and viewportRef to the containing viewport, then pass\n * pose to GroundworkBackground. Only the active viewport renders at up to 30fps.\n * Stops offscreen/in hidden tabs; preserves phase when paused; reduced motion\n * disables tours AND pointer response. Keyboard arrows and Home mirror the mouse. */\nexport function useGroundworkMotion({ base, mode = \"ambient\", enabled = true, paused = false,\n  duration = 48, speed = 1, strength = 1, responsiveness = .45,\n}: GroundworkMotionOptions) {\n  const reducedMotion = useSyncExternalStore(subscribeMotion, motionSnapshot, serverMotionSnapshot)\n  const viewportRef = useRef<HTMLDivElement>(null)\n  const [inView, setInView] = useState(false)\n  const [visible, setVisible] = useState(true)\n  const [frame, setFrame] = useState({ pose: constrainPose(base), progress: 0 })\n  const [dragging, setDragging] = useState(false)\n  const current = useRef(constrainPose(base)), target = useRef(constrainPose(base))\n  const phase = useRef(0)\n  const capture = useRef<{ id: number; x: number; y: number; pose: GroundworkPose; element: HTMLDivElement } | null>(null)\n  const config = useRef({ base, duration, speed, strength, responsiveness })\n  // Read fresh controls from the RAF without restarting the clock every render.\n  useEffect(() => { config.current = { base, duration, speed, strength, responsiveness } }, [base, duration, speed, strength, responsiveness])\n  const pointer = mode === \"hover\" || mode === \"drag\"\n  const allowed = enabled && !paused && !reducedMotion && strength > 0\n  const active = allowed && inView && visible && mode !== \"ambient\"\n\n  const release = () => {\n    const held = capture.current\n    capture.current = null\n    if (held?.element.hasPointerCapture(held.id)) held.element.releasePointerCapture(held.id)\n    setDragging(false)\n  }\n\n  useEffect(() => {\n    const element = viewportRef.current\n    if (!element) return\n    const observer = new IntersectionObserver(([entry]) => { if (entry) setInView(entry.isIntersecting) }, { threshold: .05 })\n    observer.observe(element)\n    const visibility = () => { setVisible(!document.hidden); if (document.hidden) release() }\n    visibility()\n    document.addEventListener(\"visibilitychange\", visibility)\n    window.addEventListener(\"blur\", release)\n    return () => { observer.disconnect(); document.removeEventListener(\"visibilitychange\", visibility); window.removeEventListener(\"blur\", release); release() }\n  }, [])\n\n  useEffect(() => {\n    phase.current = 0\n    const initial = sampleGroundworkTour(mode, 0, config.current.base, config.current.strength)\n    current.current = initial\n    target.current = initial\n    setFrame({ pose: initial, progress: 0 })\n    release()\n    // A mode change deliberately starts a new performance. Live controls don't.\n  }, [mode])\n\n  useEffect(() => {\n    if (!allowed) release()\n  }, [allowed])\n\n  useEffect(() => {\n    if (!active) return\n    let raf = 0, previous = 0\n    const tick = (now: number) => {\n      raf = requestAnimationFrame(tick)\n      if (!previous) { previous = now; return }\n      if (now - previous < 1000 / 30) return\n      const dt = Math.min((now - previous) / 1000, .1)\n      previous = now\n      const options = config.current\n      let next: GroundworkPose\n      if (pointer) {\n        const blend = 1 - Math.exp(-dt / (.025 + (1 - clamp(options.responsiveness, 0, 1, .45)) * .6))\n        next = blendPose(current.current, target.current, blend)\n        const keys = Object.keys(next) as (keyof GroundworkPose)[]\n        if (keys.every(key => Math.abs(next[key] - target.current[key]) < .0001)) next = target.current\n        if (keys.every(key => next[key] === current.current[key])) return\n      } else {\n        phase.current = (phase.current + dt / Math.max(2, clamp(options.duration, 2, 120, 48) / clamp(options.speed, .25, 6, 1))) % 1\n        next = sampleGroundworkTour(mode, phase.current, options.base, options.strength)\n      }\n      current.current = next\n      setFrame({ pose: next, progress: phase.current })\n    }\n    raf = requestAnimationFrame(tick)\n    return () => cancelAnimationFrame(raf)\n  }, [active, mode, pointer])\n\n  const pose = !enabled || reducedMotion || strength === 0 || mode === \"ambient\" ? constrainPose(base) : frame.pose\n  const interactionProps: HTMLAttributes<HTMLDivElement> = {\n    tabIndex: pointer && allowed ? 0 : undefined,\n    role: pointer ? \"group\" : undefined,\n    \"aria-label\": pointer ? \"Interactive model. Arrow keys adjust the view; Home resets it.\" : undefined,\n    style: { touchAction: mode === \"drag\" && allowed ? \"none\" : \"auto\", cursor: pointer && allowed ? dragging ? \"grabbing\" : mode === \"drag\" ? \"grab\" : \"crosshair\" : undefined },\n    onPointerDown(event) {\n      if (mode !== \"drag\" || !allowed || !event.isPrimary || event.button !== 0) return\n      if (event.target instanceof Element && event.target.closest(\"button, a, input, select, textarea, [role=button], [contenteditable=true]\")) return\n      event.preventDefault()\n      event.currentTarget.focus({ preventScroll: true })\n      capture.current = { id: event.pointerId, x: event.clientX, y: event.clientY, pose: current.current, element: event.currentTarget }\n      event.currentTarget.setPointerCapture(event.pointerId)\n      setDragging(true)\n    },\n    onPointerMove(event) {\n      if (!allowed || !pointer) return\n      const rect = event.currentTarget.getBoundingClientRect()\n      if (!rect.width || !rect.height) return\n      if (mode === \"hover\" && event.pointerType !== \"touch\") {\n        target.current = pointerGroundworkPose(config.current.base, (event.clientX - rect.left) / rect.width * 2 - 1, (event.clientY - rect.top) / rect.height * 2 - 1, strength)\n      } else if (mode === \"drag\" && capture.current?.id === event.pointerId) {\n        target.current = pointerGroundworkPose(capture.current.pose, (event.clientX - capture.current.x) / rect.width * 2, (event.clientY - capture.current.y) / rect.height * 2, strength)\n      }\n    },\n    onPointerLeave() { if (mode === \"hover\" && allowed) target.current = constrainPose(config.current.base) },\n    onPointerUp: release,\n    onPointerCancel: release,\n    onLostPointerCapture: release,\n    onKeyDown(event) {\n      if (!pointer || !allowed || event.target !== event.currentTarget) return\n      const next = { ...target.current }\n      if (event.key === \"ArrowLeft\") next.bearing -= 5\n      else if (event.key === \"ArrowRight\") next.bearing += 5\n      else if (event.key === \"ArrowUp\") next.relief += 15\n      else if (event.key === \"ArrowDown\") next.relief -= 15\n      else if (event.key === \"Home\") Object.assign(next, config.current.base)\n      else return\n      event.preventDefault()\n      target.current = constrainPose(next)\n    },\n  }\n  return { pose, progress: frame.progress, reducedMotion, dragging, active, viewportRef: viewportRef as RefObject<HTMLDivElement | null>, interactionProps }\n}\n",
      "type": "registry:component",
      "target": "components/blocks/groundwork-background/use-groundwork-motion.ts"
    }
  ],
  "type": "registry:block"
}