Skip to main content

ns-ui

Auxin Canal

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.

Use when a hero background whose supply network visibly routes through the exact negative space the headline/CTA/stat boxes leave open, regrowing on a rest-drain-rescatter cycle and re-topologising whenever a box moves — mechanism is Runions-style space colonisation of discrete auxin sources on a quadtree, not an authored path or a fixed branch grammar, so moving the headline changes which sources exist and therefore the shape itself; pick taproot-trace or tree-root-trace instead when the root/vein path should be a fixed, small, authored trace rather than a scatter-driven growth simulation, or hero-ascii-lichtenberg when the figure should read as an explosive discharge with no drained-area width law and no content-avoidance mechanic.

Install

npx shadcn add https://design.helpmarq.com/r/auxin-canal.json

Ask AI

Point an assistant at this component's docs (llms-full.txt) with one click.

Claude, ChatGPT, Grok, and Perplexity open with the prompt already in. Gemini copies it to your clipboard first — paste it in once the chat opens.

Source
registry/core/auxin-canal/component.tsx
"use client";

import { useEffect, useRef, useState } from "react";

// ---------------------------------------------------------------------------
// AuxinCanal — a full-bleed hero whose vein network grows by space
// colonisation (Runions et al.) through whatever area the copy leaves empty.
// Auxin sources are scattered on a jittered grid across the container, then
// every source that falls inside a measured content box (headline, subcopy,
// CTA row, stat strip — read live via getBoundingClientRect under a
// ResizeObserver) is simply never added. Avoidance is therefore a property
// of where the sources exist, not a mask or clip-path drawn over the result.
//
// Each 30Hz tick: a quadtree of current vein tips answers "which sources are
// within reach", tips advance one step toward the mean direction of every
// source that picked them as nearest, and a second quadtree of sources
// answers "which sources just got swallowed" so they can be removed before
// the next tick. Kill distance dk = 1.7x the mean source spacing (the grid
// cell size); attraction radius = 5x dk; step length = 0.6x dk (always
// smaller than dk, so a step can never leap clean over a source without
// killing it). rho — how tight the source grid is packed — is the one knob
// between a sparse palm-fan network and a dense reticulate mesh.
//
// Segment width is NOT looked up from a lookup table; it is Murray's law,
// maintained incrementally. Every node tracks leafCount = the number of live
// terminal tips its subtree currently ends in (1 while it is itself a tip).
// Extending a tip in place never changes that count. Only the moment a node
// that already has one child grows a SECOND child — an actual fork — does
// its count rise, and that rise is walked straight up its ancestor chain.
// Width of the segment feeding any node = leafCount^(1/3), so a trunk is
// only ever as thick as the tissue still draining through it.
//
// Committed geometry is batched into eight <path> elements bucketed by that
// width (colour is var(--ns-muted) for the four thin buckets, var(--foreground)
// for the four thick ones, via `currentColor` on two wrapping <g>s so a theme
// switch repaints for free); only the current tick's freshly grown segments
// live in a ninth "active tip" path until the next tick folds them in. Once
// no source remains reachable the lamina rests, then abscises — the two
// wrapping groups' `color` transitions to var(--background), draining the
// whole network into the page — and a fresh source scatter begins.
// ---------------------------------------------------------------------------

interface Box {
  x: number;
  y: number;
  w: number;
  h: number;
}

interface Source {
  x: number;
  y: number;
}

interface VNode {
  x: number;
  y: number;
  parent: number; // -1 = seed, enters from the container edge
  leafCount: number;
  children: number[];
}

interface QPoint {
  x: number;
  y: number;
  id: number;
}

/** Minimal point quadtree: insert + circular range query. Rebuilt fresh each
 * tick from whichever point set (tips, or remaining sources) it is asked to
 * index — cheap at these counts (<=1500 points) and avoids ever having to
 * remove a point from a persistent tree. */
class Quadtree {
  private readonly capacity = 8;
  private points: QPoint[] = [];
  private divided = false;
  private nw: Quadtree | null = null;
  private ne: Quadtree | null = null;
  private sw: Quadtree | null = null;
  private se: Quadtree | null = null;

  constructor(private readonly bx: number, private readonly by: number, private readonly bw: number, private readonly bh: number) {}

