{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "background-ascii-voronoi-walls",
  "title": "Background ASCII Voronoi Walls",
  "description": "An ambient ASCII background of Voronoi cell WALLS only: ink appears exactly where the first and second nearest sites are equidistant, so a sparse, slowly drifting network of hairlines and clean three-way junctions sits on an otherwise empty field, and the pointer is a weighted site whose territory visibly swells.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/background-ascii-voronoi-walls/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\n// ---------------------------------------------------------------------------\n// VoronoiWalls — an ambient ASCII background that draws a Voronoi diagram's\n// WALLS ONLY. For every grid cell the nearest and second-nearest site distances\n// d1 <= d2 are found by a plain linear scan, and ink is placed only where the\n// NORMALIZED difference w = (d2 - d1) / (d2 + d1) falls below a small\n// threshold — i.e. exactly where the two closest sites are near-equidistant.\n// The normalization is the point: raw (d2 - d1) would make walls near the\n// sites hairline and walls far away fat, whereas w is scale-free, so every\n// boundary in the frame is the same weight. Cell interiors have w near 1 and\n// draw nothing, so most of the field is empty by construction and what is left\n// is a network of hairlines meeting at clean three-way junctions — three sites\n// equidistant is a Y-junction, which is why foam and cracked mud look like\n// this. Measured coverage is ~10% of cells, stable across drift and frame\n// size. The pointer joins the diagram as an EXTRA SITE WITH A POWER WEIGHT\n// (a Laguerre / power Voronoi cell): its distance is measured as the tangent\n// length sqrt(d^2 - r^2) rather than d, with r^2 easing up while the pointer\n// is over the canvas. A positive weight makes its cell OWN MORE TERRITORY, so\n// the surrounding walls bow convexly away and the neighbours are squeezed —\n// a bubble inflating in a foam. On leave r^2 eases back to 0, the cell\n// collapses, and the walls spring back to the undisturbed diagram.\n// ---------------------------------------------------------------------------\n\nconst RAMP = \" .:-=+*#%@\";\nconst ALPHA_BUCKETS = 6;\nconst WALL_W = 0.055; // normalized (d2-d1)/(d2+d1) below which a cell is inked\nconst WALL_POW = 1.5; // luminance falloff across the wall's width\nconst JUNCTION_BOOST = 1.6; // triple points read a touch brighter, as in a foam\nconst DRIFT_AMP = 0.045; // * min(W,H) — lissajous amplitude per site\nconst DRIFT_MIN = 0.06; // rad/s\nconst DRIFT_MAX = 0.17; // rad/s\nconst MIN_SEP = 0.16; // * min(W,H) — poisson-disk rejection radius\nconst POINTER_R = 0.1; // * min(W,H) — max power radius of the pointer's cell\nconst POINTER_TAU = 0.5; // s — weight ease time constant, both directions\nconst DT_MAX = 0.05;\nconst SEED = 0x5eed1a;\n\n/** deterministic PRNG so the site layout is stable for a given size */\nfunction mulberry32(a: number) {\n  return () => {\n    a |= 0;\n    a = (a + 0x6d2b79f5) | 0;\n    let t = Math.imul(a ^ (a >>> 15), 1 | a);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nexport interface VoronoiWallsProps {\n  /** grid cell size in px */\n  cellSize?: number;\n  /** base number of drifting sites, scaled by frame area and clamped 14..34 */\n  siteCount?: number;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nexport function VoronoiWalls({\n  cellSize = 12,\n  siteCount = 22,\n  className = \"\",\n}: VoronoiWallsProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const reduced = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches;\n\n    let fg = \"currentColor\";\n    let cellW = cellSize;\n    let cellH = cellSize;\n    let cols = 0;\n    let rows = 0;\n    let width = 0;\n    let height = 0;\n    let sized = false;\n    let ready = false;\n    let disposed = false;\n\n    let charBuf = new Uint8Array(0);\n    const bucketLists: number[][] = Array.from(\n      { length: ALPHA_BUCKETS },\n      () => []\n    );\n\n    // site fields: base position, lissajous frequency and phase\n    let n = 0;\n    let sx0 = new Float32Array(0);\n    let sy0 = new Float32Array(0);\n    let swx = new Float32Array(0);\n    let swy = new Float32Array(0);\n    let spx = new Float32Array(0);\n    let spy = new Float32Array(0);\n    let amp = 0;\n    // live positions, plus one trailing slot for the pointer site\n    let px = new Float32Array(0);\n    let py = new Float32Array(0);\n\n    const readTokens = () => {\n      fg = getComputedStyle(canvas).color;\n    };\n\n    const measureCell = (fontFamily: string) => {\n      const off = document.createElement(\"canvas\");\n      const octx = off.getContext(\"2d\");\n      if (!octx) return;\n      octx.font = `${cellSize}px ${fontFamily}`;\n      cellW = Math.max(4, octx.measureText(\"MMMMMMMMMM\").width / 10);\n      cellH = cellSize;\n    };\n\n    /** poisson-disk rejection sampling — no two sites closer than minSep */\n    const seedSites = () => {\n      const m = Math.min(width, height);\n      const scale = Math.sqrt((width * height) / (1280 * 720));\n      n = Math.max(14, Math.min(34, Math.round(siteCount * scale)));\n      amp = DRIFT_AMP * m;\n      sx0 = new Float32Array(n);\n      sy0 = new Float32Array(n);\n      swx = new Float32Array(n);\n      swy = new Float32Array(n);\n      spx = new Float32Array(n);\n      spy = new Float32Array(n);\n      px = new Float32Array(n + 1);\n      py = new Float32Array(n + 1);\n\n      const rng = mulberry32(SEED);\n      let sep = MIN_SEP * m;\n      const pad = -amp * 0.5; // let sites drift a little off-frame\n      let placed = 0;\n      let fails = 0;\n      while (placed < n) {\n        const cx = pad + rng() * (width - 2 * pad);\n        const cy = pad + rng() * (height - 2 * pad);\n        let ok = true;\n        for (let i = 0; i < placed; i++) {\n          const dx = cx - sx0[i]!;\n          const dy = cy - sy0[i]!;\n          if (dx * dx + dy * dy < sep * sep) {\n            ok = false;\n            break;\n          }\n        }\n        if (!ok) {\n          // relax the separation rather than spin forever in a tight frame\n          if (++fails > 220) {\n            sep *= 0.9;\n            fails = 0;\n          }\n          continue;\n        }\n        sx0[placed] = cx;\n        sy0[placed] = cy;\n        swx[placed] = DRIFT_MIN + rng() * (DRIFT_MAX - DRIFT_MIN);\n        swy[placed] = DRIFT_MIN + rng() * (DRIFT_MAX - DRIFT_MIN);\n        spx[placed] = rng() * Math.PI * 2;\n        spy[placed] = rng() * Math.PI * 2;\n        placed++;\n      }\n    };\n\n    const resize = () => {\n      const rect = canvas.getBoundingClientRect();\n      if (rect.width < 2 || rect.height < 2) {\n        sized = false;\n        return;\n      }\n      width = rect.width;\n      height = rect.height;\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      canvas.width = Math.max(1, Math.round(width * dpr));\n      canvas.height = Math.max(1, Math.round(height * dpr));\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      const fontFamily = getComputedStyle(canvas).fontFamily;\n      measureCell(fontFamily);\n      ctx.font = `${cellSize}px ${fontFamily}`;\n      ctx.textAlign = \"center\";\n      ctx.textBaseline = \"middle\";\n      cols = Math.max(4, Math.ceil(width / cellW));\n      rows = Math.max(4, Math.ceil(height / cellH));\n      charBuf = new Uint8Array(cols * rows);\n      seedSites();\n      sized = true;\n    };\n\n    let resizeTimer: ReturnType<typeof setTimeout> | null = null;\n    const onResize = () => {\n      if (resizeTimer) clearTimeout(resizeTimer);\n      resizeTimer = setTimeout(() => {\n        resizeTimer = null;\n        resize();\n        if (reduced) draw(0, 0, 0, 0);\n      }, 150);\n    };\n\n    const draw = (t: number, pxc: number, pyc: number, pr2: number) => {\n      if (!sized) return;\n      ctx.clearRect(0, 0, width, height);\n      for (let b = 0; b < ALPHA_BUCKETS; b++) bucketLists[b]!.length = 0;\n\n      // advance every site along its own slow lissajous\n      for (let i = 0; i < n; i++) {\n        px[i] = sx0[i]! + amp * Math.sin(t * swx[i]! + spx[i]!);\n        py[i] = sy0[i]! + amp * Math.cos(t * swy[i]! + spy[i]!);\n      }\n      const hasPointer = pr2 > 1;\n      const count = hasPointer ? n + 1 : n;\n      if (hasPointer) {\n        px[n] = pxc;\n        py[n] = pyc;\n      }\n\n      let i = 0;\n      for (let gy = 0; gy < rows; gy++) {\n        const y = gy * cellH + cellH / 2;\n        for (let gx = 0; gx < cols; gx++, i++) {\n          const x = gx * cellW + cellW / 2;\n          // three smallest effective distances by linear scan. Ordering is\n          // done on the SQUARED quantity and only the three winners are\n          // square-rooted, which keeps the scan at one sqrt-free inner step.\n          let q1 = Infinity;\n          let q2 = Infinity;\n          let q3 = Infinity;\n          for (let s = 0; s < count; s++) {\n            const dx = x - px[s]!;\n            const dy = y - py[s]!;\n            let q = dx * dx + dy * dy;\n            // power (Laguerre) distance for the weighted pointer site: the\n            // squared tangent length to its circle, so the bisector is the\n            // radical axis and the cell genuinely grows with the weight\n            if (hasPointer && s === n) q = q > pr2 ? q - pr2 : 0;\n            if (q < q1) {\n              q3 = q2;\n              q2 = q1;\n              q1 = q;\n            } else if (q < q2) {\n              q3 = q2;\n              q2 = q;\n            } else if (q < q3) {\n              q3 = q;\n            }\n          }\n          const d1 = Math.sqrt(q1);\n          const d2 = Math.sqrt(q2);\n          const d3 = Math.sqrt(q3);\n\n          const sum = d1 + d2;\n          const w = sum > 1e-6 ? (d2 - d1) / sum : 1;\n          let v = 0;\n          if (w < WALL_W) {\n            v = Math.pow(1 - w / WALL_W, WALL_POW);\n            const sum3 = d1 + d3;\n            if (sum3 > 1e-6 && (d3 - d1) / sum3 < WALL_W) {\n              v = Math.min(1, v * JUNCTION_BOOST);\n            }\n          }\n\n          const ci = Math.floor(v * (RAMP.length - 1));\n          charBuf[i] = ci;\n          if (ci !== 0) {\n            const bucket = Math.min(\n              ALPHA_BUCKETS - 1,\n              Math.floor(v * ALPHA_BUCKETS)\n            );\n            bucketLists[bucket]!.push(i);\n          }\n        }\n      }\n\n      ctx.fillStyle = fg;\n      for (let b = 0; b < ALPHA_BUCKETS; b++) {\n        const list = bucketLists[b]!;\n        if (list.length === 0) continue;\n        ctx.globalAlpha = 0.14 + (b / (ALPHA_BUCKETS - 1)) * 0.86;\n        for (let k = 0; k < list.length; k++) {\n          const idx = list[k]!;\n          const gx = idx % cols;\n          const gy = (idx - gx) / cols;\n          ctx.fillText(\n            RAMP[charBuf[idx]!]!,\n            gx * cellW + cellW / 2,\n            gy * cellH + cellH / 2\n          );\n        }\n      }\n      ctx.globalAlpha = 1;\n    };\n\n    // -- hot-path state -------------------------------------------------------\n    let raf = 0;\n    let last = 0;\n    let t = 0;\n    const ptr = { x: 0, y: 0, has: false, r2: 0 };\n\n    const loop = (now: number) => {\n      const dt = last ? Math.min(DT_MAX, (now - last) / 1000) : 1 / 60;\n      last = now;\n      t += dt;\n      const m = Math.min(width, height);\n      const target = ptr.has ? (POINTER_R * m) ** 2 : 0;\n      ptr.r2 += (target - ptr.r2) * Math.min(1, dt / POINTER_TAU);\n      draw(t, ptr.x, ptr.y, ptr.r2);\n      if (!document.hidden) raf = requestAnimationFrame(loop);\n    };\n\n    const onPointerMove = (e: PointerEvent) => {\n      const rect = canvas.getBoundingClientRect();\n      ptr.x = e.clientX - rect.left;\n      ptr.y = e.clientY - rect.top;\n      ptr.has = true;\n    };\n    const onPointerLeave = () => {\n      ptr.has = false;\n    };\n\n    const onVis = () => {\n      if (!document.hidden && !reduced && ready) {\n        // a frame requested while the tab was hidden is still queued; drop it\n        // so resuming never leaves two loops running at double speed\n        cancelAnimationFrame(raf);\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    };\n\n    const mo = new MutationObserver(() => {\n      readTokens();\n      if (reduced) draw(0, 0, 0, 0);\n    });\n    mo.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    document.fonts.ready.then(() => {\n      if (disposed) return;\n      readTokens();\n      resize();\n      ready = true;\n      if (reduced) {\n        draw(0, 0, 0, 0);\n      } else {\n        raf = requestAnimationFrame(loop);\n      }\n    });\n\n    window.addEventListener(\"resize\", onResize);\n    if (!reduced) {\n      canvas.addEventListener(\"pointermove\", onPointerMove);\n      canvas.addEventListener(\"pointerleave\", onPointerLeave);\n    }\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    return () => {\n      disposed = true;\n      cancelAnimationFrame(raf);\n      if (resizeTimer) clearTimeout(resizeTimer);\n      mo.disconnect();\n      window.removeEventListener(\"resize\", onResize);\n      canvas.removeEventListener(\"pointermove\", onPointerMove);\n      canvas.removeEventListener(\"pointerleave\", onPointerLeave);\n      document.removeEventListener(\"visibilitychange\", onVis);\n    };\n  }, [cellSize, siteCount]);\n\n  return (\n    <canvas\n      ref={canvasRef}\n      aria-hidden\n      className={`block h-full w-full font-mono text-foreground ${className}`}\n    />\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/background-ascii-voronoi-walls.tsx"
    }
  ],
  "meta": {
    "collection": "core",
    "tags": [
      "background",
      "ascii",
      "canvas",
      "cursor",
      "voronoi",
      "tessellation",
      "boundary"
    ],
    "instruction": "Build <VoronoiWalls cellSize? siteCount? className?> as a full-bleed <canvas>. SITES: siteCount sites (default 22) scaled by sqrt(area / (1280*720)) and clamped to 14..34, positioned once per resize by mulberry32-seeded (seed 0x5eed1a) Poisson-disk rejection sampling at a minimum separation of 0.16 * min(W,H) so no two walls ever crowd each other; the rejection radius relaxes by 10% after 220 consecutive failures so a narrow frame terminates instead of spinning. Each site then drifts on its own slow Lissajous, p = p0 + A * [sin(t*wx + px), cos(t*wy + py)], with A = 0.045 * min(W,H) and wx, wy drawn per site from [0.06, 0.17] rad/s — a calm resting drift whose only ambient event is the topology occasionally flipping as two sites pass each other and a wall reconnects. FIELD: per grid cell the nearest, second-nearest and third-nearest site distances d1 <= d2 <= d3 are found by a plain linear scan over the site list (22 sites x cols*rows is trivial at cellSize 12) — no Delaunay, no library, no dependency; ordering is done on the squared quantity and only the three winners are square-rooted. WALL FUNCTION, which is the sparsity mechanism and must be the NORMALIZED difference rather than raw d2 - d1 (raw would make walls near a site hairline and walls far away fat, so thickness would vary wildly across the frame): w = (d2 - d1) / (d2 + d1), ink only where w < 0.055, luminance = pow(1 - w/0.055, 1.5). Cell interiors sit at w near 1 and draw absolutely nothing, so roughly 90% of the frame is empty by construction (measured 10.1% inked at 1280x720, stable across drift and across frame sizes from a 420x300 card to 2560x1400) and what remains is a network of hairlines meeting at clean triple junctions — three sites equidistant is exactly a Y-junction, which is why foam, cracked mud and a territory map all look like this. JUNCTION EMPHASIS: the third-nearest distance is carried through the same scan, and if (d3 - d1) / (d3 + d1) is also below 0.055 the luminance is multiplied by 1.6 and clamped to 1, so triple points read a touch brighter exactly as they do in a real foam. The sites themselves are never drawn as dots; only the walls, which is what keeps the read honest. POINTER: the pointer joins the diagram as an EXTRA SITE WITH A POWER WEIGHT — a Laguerre (power) Voronoi cell, which is the physically correct way for one site to own more territory than its neighbours. Its distance is measured as the tangent length sqrt(d^2 - r^2) instead of d, with r^2 eased from 0 toward (0.10 * min(W,H))^2 with a 0.5s time constant while the pointer is over the canvas. A positive weight moves every bisector it touches from a perpendicular line to a radical axis, so the surrounding walls bow convexly AWAY from the cursor and the neighbouring cells are visibly squeezed — a bubble inflating in a foam, not a stamp or a local recolor. On pointerleave r^2 eases back to 0 over the same time constant, the pointer's cell collapses to nothing, and the walls spring back to the undisturbed diagram. Direct-DOM rAF, zero React state on the hot path. Rendering is two-pass: pass one evaluates every cell into a Uint8Array ramp-index buffer and buckets each inked cell by luminance into one of 6 alpha buckets; pass two sets ctx.globalAlpha once per bucket and draws only that bucket's cells from the shared ' .:-=+*#%@' density ramp — never one globalAlpha write per cell. Ink is read once via getComputedStyle(canvas).color and re-derived on a documentElement class MutationObserver so theme flips are live in both light and dark; no colour literal anywhere. Mono cell width is measured from an offscreen canvas's measureText('MMMMMMMMMM')/10, and resize is debounced 150ms with the site layout reseeded deterministically. prefers-reduced-motion renders exactly one static frame at t = 0 with the pointer weight at 0, and skips the rAF loop and the pointer listeners entirely. The loop pauses on document.hidden and resumes on visibilitychange. The canvas is aria-hidden — it is decoration, and carries no content a screen reader should announce. Props: cellSize (grid cell px, default 12), siteCount (base site count before area scaling and the 14..34 clamp, default 22), className."
  },
  "type": "registry:ui"
}