{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "seep-lattice",
  "title": "Seep Lattice",
  "description": "A feature-flag rollout picker where blast radius is computed as literal site percolation: user segments are cells in a declared-adjacency lattice, the rollout percentage wets a fixed-order prefix of cells, and union-find continuously finds the largest connected wet cluster. Its traced perimeter and percentage are the honest readout a plain rollout number hides, with a cliff at the lattice's own measured containment threshold rather than a smooth curve.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/seep-lattice/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\n\n// ---------------------------------------------------------------------------\n// SeepLattice — a feature-flag rollout picker where \"blast radius\" is\n// computed as literal site percolation, not a smooth percentage. Every user\n// segment is a cell in a fixed grid lattice; adjacency is DECLARED as an\n// explicit edge list (each cell's up/down/left/right neighbours — the\n// lattice standing in for shared-infra/shared-tenancy proximity), never\n// inferred from distance to the slider value. One scalar `p` (the rollout\n// percentage) governs everything: a fixed seeded permutation orders every\n// cell once at mount, and the wet set is always exactly the first\n// round(p/100 * n) cells of that order — so p directly prefixes the wet\n// set, dragging back down un-wets in EXACT reverse, and nothing is ever\n// re-rolled mid-drag. On every value change, union-find runs over the\n// declared adjacency graph (never a radial or rectangular zone guess) to\n// find the largest connected wet cluster; that cluster's outer boundary is\n// traced into ONE SVG perimeter path (marching-squares-style edge walk, not\n// per-cell borders), and its size is the headline figure. The containment\n// threshold line is measured once at mount by binary search over THIS\n// lattice's actual spanning behaviour (top-row-to-bottom-row connectivity),\n// not the textbook 0.5927 — a finite lattice with this adjacency has its\n// own threshold. Newly-wet cells fill with a capillary stagger: a mini BFS\n// over just the cells added this step, seeded from wherever they touch\n// already-wet cells, delays each cell's fill transition by its BFS depth —\n// water visibly floods outward from existing wet regions, not a uniform\n// flash. Everything is a pure function of p and the lattice; the only\n// interactive control is the rollout slider itself.\n// ---------------------------------------------------------------------------\n\nconst CELL_GAP = 0.16; // inset per cell, in viewBox cell-units\nconst FRONT_SPEED_MS = 16; // ms per BFS-depth step of capillary stagger\nconst MAX_STAGGER_MS = 260; // cap so a huge jump doesn't queue forever\nconst FILL_DURATION_MS = 200;\n\nfunction clamp(v: number, lo: number, hi: number) {\n  return Math.min(hi, Math.max(lo, v));\n}\n\n// -- seeded PRNG + permutation ----------------------------------------------\n\nfunction mulberry32(seed: number) {\n  let s = seed >>> 0;\n  return function rand() {\n    s = (s + 0x6d2b79f5) | 0;\n    let t = Math.imul(s ^ (s >>> 15), 1 | s);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nfunction seededOrder(n: number, seed: number): number[] {\n  const order = Array.from({ length: n }, (_, i) => i);\n  const rand = mulberry32(seed);\n  for (let i = n - 1; i > 0; i--) {\n    const j = Math.floor(rand() * (i + 1));\n    const tmp = order[i];\n    order[i] = order[j];\n    order[j] = tmp;\n  }\n  return order;\n}\n\n// -- declared adjacency (4-neighbour grid, explicit edge list) --------------\n\nfunction buildNeighbors(rows: number, cols: number): number[][] {\n  const n = rows * cols;\n  const neighbors: number[][] = new Array(n);\n  for (let r = 0; r < rows; r++) {\n    for (let c = 0; c < cols; c++) {\n      const idx = r * cols + c;\n      const list: number[] = [];\n      if (r > 0) list.push(idx - cols);\n      if (r < rows - 1) list.push(idx + cols);\n      if (c > 0) list.push(idx - 1);\n      if (c < cols - 1) list.push(idx + 1);\n      neighbors[idx] = list;\n    }\n  }\n  return neighbors;\n}\n\n// -- spanning test (top row <-> bottom row) over the wet prefix -------------\n\nfunction isSpanning(\n  k: number,\n  order: number[],\n  neighbors: number[][],\n  rows: number,\n  cols: number,\n  n: number\n): boolean {\n  const TOP = n;\n  const BOTTOM = n + 1;\n  const parent = new Int32Array(n + 2);\n  for (let i = 0; i < n + 2; i++) parent[i] = i;\n  const find = (x: number): number => {\n    while (parent[x] !== x) {\n      parent[x] = parent[parent[x]];\n      x = parent[x];\n    }\n    return x;\n  };\n  const union = (a: number, b: number) => {\n    const ra = find(a);\n    const rb = find(b);\n    if (ra !== rb) parent[ra] = rb;\n  };\n  const wet = new Uint8Array(n);\n  for (let i = 0; i < k; i++) wet[order[i]] = 1;\n  for (let c = 0; c < n; c++) {\n    if (!wet[c]) continue;\n    const row = Math.floor(c / cols);\n    if (row === 0) union(c, TOP);\n    if (row === rows - 1) union(c, BOTTOM);\n    for (const nb of neighbors[c]) if (wet[nb]) union(c, nb);\n  }\n  return find(TOP) === find(BOTTOM);\n}\n\ninterface Lattice {\n  n: number;\n  rows: number;\n  cols: number;\n  neighbors: number[][];\n  order: number[];\n  thresholdK: number;\n  thresholdPct: number;\n}\n\n// Built once per (rows, cols, seed): declares adjacency, fixes the wetting\n// permutation, and measures this exact lattice's own percolation threshold\n// by binary search (spanning is monotonic in k since wet cells only ever\n// accumulate along a fixed prefix).\nfunction buildLattice(rows: number, cols: number, seed: number): Lattice {\n  const n = rows * cols;\n  const neighbors = buildNeighbors(rows, cols);\n  const order = seededOrder(n, seed);\n  let lo = 0;\n  let hi = n;\n  while (lo + 1 < hi) {\n    const mid = (lo + hi) >> 1;\n    if (isSpanning(mid, order, neighbors, rows, cols, n)) hi = mid;\n    else lo = mid;\n  }\n  return { n, rows, cols, neighbors, order, thresholdK: hi, thresholdPct: (hi / n) * 100 };\n}\n\n// -- union-find over the wet prefix, every frame -----------------------------\n\ninterface WetResult {\n  wetMask: Uint8Array;\n  clusterMask: Uint8Array;\n  largestSize: number;\n}\n\nfunction computeWet(k: number, lattice: Lattice): WetResult {\n  const { n, order, neighbors } = lattice;\n  const wet = new Uint8Array(n);\n  for (let i = 0; i < k; i++) wet[order[i]] = 1;\n  const parent = new Int32Array(n);\n  const size = new Int32Array(n).fill(1);\n  for (let i = 0; i < n; i++) parent[i] = i;\n  const find = (x: number): number => {\n    while (parent[x] !== x) {\n      parent[x] = parent[parent[x]];\n      x = parent[x];\n    }\n    return x;\n  };\n  const union = (a: number, b: number) => {\n    let ra = find(a);\n    let rb = find(b);\n    if (ra === rb) return;\n    if (size[ra] < size[rb]) {\n      const t = ra;\n      ra = rb;\n      rb = t;\n    }\n    parent[rb] = ra;\n    size[ra] += size[rb];\n  };\n  for (let c = 0; c < n; c++) {\n    if (!wet[c]) continue;\n    for (const nb of neighbors[c]) if (wet[nb]) union(c, nb);\n  }\n  let bestRoot = -1;\n  let bestSize = 0;\n  for (let c = 0; c < n; c++) {\n    if (!wet[c]) continue;\n    const r = find(c);\n    if (size[r] > bestSize) {\n      bestSize = size[r];\n      bestRoot = r;\n    }\n  }\n  const clusterMask = new Uint8Array(n);\n  if (bestRoot >= 0) {\n    for (let c = 0; c < n; c++) if (wet[c] && find(c) === bestRoot) clusterMask[c] = 1;\n  }\n  return { wetMask: wet, clusterMask, largestSize: bestSize };\n}\n\n// -- trace the largest cluster's outer boundary into one (or more, if it\n// has a hole) SVG path loop, by walking declared grid edges — not by\n// drawing per-cell borders. Any deterministic pick at a rare diagonal-touch\n// vertex still decomposes the edge set into valid closed loops. -----------\n\nfunction tracePerimeter(mask: Uint8Array, rows: number, cols: number): string {\n  const at = (r: number, c: number): boolean =>\n    r >= 0 && r < rows && c >= 0 && c < cols ? mask[r * cols + c] === 1 : false;\n\n  const out = new Map<string, { x: number; y: number }[]>();\n  const push = (fx: number, fy: number, tx: number, ty: number) => {\n    const key = `${fx},${fy}`;\n    const arr = out.get(key);\n    if (arr) arr.push({ x: tx, y: ty });\n    else out.set(key, [{ x: tx, y: ty }]);\n  };\n\n  for (let r = 0; r < rows; r++) {\n    for (let c = 0; c < cols; c++) {\n      if (!at(r, c)) continue;\n      if (!at(r - 1, c)) push(c + 1, r, c, r); // north edge, right -> left\n      if (!at(r, c + 1)) push(c + 1, r + 1, c + 1, r); // east edge, bottom -> top\n      if (!at(r + 1, c)) push(c, r + 1, c + 1, r + 1); // south edge, left -> right\n      if (!at(r, c - 1)) push(c, r, c, r + 1); // west edge, top -> bottom\n    }\n  }\n\n  const loops: { x: number; y: number }[][] = [];\n  for (const [startKey, arr] of out) {\n    while (arr.length) {\n      const [sx, sy] = startKey.split(\",\").map(Number);\n      let cx = sx;\n      let cy = sy;\n      const loop: { x: number; y: number }[] = [{ x: cx, y: cy }];\n      let guard = 0;\n      while (guard++ < 100000) {\n        const outs = out.get(`${cx},${cy}`);\n        if (!outs || outs.length === 0) break;\n        const next = outs.pop() as { x: number; y: number };\n        cx = next.x;\n        cy = next.y;\n        if (cx === sx && cy === sy) break;\n        loop.push({ x: cx, y: cy });\n      }\n      if (loop.length >= 3) loops.push(loop);\n    }\n  }\n\n  if (!loops.length) return \"\";\n  return loops.map((loop) => `M ${loop.map((p) => `${p.x} ${p.y}`).join(\" L \")} Z`).join(\" \");\n}\n\n// -----------------------------------------------------------------------------\n\nexport interface SeepLatticeProps {\n  /** lattice height, in cells (kept small so union-find stays cheap per frame) */\n  rows?: number;\n  /** lattice width, in cells */\n  cols?: number;\n  /** fixed seed for the wetting permutation and adjacency — same seed, same lattice */\n  seed?: number;\n  /** controlled rollout percentage, 0-100 */\n  rollout?: number;\n  /** uncontrolled initial rollout percentage */\n  defaultRollout?: number;\n  /** called with the new rollout percentage on every change */\n  onRolloutChange?: (v: number) => void;\n  /** noun used in the readouts, e.g. \"traffic\" (default) or \"requests\" */\n  segmentLabel?: string;\n  /** extra classes merged onto the root element */\n  className?: string;\n}\n\nexport function SeepLattice({\n  rows = 16,\n  cols = 24,\n  seed = 20260817,\n  rollout,\n  defaultRollout = 35,\n  onRolloutChange,\n  segmentLabel = \"traffic\",\n  className = \"\",\n}: SeepLatticeProps) {\n  const isControlled = rollout !== undefined;\n  const [rolloutInternal, setRolloutInternal] = useState(() => clamp(defaultRollout, 0, 100));\n  const value = isControlled ? (rollout as number) : rolloutInternal;\n  const valueRef = useRef(value);\n  valueRef.current = value;\n\n  const safeRows = Math.max(2, Math.floor(rows));\n  const safeCols = Math.max(2, Math.floor(cols));\n  const lattice = useMemo(() => buildLattice(safeRows, safeCols, seed), [safeRows, safeCols, seed]);\n\n  const [reduced, setReduced] = useState(false);\n  useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const update = () => setReduced(mq.matches);\n    update();\n    mq.addEventListener(\"change\", update);\n    return () => mq.removeEventListener(\"change\", update);\n  }, []);\n\n  const k = clamp(Math.round((value / 100) * lattice.n), 0, lattice.n);\n\n  const { wetMask, clusterMask, largestSize } = useMemo(() => computeWet(k, lattice), [k, lattice]);\n  const perimeterD = useMemo(\n    () => tracePerimeter(clusterMask, lattice.rows, lattice.cols),\n    [clusterMask, lattice]\n  );\n\n  const clusterPct = lattice.n > 0 ? Math.round((largestSize / lattice.n) * 100) : 0;\n  const roundedRollout = Math.round(value);\n  const thresholdRoundedPct = Math.round(lattice.thresholdPct);\n  const aboveThreshold = k >= lattice.thresholdK;\n\n  // -- capillary stagger: a mini BFS over just this step's newly-wet cells,\n  // seeded from wherever they touch already-wet cells, sets each affected\n  // cell's transition-delay directly (skipped outright under reduced motion\n  // — cells then just snap, no stagger, no entrance). Runs in a layout\n  // effect so the delay lands before the browser paints the fill change\n  // committed in the same render. --------------------------------------\n  const cellElsRef = useRef<(SVGRectElement | null)[]>([]);\n  const prevKRef = useRef(0);\n  const prevWetRef = useRef<Uint8Array | null>(null);\n\n  useLayoutEffect(() => {\n    const { n, order, neighbors } = lattice;\n    if (!prevWetRef.current || prevWetRef.current.length !== n) {\n      prevWetRef.current = new Uint8Array(n);\n      prevKRef.current = 0;\n    }\n    const prevWet = prevWetRef.current;\n    const prevK = prevKRef.current;\n\n    // Un-wetting (dragging p down) is always instant with no stagger — clear\n    // any delay left over from a prior flood before deciding whether this\n    // step needs a new one, or a cell that flooded in staggered would dry\n    // out staggered by a delay computed for a different pass.\n    for (let i = 0; i < n; i++) {\n      const el = cellElsRef.current[i];\n      if (el && el.style.transitionDelay) el.style.transitionDelay = \"\";\n    }\n\n    if (!reduced && k > prevK) {\n      const newlySet = new Set<number>();\n      for (let i = prevK; i < k; i++) newlySet.add(order[i]);\n      const depth = new Map<number, number>();\n      const queue: number[] = [];\n      for (const cell of newlySet) {\n        for (const nb of neighbors[cell]) {\n          if (prevWet[nb]) {\n            depth.set(cell, 0);\n            queue.push(cell);\n            break;\n          }\n        }\n      }\n      if (queue.length === 0) {\n        for (const cell of newlySet) {\n          depth.set(cell, 0);\n          queue.push(cell);\n        }\n      }\n      let qi = 0;\n      while (qi < queue.length) {\n        const cur = queue[qi++];\n        const d = depth.get(cur) as number;\n        for (const nb of neighbors[cur]) {\n          if (newlySet.has(nb) && !depth.has(nb)) {\n            depth.set(nb, d + 1);\n            queue.push(nb);\n          }\n        }\n      }\n      for (const [cell, d] of depth) {\n        const el = cellElsRef.current[cell];\n        if (el) el.style.transitionDelay = `${Math.min(d * FRONT_SPEED_MS, MAX_STAGGER_MS)}ms`;\n      }\n    }\n\n    const curWet = new Uint8Array(n);\n    for (let i = 0; i < k; i++) curWet[order[i]] = 1;\n    prevWetRef.current = curWet;\n    prevKRef.current = k;\n  }, [lattice, k, reduced]);\n\n  // -- one polite announcement per threshold crossing, in each direction --\n  const liveRegionRef = useRef<HTMLDivElement>(null);\n  const mountedRef = useRef(false);\n  const prevAboveRef = useRef(aboveThreshold);\n  useEffect(() => {\n    if (!mountedRef.current) {\n      mountedRef.current = true;\n      prevAboveRef.current = aboveThreshold;\n      return;\n    }\n    if (aboveThreshold !== prevAboveRef.current) {\n      prevAboveRef.current = aboveThreshold;\n      if (liveRegionRef.current) {\n        liveRegionRef.current.textContent = aboveThreshold\n          ? `Crossed containment threshold — largest connected exposure now spans the lattice.`\n          : `Back below containment threshold.`;\n      }\n    }\n  }, [aboveThreshold]);\n\n  const commit = (v: number) => {\n    const q = clamp(v, 0, 100);\n    if (Math.abs(q - valueRef.current) < 1e-9) return;\n    if (!isControlled) setRolloutInternal(q);\n    onRolloutChange?.(q);\n  };\n\n  const trackRef = useRef<HTMLDivElement>(null);\n  const draggingRef = useRef(false);\n  const [active, setActive] = useState(false);\n\n  const posToValue = (clientX: number): number => {\n    const track = trackRef.current;\n    if (!track) return valueRef.current;\n    const rect = track.getBoundingClientRect();\n    if (rect.width <= 0) return valueRef.current;\n    return clamp(((clientX - rect.left) / rect.width) * 100, 0, 100);\n  };\n\n  const onTrackPointerDown = (e: React.PointerEvent) => {\n    e.preventDefault();\n    e.currentTarget.setPointerCapture(e.pointerId);\n    draggingRef.current = true;\n    setActive(true);\n    commit(posToValue(e.clientX));\n  };\n  const onTrackPointerMove = (e: React.PointerEvent) => {\n    if (!draggingRef.current) return;\n    commit(posToValue(e.clientX));\n  };\n  const endDrag = () => {\n    draggingRef.current = false;\n    setActive(false);\n  };\n\n  const onKeyDown = (e: React.KeyboardEvent) => {\n    const v = valueRef.current;\n    const cellStep = Math.max(100 / lattice.n, 0.5);\n    let next: number | null = null;\n    switch (e.key) {\n      case \"ArrowRight\":\n      case \"ArrowUp\":\n        next = v + cellStep;\n        break;\n      case \"ArrowLeft\":\n      case \"ArrowDown\":\n        next = v - cellStep;\n        break;\n      case \"PageUp\":\n        next = v + 10;\n        break;\n      case \"PageDown\":\n        next = v - 10;\n        break;\n      case \"Home\":\n        next = 0;\n        break;\n      case \"End\":\n        next = 100;\n        break;\n      default:\n        return;\n    }\n    e.preventDefault();\n    commit(clamp(next, 0, 100));\n  };\n\n  const valueText = `rollout ${roundedRollout}% — largest connected exposure ${clusterPct}% of ${segmentLabel}, ${\n    aboveThreshold ? \"above\" : \"below\"\n  } containment threshold`;\n\n  return (\n    <div className={`w-full select-none ${className}`}>\n      <svg\n        aria-hidden=\"true\"\n        focusable=\"false\"\n        viewBox={`0 0 ${lattice.cols} ${lattice.rows}`}\n        preserveAspectRatio=\"xMidYMid meet\"\n        className=\"block w-full rounded-md border border-border bg-background\"\n        style={{ aspectRatio: `${lattice.cols} / ${lattice.rows}` }}\n      >\n        {Array.from({ length: lattice.n }, (_, i) => {\n          const r = Math.floor(i / lattice.cols);\n          const c = i % lattice.cols;\n          const isWet = wetMask[i] === 1;\n          const isCluster = clusterMask[i] === 1;\n          const fill = isCluster ? \"var(--foreground)\" : isWet ? \"var(--ns-muted)\" : \"var(--border)\";\n          return (\n            <rect\n              key={i}\n              ref={(el) => {\n                cellElsRef.current[i] = el;\n              }}\n              x={c + CELL_GAP / 2}\n              y={r + CELL_GAP / 2}\n              width={1 - CELL_GAP}\n              height={1 - CELL_GAP}\n              rx={0.06}\n              fill={fill}\n              className=\"transition-colors ease-out motion-reduce:transition-none\"\n              style={{ transitionDuration: `${FILL_DURATION_MS}ms` }}\n            />\n          );\n        })}\n        {perimeterD && (\n          <path\n            d={perimeterD}\n            fill=\"var(--foreground)\"\n            fillOpacity={0.06}\n            fillRule=\"evenodd\"\n            stroke=\"var(--foreground)\"\n            strokeWidth={2}\n            strokeLinejoin=\"round\"\n            vectorEffect=\"non-scaling-stroke\"\n          />\n        )}\n      </svg>\n\n      <div className=\"mt-3 flex items-center justify-between font-mono text-xs\">\n        <span className=\"text-ns-muted\">Rollout</span>\n        <span className=\"tabular-nums text-foreground\">{roundedRollout}%</span>\n      </div>\n\n      <div\n        ref={trackRef}\n        role=\"slider\"\n        tabIndex={0}\n        aria-label=\"Rollout percentage\"\n        aria-orientation=\"horizontal\"\n        aria-valuemin={0}\n        aria-valuemax={100}\n        aria-valuenow={roundedRollout}\n        aria-valuetext={valueText}\n        onKeyDown={onKeyDown}\n        onFocus={() => setActive(true)}\n        onBlur={() => setActive(false)}\n        onPointerDown={onTrackPointerDown}\n        onPointerMove={onTrackPointerMove}\n        onPointerUp={endDrag}\n        onPointerCancel={endDrag}\n        className=\"group relative mt-1.5 h-5 w-full cursor-pointer touch-none outline-none focus-visible:ring-2 focus-visible:ring-ns-accent focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n      >\n        <span className=\"absolute left-0 top-1/2 h-1 w-full -translate-y-1/2 rounded-full bg-border\" />\n        <span\n          aria-hidden\n          className=\"absolute top-1/2 h-1 -translate-y-1/2 rounded-full bg-foreground/70\"\n          style={{ width: `${roundedRollout}%` }}\n        />\n        <span\n          aria-hidden\n          className=\"absolute top-1/2 h-3 w-px -translate-y-1/2 bg-foreground/50\"\n          style={{ left: `${thresholdRoundedPct}%` }}\n        />\n        <span\n          aria-hidden\n          data-active={active}\n          style={{ left: `${roundedRollout}%` }}\n          className=\"absolute top-1/2 h-3.5 w-3.5 -translate-x-1/2 -translate-y-1/2 rounded-full border border-border bg-background transition-colors duration-150 group-hover:border-foreground/40 data-[active=true]:border-ns-accent\"\n        />\n      </div>\n\n      <p className=\"mt-2 font-mono text-xs text-ns-muted\">\n        threshold <span className=\"tabular-nums text-foreground\">{thresholdRoundedPct}%</span>\n        {\" · \"}largest connected exposure{\" \"}\n        <span className=\"tabular-nums text-foreground\">{clusterPct}%</span> of {segmentLabel}\n        {\" — \"}\n        <span className={aboveThreshold ? \"text-foreground\" : \"text-ns-muted\"}>\n          {aboveThreshold ? \"above\" : \"below\"} containment threshold\n        </span>\n      </p>\n\n      <div aria-live=\"polite\" className=\"sr-only\" ref={liveRegionRef} />\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/seep-lattice.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "slider",
      "percolation",
      "union-find",
      "feature-flag",
      "rollout",
      "svg",
      "graph",
      "risk",
      "form"
    ],
    "instruction": "Build a rollout slider whose entire content is a phase transition, not a smooth mapping from percentage to outcome. LATTICE: at mount, build a fixed rows x cols grid of cells (default 16x24 = 384, kept comfortably under ~600 so union-find stays cheap every frame) representing user segments, with DECLARED adjacency — an explicit edge list of each cell's up/down/left/right grid neighbours, built once and reused for the lattice's entire lifetime, standing in for shared-infra/shared-tenancy proximity. Separately, a seeded Fisher-Yates shuffle (mulberry32 PRNG, fixed seed prop) produces a permutation `order` of all cell indices, ALSO fixed for the component's lifetime. MECHANISM: there is exactly one governing scalar, the rollout percentage `p` (0-100). At any moment the wet set is defined as `order.slice(0, round(p/100 * n))` — a literal prefix of the fixed permutation — so p directly determines the wet set with no randomness at read time: dragging p up wets cells in exactly that fixed order, dragging back down un-wets in EXACT reverse, deterministic and replayable, never re-rolled mid-drag. On every value change (drag frame, keystroke, or external prop change) run union-find fresh over the wet cells using ONLY the declared adjacency edge list — never a radial distance-from-slider-value approximation and never a rectangular zone guess; that shortcut is the specific wrong version this component must not become, since it would fabricate containment the topology does not guarantee. From the union-find result, find the single largest connected component among wet cells (by cell count) and trace ONLY that cluster's outer boundary into one SVG path — walk the grid's boundary edges (an edge exists between a wet cluster cell and any non-cluster neighbour or the lattice edge, oriented consistently so cluster interior stays on one side) and stitch them tail-to-head into closed loops, rendered as a single `<path>` with evenodd fill (handles a cluster with a hole correctly) — never per-cell borders/strokes on every wet cell, the perimeter is drawn once around the whole blob. THRESHOLD: once at mount, binary-search over k (0..n) for the minimal prefix length at which the wet cells connect the lattice's top row to its bottom row (spanning is monotonic in k since the prefix only ever grows) — this measures THIS specific seeded lattice's own containment threshold, not the textbook infinite-lattice value 0.5927, because a finite grid with this declared adjacency has its own number. Render that threshold as a static tick mark on the slider track. HEADLINE FIGURE: the largest cluster's cell count as a percentage of total cells is the number that matters, always shown, e.g. 'largest connected exposure 41% of traffic' — never just the rollout percentage alone. RENDERING: cells are SVG `<rect>`s in a single `viewBox=\"0 0 cols rows\"` (one unit per cell, small inset gap, aria-hidden, non-focusable — they are derived from p, never independently choosable), coloured `var(--border)` when dry, `var(--ns-muted)` when wet but outside the largest cluster, `var(--foreground)` when inside the largest cluster; the traced perimeter path is stroked `var(--foreground)` with `vector-effect=\"non-scaling-stroke\"` and a faint `var(--foreground)` fill for legibility. `--ns-accent` never appears on the lattice itself — only on the slider handle/focus ring while focused or dragging, matching this registry's interaction-only accent rule. CAPILLARY WETTING: when p increases, run a small multi-source BFS restricted to just the cells newly added this step, seeded at depth 0 from any of them that touch an already-wet cell (or, if an entirely disconnected new patch appears with no wet neighbour yet, seed all of them at depth 0), and set each newly-wet cell's fill-transition delay to `min(depth * frontSpeedMs, capMs)` before the fill colour itself changes (a layout effect run before paint, so the delay is in effect for the very transition it targets) — the result reads as water flooding outward from existing wet regions in visible fronts, not a uniform flash across scattered cells. Un-wetting (dragging p down) is instant with no stagger. INTERACTION: the ONLY interactive, focusable element is a horizontal `role=\"slider\"` track below the lattice and the live readouts (drag anywhere on it to jump/scrub, matching this registry's click-jumps-to-position convention; ArrowLeft/Right step by roughly one cell's worth of percentage, PageUp/PageDown step 10, Home/End go to 0/100); `outline-none` is paired ONLY with `focus-visible:ring-2 focus-visible:ring-ns-accent`, never with `focus-visible:outline-*` on the same element. ACCESSIBILITY: `aria-valuenow` is the rounded rollout percentage; `aria-valuetext` is composed every change as e.g. 'rollout 35% — largest connected exposure 41% of traffic, above containment threshold' (or 'below') so a screen reader gets the honest readout, not just the input percentage; a permanently visible mono caption below the slider duplicates the threshold percentage, the largest-cluster percentage, and the above/below containment-threshold state in plain text at all times; a `sr-only` `aria-live=\"polite\"` region announces exactly once per genuine threshold crossing in each direction ('Crossed containment threshold — largest connected exposure now spans the lattice.' / 'Back below containment threshold.'), driven by a React effect on the above/below boolean so it never fires on every drag frame, only on an actual crossing. REDUCED MOTION: `prefers-reduced-motion` (detected via `matchMedia`, not just a CSS variant) skips the BFS stagger and disables the fill transition outright (`motion-reduce:transition-none` on every cell) — cells appear and disappear instantly, while the traced perimeter, which was never animated to begin with (it's a direct attribute set every change, not tweened), keeps drawing exactly as before. PROPS: `rows`/`cols` (lattice dimensions), `seed` (fixed PRNG seed — same seed reproduces the identical lattice and threshold), `rollout`/`defaultRollout` (controlled/uncontrolled 0-100, standard pattern), `onRolloutChange`, `segmentLabel` (noun used in readouts, default \"traffic\"), `className`. PERF: wet-set membership, cluster detection, and perimeter tracing are all pure functions of `(p, lattice)` recomputed via `useMemo` on every value change — cheap at this cell count (a few hundred cells, one union-find pass, one boundary walk) — only the capillary stagger's per-cell transition-delay is set imperatively outside React render, in a layout effect, for correct timing. DOM + SVG + CSS only, no canvas."
  },
  "type": "registry:ui"
}