  private contains(p: QPoint): boolean {
    return p.x >= this.bx && p.x < this.bx + this.bw && p.y >= this.by && p.y < this.by + this.bh;
  }

  private intersectsCircle(cx: number, cy: number, r: number): boolean {
    const nx = Math.max(this.bx, Math.min(cx, this.bx + this.bw));
    const ny = Math.max(this.by, Math.min(cy, this.by + this.bh));
    const dx = cx - nx;
    const dy = cy - ny;
    return dx * dx + dy * dy <= r * r;
  }

  private subdivide(): void {
    const hw = this.bw / 2;
    const hh = this.bh / 2;
    this.nw = new Quadtree(this.bx, this.by, hw, hh);
    this.ne = new Quadtree(this.bx + hw, this.by, hw, hh);
    this.sw = new Quadtree(this.bx, this.by + hh, hw, hh);
    this.se = new Quadtree(this.bx + hw, this.by + hh, hw, hh);
    this.divided = true;
  }

  insert(p: QPoint): boolean {
    if (!this.contains(p)) return false;
    if (!this.divided && this.points.length < this.capacity) {
      this.points.push(p);
      return true;
    }
    if (this.bw < 1 || this.bh < 1) {
      // degenerate leaf — stop subdividing, just accumulate
      this.points.push(p);
      return true;
    }
    if (!this.divided) this.subdivide();
    return (
      (this.nw?.insert(p) ?? false) ||
      (this.ne?.insert(p) ?? false) ||
      (this.sw?.insert(p) ?? false) ||
      (this.se?.insert(p) ?? false)
    );
  }

  queryRadius(cx: number, cy: number, r: number, out: QPoint[]): void {
    if (!this.intersectsCircle(cx, cy, r)) return;
    const r2 = r * r;
    for (const p of this.points) {
      const dx = p.x - cx;
      const dy = p.y - cy;
      if (dx * dx + dy * dy <= r2) out.push(p);
    }
    if (this.divided) {
      this.nw?.queryRadius(cx, cy, r, out);
      this.ne?.queryRadius(cx, cy, r, out);
      this.sw?.queryRadius(cx, cy, r, out);
      this.se?.queryRadius(cx, cy, r, out);
    }
  }
}

