{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "auxin-canal",
  "title": "Auxin Canal",
  "description": "Full-bleed hero whose vein network grows by space colonisation through whatever negative space the copy leaves empty: sources scattered around measured content boxes, tips advancing toward and consuming them, widths thickened by Murray's law, then a slow drain to background and a fresh scatter begins.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/auxin-canal/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\n// ---------------------------------------------------------------------------\n// AuxinCanal — a full-bleed hero whose vein network grows by space\n// colonisation (Runions et al.) through whatever area the copy leaves empty.\n// Auxin sources are scattered on a jittered grid across the container, then\n// every source that falls inside a measured content box (headline, subcopy,\n// CTA row, stat strip — read live via getBoundingClientRect under a\n// ResizeObserver) is simply never added. Avoidance is therefore a property\n// of where the sources exist, not a mask or clip-path drawn over the result.\n//\n// Each 30Hz tick: a quadtree of current vein tips answers \"which sources are\n// within reach\", tips advance one step toward the mean direction of every\n// source that picked them as nearest, and a second quadtree of sources\n// answers \"which sources just got swallowed\" so they can be removed before\n// the next tick. Kill distance dk = 1.7x the mean source spacing (the grid\n// cell size); attraction radius = 5x dk; step length = 0.6x dk (always\n// smaller than dk, so a step can never leap clean over a source without\n// killing it). rho — how tight the source grid is packed — is the one knob\n// between a sparse palm-fan network and a dense reticulate mesh.\n//\n// Segment width is NOT looked up from a lookup table; it is Murray's law,\n// maintained incrementally. Every node tracks leafCount = the number of live\n// terminal tips its subtree currently ends in (1 while it is itself a tip).\n// Extending a tip in place never changes that count. Only the moment a node\n// that already has one child grows a SECOND child — an actual fork — does\n// its count rise, and that rise is walked straight up its ancestor chain.\n// Width of the segment feeding any node = leafCount^(1/3), so a trunk is\n// only ever as thick as the tissue still draining through it.\n//\n// Committed geometry is batched into eight <path> elements bucketed by that\n// width (colour is var(--ns-muted) for the four thin buckets, var(--foreground)\n// for the four thick ones, via `currentColor` on two wrapping <g>s so a theme\n// switch repaints for free); only the current tick's freshly grown segments\n// live in a ninth \"active tip\" path until the next tick folds them in. Once\n// no source remains reachable the lamina rests, then abscises — the two\n// wrapping groups' `color` transitions to var(--background), draining the\n// whole network into the page — and a fresh source scatter begins.\n// ---------------------------------------------------------------------------\n\ninterface Box {\n  x: number;\n  y: number;\n  w: number;\n  h: number;\n}\n\ninterface Source {\n  x: number;\n  y: number;\n}\n\ninterface VNode {\n  x: number;\n  y: number;\n  parent: number; // -1 = seed, enters from the container edge\n  leafCount: number;\n  children: number[];\n}\n\ninterface QPoint {\n  x: number;\n  y: number;\n  id: number;\n}\n\n/** Minimal point quadtree: insert + circular range query. Rebuilt fresh each\n * tick from whichever point set (tips, or remaining sources) it is asked to\n * index — cheap at these counts (<=1500 points) and avoids ever having to\n * remove a point from a persistent tree. */\nclass Quadtree {\n  private readonly capacity = 8;\n  private points: QPoint[] = [];\n  private divided = false;\n  private nw: Quadtree | null = null;\n  private ne: Quadtree | null = null;\n  private sw: Quadtree | null = null;\n  private se: Quadtree | null = null;\n\n  constructor(private readonly bx: number, private readonly by: number, private readonly bw: number, private readonly bh: number) {}\n\n  private contains(p: QPoint): boolean {\n    return p.x >= this.bx && p.x < this.bx + this.bw && p.y >= this.by && p.y < this.by + this.bh;\n  }\n\n  private intersectsCircle(cx: number, cy: number, r: number): boolean {\n    const nx = Math.max(this.bx, Math.min(cx, this.bx + this.bw));\n    const ny = Math.max(this.by, Math.min(cy, this.by + this.bh));\n    const dx = cx - nx;\n    const dy = cy - ny;\n    return dx * dx + dy * dy <= r * r;\n  }\n\n  private subdivide(): void {\n    const hw = this.bw / 2;\n    const hh = this.bh / 2;\n    this.nw = new Quadtree(this.bx, this.by, hw, hh);\n    this.ne = new Quadtree(this.bx + hw, this.by, hw, hh);\n    this.sw = new Quadtree(this.bx, this.by + hh, hw, hh);\n    this.se = new Quadtree(this.bx + hw, this.by + hh, hw, hh);\n    this.divided = true;\n  }\n\n  insert(p: QPoint): boolean {\n    if (!this.contains(p)) return false;\n    if (!this.divided && this.points.length < this.capacity) {\n      this.points.push(p);\n      return true;\n    }\n    if (this.bw < 1 || this.bh < 1) {\n      // degenerate leaf — stop subdividing, just accumulate\n      this.points.push(p);\n      return true;\n    }\n    if (!this.divided) this.subdivide();\n    return (\n      (this.nw?.insert(p) ?? false) ||\n      (this.ne?.insert(p) ?? false) ||\n      (this.sw?.insert(p) ?? false) ||\n      (this.se?.insert(p) ?? false)\n    );\n  }\n\n  queryRadius(cx: number, cy: number, r: number, out: QPoint[]): void {\n    if (!this.intersectsCircle(cx, cy, r)) return;\n    const r2 = r * r;\n    for (const p of this.points) {\n      const dx = p.x - cx;\n      const dy = p.y - cy;\n      if (dx * dx + dy * dy <= r2) out.push(p);\n    }\n    if (this.divided) {\n      this.nw?.queryRadius(cx, cy, r, out);\n      this.ne?.queryRadius(cx, cy, r, out);\n      this.sw?.queryRadius(cx, cy, r, out);\n      this.se?.queryRadius(cx, cy, r, out);\n    }\n  }\n}\n\nfunction mulberry32(seed: number) {\n  let a = seed >>> 0;\n  return () => {\n    a = (a + 0x6d2b79f5) >>> 0;\n    let t = a;\n    t = Math.imul(t ^ (t >>> 15), t | 1);\n    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nconst KILL_RATIO = 1.7; // dk = KILL_RATIO * mean source spacing\nconst ATTRACT_RATIO = 5; // di = ATTRACT_RATIO * dk\nconst STEP_RATIO = 0.6; // D = STEP_RATIO * dk — always < dk\nconst BOX_PAD = 22; // px clearance kept around every measured content box\nconst STEP_MS = 1000 / 30; // fixed 30Hz colonisation tick\nconst STAGNANT_LIMIT = 3; // consecutive empty ticks before growth is \"done\"\nconst MAX_TICKS = 900; // hard safety cap (~30s at 30Hz)\nconst MAX_NODES = 1600;\nconst REST_MS = 3200; // how long a completed lamina rests before it drains\nconst DRAIN_MS = 1500; // colour-drain to --background\nconst W0 = 0.55; // leaf (tip) stroke width in px\n\nconst BUCKET_WIDTH = [0.55, 0.85, 1.15, 1.5, 1.9, 2.4, 3.0, 3.7];\nconst BUCKET_OPACITY = [0.3, 0.4, 0.5, 0.6, 0.5, 0.66, 0.82, 1];\nconst THIN_BUCKETS = 4; // buckets 0..3 render in the --ns-muted group\n\nfunction buildSeeds(w: number, h: number): Source[] {\n  return [\n    { x: w * 0.14, y: h - 1 },\n    { x: w * 0.5, y: h - 1 },\n    { x: w * 0.86, y: h - 1 },\n    { x: 1, y: h * 0.28 },\n    { x: 1, y: h * 0.72 },\n    { x: w - 1, y: h * 0.28 },\n    { x: w - 1, y: h * 0.72 },\n  ];\n}\n\nfunction insideAnyBox(x: number, y: number, boxes: Box[]): boolean {\n  for (const b of boxes) {\n    if (x >= b.x - BOX_PAD && x <= b.x + b.w + BOX_PAD && y >= b.y - BOX_PAD && y <= b.y + b.h + BOX_PAD) {\n      return true;\n    }\n  }\n  return false;\n}\n\nfunction buildSources(\n  w: number,\n  h: number,\n  boxes: Box[],\n  targetCount: number,\n  rand: () => number\n): { sources: Source[]; meanSpacing: number } {\n  const area = Math.max(1, w * h);\n  const cell = Math.sqrt(area / Math.max(1, targetCount));\n  const cols = Math.max(1, Math.round(w / cell));\n  const rows = Math.max(1, Math.round(h / cell));\n  const cw = w / cols;\n  const ch = h / rows;\n  const sources: Source[] = [];\n  for (let j = 0; j < rows; j++) {\n    for (let i = 0; i < cols; i++) {\n      const x = (i + 0.5) * cw + (rand() - 0.5) * 0.8 * cw;\n      const y = (j + 0.5) * ch + (rand() - 0.5) * 0.8 * ch;\n      if (x < 0 || y < 0 || x > w || y > h) continue;\n      if (insideAnyBox(x, y, boxes)) continue;\n      sources.push({ x, y });\n    }\n  }\n  return { sources, meanSpacing: (cw + ch) / 2 };\n}\n\nexport interface AuxinCanalProps {\n  /** mono eyebrow line above the headline */\n  eyebrow?: string;\n  /** display headline, one string per rendered line */\n  headlineLines?: string[];\n  /** muted supporting copy under the headline */\n  subcopy?: string;\n  /** accent primary CTA */\n  primaryCta?: { label: string; href: string };\n  /** ghost secondary CTA */\n  secondaryCta?: { label: string; href: string };\n  /** three mono stats for the bordered strip below the CTAs */\n  stats?: { value: string; label: string }[];\n  /** target auxin source count before content-box exclusion (rho's knob) */\n  sourceCount?: number;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nexport function AuxinCanal({\n  eyebrow = \"SPACE-COLONISED ROUTING\",\n  headlineLines = [\"Traffic finds every channel\", \"your interface leaves open\"],\n  subcopy = \"Auxin Canal grows a delivery network through whatever capacity the page doesn't use — sources scattered across the empty space, consumed as tips reach them, and widened by Murray's law so trunks earn their thickness from the load they actually carry.\",\n  primaryCta = { label: \"Read the docs\", href: \"#docs\" },\n  secondaryCta = { label: \"View the API\", href: \"#api\" },\n  stats = [\n    { value: \"1,200\", label: \"auxin sources / cycle\" },\n    { value: \"1.7×\", label: \"kill-distance ratio\" },\n    { value: \"30Hz\", label: \"colonisation tick\" },\n  ],\n  sourceCount = 1200,\n  className = \"\",\n}: AuxinCanalProps) {\n  const rootRef = useRef<HTMLElement>(null);\n  const svgRef = useRef<SVGSVGElement>(null);\n  const bucketRefs = useRef<(SVGPathElement | null)[]>([]);\n  const activeRef = useRef<SVGPathElement | null>(null);\n  const [draining, setDraining] = useState(false);\n  const linesKey = headlineLines.join(\" \");\n\n  useEffect(() => {\n    const root = rootRef.current;\n    const svg = svgRef.current;\n    if (!root || !svg) return;\n\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    const rand = mulberry32(0xa0c1a2);\n\n    let disposed = false;\n    let w = 0;\n    let h = 0;\n    let sized = false;\n\n    let nodes: VNode[] = [];\n    let sources: Source[] = [];\n    let dk = 1;\n    let di = 1;\n    let step = 1;\n    let newTickStart = 0;\n    let stagnant = 0;\n    let tickCount = 0;\n    let phase: \"grow\" | \"rest\" | \"drain\" = \"grow\";\n    let visible = true;\n\n    let raf = 0;\n    let acc = 0;\n    let last = 0;\n    let restTimer = 0;\n    let drainTimer = 0;\n\n    const measureBoxes = (): Box[] => {\n      const rect = root.getBoundingClientRect();\n      const boxes: Box[] = [];\n      root.querySelectorAll<HTMLElement>(\"[data-auxin-box]\").forEach((el) => {\n        const r = el.getBoundingClientRect();\n        if (r.width <= 0 || r.height <= 0) return;\n        boxes.push({ x: r.left - rect.left, y: r.top - rect.top, w: r.width, h: r.height });\n      });\n      return boxes;\n    };\n\n    const applyD = (idx: number, d: string) => {\n      const el = bucketRefs.current[idx];\n      if (el) el.setAttribute(\"d\", d);\n    };\n\n    const clearPaths = () => {\n      for (let i = 0; i < 8; i++) applyD(i, \"\");\n      activeRef.current?.setAttribute(\"d\", \"\");\n    };\n\n    // -- one node ends a tip's flight; adding a child is where Murray's law\n    // actually happens: extending an existing tip in place costs nothing,\n    // only a genuine fork (a node gaining its SECOND child) raises leafCount,\n    // and that rise is walked straight up the ancestor chain. --------------\n    const addChild = (parentIdx: number, x: number, y: number): number => {\n      const idx = nodes.length;\n      nodes.push({ x, y, parent: parentIdx, leafCount: 1, children: [] });\n      const parent = nodes[parentIdx];\n      if (!parent) return idx;\n      const wasLeaf = parent.children.length === 0;\n      parent.children.push(idx);\n      if (!wasLeaf) {\n        let cur: number = parentIdx;\n        while (cur !== -1) {\n          const n = nodes[cur];\n          if (!n) break;\n          n.leafCount += 1;\n          cur = n.parent;\n        }\n      }\n      return idx;\n    };\n\n    // -- one fixed-length colonisation step: attract every live source to\n    // its nearest reachable tip via a quadtree of nodes, advance every tip\n    // that attracted at least one source, then kill every source a fresh\n    // node lands within dk of via a quadtree of sources. ------------------\n    const tick = (): boolean => {\n      if (sources.length === 0 || nodes.length >= MAX_NODES) return false;\n\n      const nodeQT = new Quadtree(-di, -di, w + 2 * di, h + 2 * di);\n      for (let i = 0; i < nodes.length; i++) {\n        const n = nodes[i];\n        if (n) nodeQT.insert({ x: n.x, y: n.y, id: i });\n      }\n\n      const sumX = new Map<number, number>();\n      const sumY = new Map<number, number>();\n      const buf: QPoint[] = [];\n      for (const s of sources) {\n        buf.length = 0;\n        nodeQT.queryRadius(s.x, s.y, di, buf);\n        if (buf.length === 0) continue;\n        let bestId = -1;\n        let bestD = Infinity;\n        for (const p of buf) {\n          const dx = p.x - s.x;\n          const dy = p.y - s.y;\n          const d = dx * dx + dy * dy;\n          if (d < bestD) {\n            bestD = d;\n            bestId = p.id;\n          }\n        }\n        if (bestId < 0) continue;\n        const node = nodes[bestId];\n        if (!node) continue;\n        const dx = s.x - node.x;\n        const dy = s.y - node.y;\n        const len = Math.hypot(dx, dy) || 1;\n        sumX.set(bestId, (sumX.get(bestId) ?? 0) + dx / len);\n        sumY.set(bestId, (sumY.get(bestId) ?? 0) + dy / len);\n      }\n      if (sumX.size === 0) return false;\n\n      newTickStart = nodes.length;\n      for (const [idx, sx] of sumX) {\n        const sy = sumY.get(idx) ?? 0;\n        const len = Math.hypot(sx, sy) || 1;\n        const parent = nodes[idx];\n        if (!parent) continue;\n        const nx = parent.x + (sx / len) * step;\n        const ny = parent.y + (sy / len) * step;\n        addChild(idx, nx, ny);\n        if (nodes.length >= MAX_NODES) break;\n      }\n      if (nodes.length === newTickStart) return false;\n\n      const srcQT = new Quadtree(-1, -1, w + 2, h + 2);\n      for (let i = 0; i < sources.length; i++) {\n        const s = sources[i];\n        if (s) srcQT.insert({ x: s.x, y: s.y, id: i });\n      }\n      const dead = new Set<number>();\n      const kbuf: QPoint[] = [];\n      for (let i = newTickStart; i < nodes.length; i++) {\n        const n = nodes[i];\n        if (!n) continue;\n        kbuf.length = 0;\n        srcQT.queryRadius(n.x, n.y, dk, kbuf);\n        for (const p of kbuf) dead.add(p.id);\n      }\n      if (dead.size > 0) sources = sources.filter((_, i) => !dead.has(i));\n      return true;\n    };\n\n    // -- rebuild the eight committed buckets (everything grown before this\n    // tick) and the one active-tip path (everything grown this tick). Full\n    // rebuild every tick is O(nodes) — trivial at these counts (<=1600). --\n    const render = () => {\n      let maxLeaf = 1;\n      for (const n of nodes) if (n.leafCount > maxLeaf) maxLeaf = n.leafCount;\n      const maxWidth = W0 * Math.cbrt(maxLeaf);\n      const spans = new Array(8).fill(\"\") as string[];\n      let activeSpan = \"\";\n      for (let i = 0; i < nodes.length; i++) {\n        const n = nodes[i];\n        if (!n || n.parent === -1) continue;\n        const p = nodes[n.parent];\n        if (!p) continue;\n        const seg = `M${p.x.toFixed(1)} ${p.y.toFixed(1)}L${n.x.toFixed(1)} ${n.y.toFixed(1)}`;\n        if (i >= newTickStart) {\n          activeSpan += seg;\n          continue;\n        }\n        const width = W0 * Math.cbrt(n.leafCount);\n        const t = maxWidth > W0 ? (width - W0) / (maxWidth - W0) : 0;\n        const bi = Math.min(7, Math.max(0, Math.floor(t * 8)));\n        spans[bi] += seg;\n      }\n      for (let i = 0; i < 8; i++) applyD(i, spans[i] ?? \"\");\n      activeRef.current?.setAttribute(\"d\", activeSpan);\n    };\n\n    const finishGrowth = () => {\n      phase = \"rest\";\n      newTickStart = nodes.length; // fold whatever was still \"active\" in\n      render();\n      window.clearTimeout(restTimer);\n      restTimer = window.setTimeout(beginDrain, REST_MS);\n    };\n\n    const beginDrain = () => {\n      if (disposed) return;\n      phase = \"drain\";\n      setDraining(true);\n      window.clearTimeout(drainTimer);\n      drainTimer = window.setTimeout(resetCycle, DRAIN_MS);\n    };\n\n    const resetCycle = () => {\n      if (disposed) return;\n      setDraining(false);\n      clearPaths();\n      startCycle();\n    };\n\n    const startCycle = () => {\n      window.clearTimeout(restTimer);\n      window.clearTimeout(drainTimer);\n      const rect = root.getBoundingClientRect();\n      w = rect.width;\n      h = rect.height;\n      if (w < 2 || h < 2) {\n        sized = false;\n        return;\n      }\n      sized = true;\n      const boxes = measureBoxes();\n      const built = buildSources(w, h, boxes, sourceCount, rand);\n      sources = built.sources;\n      dk = KILL_RATIO * built.meanSpacing;\n      di = ATTRACT_RATIO * dk;\n      step = STEP_RATIO * dk;\n      nodes = buildSeeds(w, h).map((s) => ({ x: s.x, y: s.y, parent: -1, leafCount: 1, children: [] }));\n      newTickStart = nodes.length;\n      stagnant = 0;\n      tickCount = 0;\n      phase = \"grow\";\n\n      if (reduced) {\n        // run the whole cycle to completion synchronously, once, and stop\n        for (let i = 0; i < MAX_TICKS; i++) {\n          const grew = tick();\n          if (!grew) {\n            stagnant += 1;\n            if (stagnant >= STAGNANT_LIMIT) break;\n          } else {\n            stagnant = 0;\n          }\n        }\n        newTickStart = nodes.length;\n        render();\n        phase = \"rest\"; // settled — no drain, no further cycles\n        return;\n      }\n\n      last = 0;\n      acc = 0;\n      if (!raf) raf = requestAnimationFrame(loop);\n    };\n\n    const loop = (now: number) => {\n      raf = 0;\n      if (!visible || !sized || phase !== \"grow\") return;\n      if (last === 0) last = now;\n      acc += Math.min(100, now - last);\n      last = now;\n      while (acc >= STEP_MS) {\n        acc -= STEP_MS;\n        const grew = tick();\n        tickCount += 1;\n        if (!grew) {\n          stagnant += 1;\n        } else {\n          stagnant = 0;\n        }\n        if (stagnant >= STAGNANT_LIMIT || tickCount >= MAX_TICKS) {\n          finishGrowth();\n          return;\n        }\n      }\n      render();\n      raf = requestAnimationFrame(loop);\n    };\n\n    startCycle();\n\n    // -- resize / layout observers: re-measure content boxes and restart\n    // the whole cycle, so a moved headline genuinely changes the topology\n    // rather than just clipping the same network differently. ------------\n    let resizeTimer = 0;\n    const onResize = () => {\n      window.clearTimeout(resizeTimer);\n      resizeTimer = window.setTimeout(() => {\n        if (disposed) return;\n        cancelAnimationFrame(raf);\n        raf = 0;\n        window.clearTimeout(restTimer);\n        window.clearTimeout(drainTimer);\n        setDraining(false);\n        clearPaths();\n        startCycle();\n      }, 120);\n    };\n    const ro = new ResizeObserver(onResize);\n    ro.observe(root);\n\n    const io = new IntersectionObserver((entries) => {\n      visible = entries[0]?.isIntersecting ?? true;\n      if (visible && phase === \"grow\" && !raf) {\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    });\n    io.observe(root);\n\n    document.fonts.ready.then(() => {\n      if (!disposed) onResize();\n    });\n\n    return () => {\n      disposed = true;\n      cancelAnimationFrame(raf);\n      raf = 0;\n      window.clearTimeout(restTimer);\n      window.clearTimeout(drainTimer);\n      window.clearTimeout(resizeTimer);\n      ro.disconnect();\n      io.disconnect();\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [linesKey, sourceCount]);\n\n  return (\n    <section\n      ref={rootRef}\n      className={`relative isolate overflow-hidden bg-background ${className}`}\n    >\n      <svg\n        ref={svgRef}\n        aria-hidden=\"true\"\n        focusable=\"false\"\n        className=\"pointer-events-none absolute inset-0 h-full w-full\"\n      >\n        <g\n          style={{\n            color: draining ? \"var(--background)\" : \"var(--ns-muted)\",\n            transition: \"color 1500ms cubic-bezier(0.4, 0, 0.2, 1)\",\n          }}\n        >\n          {BUCKET_WIDTH.slice(0, THIN_BUCKETS).map((width, i) => (\n            <path\n              key={i}\n              ref={(el) => {\n                bucketRefs.current[i] = el;\n              }}\n              d=\"\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeOpacity={BUCKET_OPACITY[i]}\n              strokeWidth={width}\n              strokeLinecap=\"round\"\n            />\n          ))}\n          <path\n            ref={activeRef}\n            d=\"\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeOpacity={0.4}\n            strokeWidth={BUCKET_WIDTH[0]}\n            strokeLinecap=\"round\"\n          />\n        </g>\n        <g\n          style={{\n            color: draining ? \"var(--background)\" : \"var(--foreground)\",\n            transition: \"color 1500ms cubic-bezier(0.4, 0, 0.2, 1)\",\n          }}\n        >\n          {BUCKET_WIDTH.slice(THIN_BUCKETS).map((width, i) => (\n            <path\n              key={i + THIN_BUCKETS}\n              ref={(el) => {\n                bucketRefs.current[i + THIN_BUCKETS] = el;\n              }}\n              d=\"\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeOpacity={BUCKET_OPACITY[i + THIN_BUCKETS]}\n              strokeWidth={width}\n              strokeLinecap=\"round\"\n            />\n          ))}\n        </g>\n      </svg>\n      <div className=\"relative z-10 mx-auto flex w-full max-w-5xl flex-col items-center px-6 pb-16 pt-24 text-center sm:pb-24 sm:pt-32\">\n        <p className=\"mb-6 font-mono text-[11px] tracking-widest text-ns-muted\">{eyebrow}</p>\n        <h1\n          data-auxin-box\n          className=\"font-semibold text-foreground\"\n          style={{ fontSize: \"clamp(2.5rem, 6.5vw, 4.5rem)\", lineHeight: 1.06, letterSpacing: \"-0.03em\" }}\n        >\n          {headlineLines.map((line, i) => (\n            <span key={i} className=\"block\">\n              {line}\n            </span>\n          ))}\n        </h1>\n        <p data-auxin-box className=\"mt-6 max-w-xl text-base leading-relaxed text-ns-muted\">\n          {subcopy}\n        </p>\n        <div data-auxin-box className=\"mt-9 flex flex-wrap items-center justify-center gap-3\">\n          <a\n            href={primaryCta.href}\n            className=\"rounded-sm bg-ns-accent px-5 py-2.5 text-sm font-medium text-white transition-colors duration-200 hover:bg-ns-accent-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n          >\n            {primaryCta.label}\n          </a>\n          <a\n            href={secondaryCta.href}\n            className=\"rounded-sm border border-border px-5 py-2.5 text-sm font-medium text-ns-muted transition-colors duration-200 hover:border-foreground/20 hover:bg-surface hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n          >\n            {secondaryCta.label}\n          </a>\n        </div>\n        <div\n          data-auxin-box\n          className=\"mt-16 grid w-full max-w-2xl grid-cols-1 divide-y divide-border border-y border-border sm:grid-cols-3 sm:divide-x sm:divide-y-0\"\n        >\n          {stats.slice(0, 3).map((s) => (\n            <div key={s.label} className=\"flex flex-col items-center gap-1 px-6 py-4\">\n              <span className=\"font-mono text-lg text-foreground\">{s.value}</span>\n              <span className=\"font-mono text-[11px] tracking-widest text-ns-muted\">\n                {s.label.toUpperCase()}\n              </span>\n            </div>\n          ))}\n        </div>\n      </div>\n    </section>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/auxin-canal.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)",
      "color-ns-accent-hover": "var(--ns-accent-hover)",
      "color-surface": "var(--surface)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff",
      "ns-accent-hover": "#0059d1",
      "surface": "#fafafa"
    },
    "dark": {
      "ns-muted": "#8f8f8f",
      "surface": "#171717"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "hero",
      "svg",
      "vein",
      "space-colonisation",
      "generative",
      "quadtree",
      "murray-law",
      "ambient",
      "api",
      "docs"
    ],
    "instruction": "A full-bleed hero whose SVG vein network grows by Runions-style space colonisation through whatever area the DOM copy does not occupy. On mount and on every debounced ResizeObserver/webfont-ready event, the container is measured and every descendant carrying `data-auxin-box` (the headline, the subcopy paragraph, the CTA row, the stat strip) is measured too via getBoundingClientRect; a jittered grid of roughly `sourceCount` (default 1200) auxin sources is scattered across the container and any source that falls within a 22px margin of a content box is simply never added to the set — avoidance is a property of where sources exist, not a clip-path applied to the result. Mean source spacing is the grid's own cell size (sqrt(area / sourceCount)); kill distance dk = 1.7x that spacing (the single governing scalar between a sparse fan and a dense reticulate mesh), attraction radius = 5x dk, and step length = 0.6x dk so a single step can never leap clean over a source without killing it. Seven fixed seed points enter from the container's edges (three along the bottom, two per side) as the network's roots. Growth runs on a fixed 30Hz tick (a rAF loop accumulating real elapsed time into 33.3ms steps, so speed never depends on frame rate): each tick builds a quadtree of current tip nodes, finds every live source within reach of some tip, advances each attracting tip one step toward the mean unit-vector direction of the sources that picked it as nearest, then builds a second quadtree of remaining sources to kill every source within dk of a node that was just created. Growth halts after three consecutive ticks that create no new node (unreachable sources are left alive and simply never drawn toward — veins terminate exactly where open space runs out, never mid-void) or a 1600-node safety cap. Width is Murray's law maintained incrementally rather than solved as a post-pass: every node carries a leafCount, starting at 1; extending an existing tip in place costs nothing (the tip's count just moves to its new endpoint), but the instant a node that already has one child grows a second — an actual fork — its count rises by one and that rise is walked straight up its own ancestor chain, so a trunk's width (leafCount^(1/3)) is always exactly the terminal count still draining through it. Each tick, every segment except the ones grown that same tick is bucketed by width into one of eight `<path>` elements (four rendered inside a `<g>` whose `color` is var(--ns-muted) for the thinner half, four inside a `<g>` whose `color` is var(--foreground) for the thicker half, both using `stroke=\"currentColor\"` so a theme flip repaints for free with zero JS); the current tick's freshly grown segments live in one extra active-tip path until the following tick folds them into the buckets. Once growth stalls, the lamina rests for 3.2s, then both wrapping `<g>` elements' `color` transitions to var(--background) over 1.5s (a genuine colour drain into the page, not an opacity fade), after which every path is cleared and the whole cycle — fresh jittered scatter, fresh seeds, fresh growth — begins again. The svg is `aria-hidden` with `pointer-events-none`; the headline, subcopy and both CTAs are ordinary focusable DOM, untouched by any of this. `autoplay: none` because the network animates on its own internal clock regardless of pointer/scroll/press input — there is nothing for the site's synthetic-input driver to do. Under `prefers-reduced-motion` the entire colonisation-to-completion loop runs once, synchronously, at mount (same tick function, called in a tight loop instead of on rAF) and renders exactly one finished, fully usable venation with no rest/drain/rescatter cycle ever starting. Zero dependencies, DOM+SVG+CSS only, no canvas, every colour a token (--background --foreground --ns-muted --border --ns-accent)."
  },
  "type": "registry:ui"
}