function mulberry32(seed: number) {
  let a = seed >>> 0;
  return () => {
    a = (a + 0x6d2b79f5) >>> 0;
    let t = a;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

const KILL_RATIO = 1.7; // dk = KILL_RATIO * mean source spacing
const ATTRACT_RATIO = 5; // di = ATTRACT_RATIO * dk
const STEP_RATIO = 0.6; // D = STEP_RATIO * dk — always < dk
const BOX_PAD = 22; // px clearance kept around every measured content box
const STEP_MS = 1000 / 30; // fixed 30Hz colonisation tick
const STAGNANT_LIMIT = 3; // consecutive empty ticks before growth is "done"
const MAX_TICKS = 900; // hard safety cap (~30s at 30Hz)
const MAX_NODES = 1600;
const REST_MS = 3200; // how long a completed lamina rests before it drains
const DRAIN_MS = 1500; // colour-drain to --background
const W0 = 0.55; // leaf (tip) stroke width in px

const BUCKET_WIDTH = [0.55, 0.85, 1.15, 1.5, 1.9, 2.4, 3.0, 3.7];
const BUCKET_OPACITY = [0.3, 0.4, 0.5, 0.6, 0.5, 0.66, 0.82, 1];
const THIN_BUCKETS = 4; // buckets 0..3 render in the --ns-muted group

function buildSeeds(w: number, h: number): Source[] {
  return [
    { x: w * 0.14, y: h - 1 },
    { x: w * 0.5, y: h - 1 },
    { x: w * 0.86, y: h - 1 },
    { x: 1, y: h * 0.28 },
    { x: 1, y: h * 0.72 },
    { x: w - 1, y: h * 0.28 },
    { x: w - 1, y: h * 0.72 },
  ];
}

function insideAnyBox(x: number, y: number, boxes: Box[]): boolean {
  for (const b of boxes) {
    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) {
      return true;
    }
  }
  return false;
}

function buildSources(
  w: number,
  h: number,
  boxes: Box[],
  targetCount: number,
  rand: () => number
): { sources: Source[]; meanSpacing: number } {
  const area = Math.max(1, w * h);
  const cell = Math.sqrt(area / Math.max(1, targetCount));
  const cols = Math.max(1, Math.round(w / cell));
  const rows = Math.max(1, Math.round(h / cell));
  const cw = w / cols;
  const ch = h / rows;
  const sources: Source[] = [];
  for (let j = 0; j < rows; j++) {
    for (let i = 0; i < cols; i++) {
      const x = (i + 0.5) * cw + (rand() - 0.5) * 0.8 * cw;
      const y = (j + 0.5) * ch + (rand() - 0.5) * 0.8 * ch;
      if (x < 0 || y < 0 || x > w || y > h) continue;
      if (insideAnyBox(x, y, boxes)) continue;
      sources.push({ x, y });
    }
  }
  return { sources, meanSpacing: (cw + ch) / 2 };
}

export interface AuxinCanalProps {
  /** mono eyebrow line above the headline */
  eyebrow?: string;
  /** display headline, one string per rendered line */
  headlineLines?: string[];
  /** muted supporting copy under the headline */
  subcopy?: string;
  /** accent primary CTA */
  primaryCta?: { label: string; href: string };
  /** ghost secondary CTA */
  secondaryCta?: { label: string; href: string };
  /** three mono stats for the bordered strip below the CTAs */
  stats?: { value: string; label: string }[];
  /** target auxin source count before content-box exclusion (rho's knob) */
  sourceCount?: number;
  /** extra classes merged onto the rendered root element */
  className?: string;
}

export function AuxinCanal({
  eyebrow = "SPACE-COLONISED ROUTING",
  headlineLines = ["Traffic finds every channel", "your interface leaves open"],
  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.",
  primaryCta = { label: "Read the docs", href: "#docs" },
  secondaryCta = { label: "View the API", href: "#api" },
  stats = [
    { value: "1,200", label: "auxin sources / cycle" },
    { value: "1.7×", label: "kill-distance ratio" },
    { value: "30Hz", label: "colonisation tick" },
  ],
  sourceCount = 1200,
  className = "",
}: AuxinCanalProps) {
  const rootRef = useRef<HTMLElement>(null);
  const svgRef = useRef<SVGSVGElement>(null);
  const bucketRefs = useRef<(SVGPathElement | null)[]>([]);
  const activeRef = useRef<SVGPathElement | null>(null);
  const [draining, setDraining] = useState(false);
  const linesKey = headlineLines.join(" ");

  useEffect(() => {
    const root = rootRef.current;
    const svg = svgRef.current;
    if (!root || !svg) return;

    const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    const rand = mulberry32(0xa0c1a2);

    let disposed = false;
    let w = 0;
    let h = 0;
    let sized = false;

    let nodes: VNode[] = [];
    let sources: Source[] = [];
    let dk = 1;
    let di = 1;
    let step = 1;
    let newTickStart = 0;
    let stagnant = 0;
    let tickCount = 0;
    let phase: "grow" | "rest" | "drain" = "grow";
    let visible = true;

    let raf = 0;
    let acc = 0;
    let last = 0;
    let restTimer = 0;
    let drainTimer = 0;

    const measureBoxes = (): Box[] => {
      const rect = root.getBoundingClientRect();
      const boxes: Box[] = [];
      root.querySelectorAll<HTMLElement>("[data-auxin-box]").forEach((el) => {
        const r = el.getBoundingClientRect();
        if (r.width <= 0 || r.height <= 0) return;
        boxes.push({ x: r.left - rect.left, y: r.top - rect.top, w: r.width, h: r.height });
      });
      return boxes;
    };

    const applyD = (idx: number, d: string) => {
      const el = bucketRefs.current[idx];
      if (el) el.setAttribute("d", d);
    };

    const clearPaths = () => {
      for (let i = 0; i < 8; i++) applyD(i, "");
      activeRef.current?.setAttribute("d", "");
    };

    // -- one node ends a tip's flight; adding a child is where Murray's law
    // actually happens: extending an existing tip in place costs nothing,
    // only a genuine fork (a node gaining its SECOND child) raises leafCount,
    // and that rise is walked straight up the ancestor chain. --------------
    const addChild = (parentIdx: number, x: number, y: number): number => {
      const idx = nodes.length;
      nodes.push({ x, y, parent: parentIdx, leafCount: 1, children: [] });
      const parent = nodes[parentIdx];
      if (!parent) return idx;
      const wasLeaf = parent.children.length === 0;
      parent.children.push(idx);
      if (!wasLeaf) {
        let cur: number = parentIdx;
        while (cur !== -1) {
          const n = nodes[cur];
          if (!n) break;
          n.leafCount += 1;
          cur = n.parent;
        }
      }
      return idx;
    };

    // -- one fixed-length colonisation step: attract every live source to
    // its nearest reachable tip via a quadtree of nodes, advance every tip
    // that attracted at least one source, then kill every source a fresh
    // node lands within dk of via a quadtree of sources. ------------------
    const tick = (): boolean => {
      if (sources.length === 0 || nodes.length >= MAX_NODES) return false;

      const nodeQT = new Quadtree(-di, -di, w + 2 * di, h + 2 * di);
      for (let i = 0; i < nodes.length; i++) {
        const n = nodes[i];
        if (n) nodeQT.insert({ x: n.x, y: n.y, id: i });
      }

      const sumX = new Map<number, number>();
      const sumY = new Map<number, number>();
      const buf: QPoint[] = [];
      for (const s of sources) {
        buf.length = 0;
        nodeQT.queryRadius(s.x, s.y, di, buf);
        if (buf.length === 0) continue;
        let bestId = -1;
        let bestD = Infinity;
        for (const p of buf) {
          const dx = p.x - s.x;
          const dy = p.y - s.y;
          const d = dx * dx + dy * dy;
          if (d < bestD) {
            bestD = d;
            bestId = p.id;
          }
        }
        if (bestId < 0) continue;
        const node = nodes[bestId];
        if (!node) continue;
        const dx = s.x - node.x;
        const dy = s.y - node.y;
        const len = Math.hypot(dx, dy) || 1;
        sumX.set(bestId, (sumX.get(bestId) ?? 0) + dx / len);
        sumY.set(bestId, (sumY.get(bestId) ?? 0) + dy / len);
      }
      if (sumX.size === 0) return false;

      newTickStart = nodes.length;
      for (const [idx, sx] of sumX) {
        const sy = sumY.get(idx) ?? 0;
        const len = Math.hypot(sx, sy) || 1;
        const parent = nodes[idx];
        if (!parent) continue;
        const nx = parent.x + (sx / len) * step;
        const ny = parent.y + (sy / len) * step;
        addChild(idx, nx, ny);
        if (nodes.length >= MAX_NODES) break;
      }
      if (nodes.length === newTickStart) return false;

      const srcQT = new Quadtree(-1, -1, w + 2, h + 2);
      for (let i = 0; i < sources.length; i++) {
        const s = sources[i];
        if (s) srcQT.insert({ x: s.x, y: s.y, id: i });
      }
      const dead = new Set<number>();
      const kbuf: QPoint[] = [];
      for (let i = newTickStart; i < nodes.length; i++) {
        const n = nodes[i];
        if (!n) continue;
        kbuf.length = 0;
        srcQT.queryRadius(n.x, n.y, dk, kbuf);
        for (const p of kbuf) dead.add(p.id);
      }
      if (dead.size > 0) sources = sources.filter((_, i) => !dead.has(i));
      return true;
    };

    // -- rebuild the eight committed buckets (everything grown before this
    // tick) and the one active-tip path (everything grown this tick). Full
    // rebuild every tick is O(nodes) — trivial at these counts (<=1600). --
    const render = () => {
      let maxLeaf = 1;
      for (const n of nodes) if (n.leafCount > maxLeaf) maxLeaf = n.leafCount;
      const maxWidth = W0 * Math.cbrt(maxLeaf);
      const spans = new Array(8).fill("") as string[];
      let activeSpan = "";
      for (let i = 0; i < nodes.length; i++) {
        const n = nodes[i];
        if (!n || n.parent === -1) continue;
        const p = nodes[n.parent];
        if (!p) continue;
        const seg = `M${p.x.toFixed(1)} ${p.y.toFixed(1)}L${n.x.toFixed(1)} ${n.y.toFixed(1)}`;
        if (i >= newTickStart) {
          activeSpan += seg;
          continue;
        }
        const width = W0 * Math.cbrt(n.leafCount);
        const t = maxWidth > W0 ? (width - W0) / (maxWidth - W0) : 0;
        const bi = Math.min(7, Math.max(0, Math.floor(t * 8)));
        spans[bi] += seg;
      }
      for (let i = 0; i < 8; i++) applyD(i, spans[i] ?? "");
      activeRef.current?.setAttribute("d", activeSpan);
    };

    const finishGrowth = () => {
      phase = "rest";
      newTickStart = nodes.length; // fold whatever was still "active" in
      render();
      window.clearTimeout(restTimer);
      restTimer = window.setTimeout(beginDrain, REST_MS);
    };

    const beginDrain = () => {
      if (disposed) return;
      phase = "drain";
      setDraining(true);
      window.clearTimeout(drainTimer);
      drainTimer = window.setTimeout(resetCycle, DRAIN_MS);
    };

    const resetCycle = () => {
      if (disposed) return;
      setDraining(false);
      clearPaths();
      startCycle();
    };

    const startCycle = () => {
      window.clearTimeout(restTimer);
      window.clearTimeout(drainTimer);
      const rect = root.getBoundingClientRect();
      w = rect.width;
      h = rect.height;
      if (w < 2 || h < 2) {
        sized = false;
        return;
      }
      sized = true;
      const boxes = measureBoxes();
      const built = buildSources(w, h, boxes, sourceCount, rand);
      sources = built.sources;
      dk = KILL_RATIO * built.meanSpacing;
      di = ATTRACT_RATIO * dk;
      step = STEP_RATIO * dk;
      nodes = buildSeeds(w, h).map((s) => ({ x: s.x, y: s.y, parent: -1, leafCount: 1, children: [] }));
      newTickStart = nodes.length;
      stagnant = 0;
      tickCount = 0;
      phase = "grow";

      if (reduced) {
        // run the whole cycle to completion synchronously, once, and stop
        for (let i = 0; i < MAX_TICKS; i++) {
          const grew = tick();
          if (!grew) {
            stagnant += 1;
            if (stagnant >= STAGNANT_LIMIT) break;
          } else {
            stagnant = 0;
          }
        }
        newTickStart = nodes.length;
        render();
        phase = "rest"; // settled — no drain, no further cycles
        return;
      }

      last = 0;
      acc = 0;
      if (!raf) raf = requestAnimationFrame(loop);
    };

    const loop = (now: number) => {
      raf = 0;
      if (!visible || !sized || phase !== "grow") return;
      if (last === 0) last = now;
      acc += Math.min(100, now - last);
      last = now;
      while (acc >= STEP_MS) {
        acc -= STEP_MS;
        const grew = tick();
        tickCount += 1;
        if (!grew) {
          stagnant += 1;
        } else {
          stagnant = 0;
        }
        if (stagnant >= STAGNANT_LIMIT || tickCount >= MAX_TICKS) {
          finishGrowth();
          return;
        }
      }
      render();
      raf = requestAnimationFrame(loop);
    };

    startCycle();

    // -- resize / layout observers: re-measure content boxes and restart
    // the whole cycle, so a moved headline genuinely changes the topology
    // rather than just clipping the same network differently. ------------
    let resizeTimer = 0;
    const onResize = () => {
      window.clearTimeout(resizeTimer);
      resizeTimer = window.setTimeout(() => {
        if (disposed) return;
        cancelAnimationFrame(raf);
        raf = 0;
        window.clearTimeout(restTimer);
        window.clearTimeout(drainTimer);
        setDraining(false);
        clearPaths();
        startCycle();
      }, 120);
    };
    const ro = new ResizeObserver(onResize);
    ro.observe(root);

    const io = new IntersectionObserver((entries) => {
      visible = entries[0]?.isIntersecting ?? true;
      if (visible && phase === "grow" && !raf) {
        last = 0;
        raf = requestAnimationFrame(loop);
      }
    });
    io.observe(root);

    document.fonts.ready.then(() => {
      if (!disposed) onResize();
    });

    return () => {
      disposed = true;
      cancelAnimationFrame(raf);
      raf = 0;
      window.clearTimeout(restTimer);
      window.clearTimeout(drainTimer);
      window.clearTimeout(resizeTimer);
      ro.disconnect();
      io.disconnect();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [linesKey, sourceCount]);

  return (
    <section
      ref={rootRef}
      className={`relative isolate overflow-hidden bg-background ${className}`}
    >
      <svg
        ref={svgRef}
        aria-hidden="true"
        focusable="false"
        className="pointer-events-none absolute inset-0 h-full w-full"
      >
        <g
          style={{
            color: draining ? "var(--background)" : "var(--ns-muted)",
            transition: "color 1500ms cubic-bezier(0.4, 0, 0.2, 1)",
          }}
        >
          {BUCKET_WIDTH.slice(0, THIN_BUCKETS).map((width, i) => (
            <path
              key={i}
              ref={(el) => {
                bucketRefs.current[i] = el;
              }}
              d=""
              fill="none"
              stroke="currentColor"
              strokeOpacity={BUCKET_OPACITY[i]}
              strokeWidth={width}
              strokeLinecap="round"
            />
          ))}
          <path
            ref={activeRef}
            d=""
            fill="none"
            stroke="currentColor"
            strokeOpacity={0.4}
            strokeWidth={BUCKET_WIDTH[0]}
            strokeLinecap="round"
          />
        </g>
        <g
          style={{
            color: draining ? "var(--background)" : "var(--foreground)",
            transition: "color 1500ms cubic-bezier(0.4, 0, 0.2, 1)",
          }}
        >
          {BUCKET_WIDTH.slice(THIN_BUCKETS).map((width, i) => (
            <path
              key={i + THIN_BUCKETS}
              ref={(el) => {
                bucketRefs.current[i + THIN_BUCKETS] = el;
              }}
              d=""
              fill="none"
              stroke="currentColor"
              strokeOpacity={BUCKET_OPACITY[i + THIN_BUCKETS]}
              strokeWidth={width}
              strokeLinecap="round"
            />
          ))}
        </g>
      </svg>
      <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">
        <p className="mb-6 font-mono text-[11px] tracking-widest text-ns-muted">{eyebrow}</p>
        <h1
          data-auxin-box
          className="font-semibold text-foreground"
          style={{ fontSize: "clamp(2.5rem, 6.5vw, 4.5rem)", lineHeight: 1.06, letterSpacing: "-0.03em" }}
        >
          {headlineLines.map((line, i) => (
            <span key={i} className="block">
              {line}
            </span>
          ))}
        </h1>
        <p data-auxin-box className="mt-6 max-w-xl text-base leading-relaxed text-ns-muted">
          {subcopy}
        </p>
        <div data-auxin-box className="mt-9 flex flex-wrap items-center justify-center gap-3">
          <a
            href={primaryCta.href}
            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"
          >
            {primaryCta.label}
          </a>
          <a
            href={secondaryCta.href}
            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"
          >
            {secondaryCta.label}
          </a>
        </div>
        <div
          data-auxin-box
          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"
        >
          {stats.slice(0, 3).map((s) => (
            <div key={s.label} className="flex flex-col items-center gap-1 px-6 py-4">
              <span className="font-mono text-lg text-foreground">{s.value}</span>
              <span className="font-mono text-[11px] tracking-widest text-ns-muted">
                {s.label.toUpperCase()}
              </span>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}
Build spec

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).

Props

PropTypeDefaultDescription
eyebrow?string"SPACE-COLONISED ROUTING"mono eyebrow line above the headline
headlineLines?string[]["Traffic finds every channel", "your interface leaves open"]display headline, one string per rendered line
subcopy?string"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."muted supporting copy under the headline
primaryCta?{ label: string; href: string }{ label: "Read the docs", href: "#docs" }accent primary CTA
secondaryCta?{ label: string; href: string }{ label: "View the API", href: "#api" }ghost secondary CTA
stats?{ value: string; label: string }[][ { value: "1,200", label: "auxin sources / cycle" }, { value: "1.7×", label: "kill-distance ratio" }, { value: "30Hz", label: "colonisation tick" }, ]three mono stats for the bordered strip below the CTAs
sourceCount?number1200target auxin source count before content-box exclusion (rho's knob)
className?stringextra classes merged onto the rendered root element