{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hero-vortex-street",
  "title": "Hero Vortex Street",
  "description": "Cursor fluid: moving the pointer through a full-bleed dark field sheds a von Karman vortex street of alternating-sign vortices that drift downstream and decay, revealed by ~600 velocity-aligned tracer streaks; clicking drops a counter-rotating vortex pair that stirs the field for exactly 3 seconds.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/loud/hero-vortex-street/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\n// ---------------------------------------------------------------------------\n// VortexStreet — cursor fluid. Moving the pointer through a full-bleed dark\n// field sheds a von Karman vortex street: alternating-sign vortex blobs spawn\n// behind the cursor at speed-dependent intervals, drift downstream on a\n// gentle ambient current, and decay away over a few seconds. ~600 tracer\n// particles (Float32Array positions, zero per-frame allocation) are advected\n// every frame by the summed Lamb-Oseen velocity field of all active vortices\n// plus the ambient drift, and drawn as velocity-aligned streaks whose alpha\n// scales with speed — fast tracers get an extra blue-white glow pass.\n//\n// The shedding \"driver\" is the real pointer while it has moved within the\n// last 1.5s, and an internal slow Lissajous orbit otherwise — one mechanism\n// gives both idle ambient motion (the field is never static) and a\n// self-driving demo with no synthetic events. A real pointerdown drops a\n// standing counter-rotating vortex pair at the click point with elevated\n// strength that decays to nothing over exactly 3 seconds — a 3s stir.\n//\n// All ink is read from CSS custom properties at mount and re-read via a\n// MutationObserver on documentElement's class attribute, so both themes\n// render correctly. The rAF loop pauses on visibilitychange; the backing\n// store is dpr-clamped(2) with a zero-size guard. No interactive controls —\n// the canvas is aria-hidden decoration, matching hero-particles-webgl's precedent.\n//\n// prefers-reduced-motion: no rAF loop, no pointer tracking at all. One\n// static frame is drawn instead: streamlines integrated once through a fixed\n// four-vortex street configuration and stroked as thin curves.\n// ---------------------------------------------------------------------------\n\nexport interface VortexStreetProps {\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nconst TRACER_COUNT = 600;\nconst MAX_VORTICES = 24;\nconst CORE_R = 34; // Lamb-Oseen core radius, px\nconst AMBIENT_X = 22; // ambient drift, px/s — the field is never static\nconst AMBIENT_Y = -7;\nconst SHED_MIN_SPEED = 90; // px/s driver speed before shedding starts\nconst SHED_SPACING = 44; // px of driver travel per shed vortex\nconst SHED_GAMMA_PER_SPEED = 140; // circulation = 140 * clamped speed\nconst SHED_SPEED_LO = 120;\nconst SHED_SPEED_HI = 800;\nconst SHED_TAU = 2.2; // s, exponential strength decay of shed vortices\nconst GAMMA_PRUNE = 1500; // |Γ| below this = negligible, slot freed\nconst CLICK_GAMMA = 90000; // elevated strength for the click pair\nconst CLICK_LIFE = 3; // s, exact lifetime of a click vortex\nconst CLICK_PAIR_OFFSET = 16; // px, half-separation of the pair\nconst POINTER_HOLD = 1.5; // s of pointer recency before idle orbit resumes\nconst DRIVER_EASE = 6; // 1/s exponential smoothing toward the target\nconst STREAK_K = 0.055; // s — streak length = |v| * K (≈3–5px at drift speed)\nconst GLOW_SPEED = 150; // px/s, above this a tracer gets the glow pass\nconst TWO_PI = Math.PI * 2;\n\nfunction parseHex(raw: string): [number, number, number] | null {\n  const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(raw.trim());\n  if (!m) return null;\n  let h = m[1];\n  if (h.length === 3) h = h.split(\"\").map((c) => c + c).join(\"\");\n  const n = parseInt(h, 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\nfunction mixToWhite(c: [number, number, number], t: number): [number, number, number] {\n  return [\n    Math.round(c[0] + (255 - c[0]) * t),\n    Math.round(c[1] + (255 - c[1]) * t),\n    Math.round(c[2] + (255 - c[2]) * t),\n  ];\n}\n\nexport function VortexStreet({ className = \"\" }: VortexStreetProps) {\n  const containerRef = useRef<HTMLDivElement | null>(null);\n  const canvasRef = useRef<HTMLCanvasElement | null>(null);\n\n  useEffect(() => {\n    const container = containerRef.current;\n    const canvas = canvasRef.current;\n    if (!container || !canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    let w = 0;\n    let h = 0;\n    let raf = 0;\n    let running = false;\n    let last = 0;\n    let seeded = false;\n\n    // ---- ink, read from tokens; re-read on theme flip ---------------------\n    let fgStyle = \"#888\";\n    let mutedStyle = \"#888\";\n    let glowStyle = \"rgb(128,180,255)\";\n    let fadeStyle = \"rgba(10,10,10,0.32)\";\n    let bgStyle = \"#0a0a0a\";\n    const readInk = () => {\n      const cs = getComputedStyle(document.documentElement);\n      const fg = cs.getPropertyValue(\"--foreground\").trim();\n      const muted = cs.getPropertyValue(\"--ns-muted\").trim();\n      const bg = cs.getPropertyValue(\"--background\").trim();\n      const accent = cs.getPropertyValue(\"--ns-accent\").trim();\n      fgStyle = fg || \"#888\";\n      mutedStyle = muted || \"#888\";\n      bgStyle = bg || \"#0a0a0a\";\n      const bgRgb = parseHex(bg) ?? [10, 10, 10];\n      fadeStyle = `rgba(${bgRgb[0]},${bgRgb[1]},${bgRgb[2]},0.32)`;\n      // blue-white glow: the accent token pulled halfway toward white\n      const g = mixToWhite(parseHex(accent) ?? [0, 107, 255], 0.5);\n      glowStyle = `rgb(${g[0]},${g[1]},${g[2]})`;\n    };\n    readInk();\n\n    // ---- tracers: typed arrays, mutated in place --------------------------\n    const tx = new Float32Array(TRACER_COUNT);\n    const ty = new Float32Array(TRACER_COUNT);\n\n    // ---- vortex store: fixed capacity, slots reused -----------------------\n    const vActive = new Uint8Array(MAX_VORTICES);\n    const vx = new Float32Array(MAX_VORTICES);\n    const vy = new Float32Array(MAX_VORTICES);\n    const vg = new Float32Array(MAX_VORTICES); // signed initial circulation\n    const vBirth = new Float32Array(MAX_VORTICES); // s\n    const vKind = new Uint8Array(MAX_VORTICES); // 0 = shed, 1 = click\n    const vEff = new Float32Array(MAX_VORTICES); // per-frame effective Γ\n    const activeIdx = new Int32Array(MAX_VORTICES);\n\n    const allocSlot = (): number => {\n      let oldest = 0;\n      let oldestBirth = Infinity;\n      for (let i = 0; i < MAX_VORTICES; i++) {\n        if (!vActive[i]) return i;\n        if (vBirth[i] < oldestBirth) {\n          oldestBirth = vBirth[i];\n          oldest = i;\n        }\n      }\n      return oldest; // full: evict the oldest\n    };\n\n    const spawnVortex = (x: number, y: number, gamma: number, kind: 0 | 1, nowS: number) => {\n      const i = allocSlot();\n      vActive[i] = 1;\n      vx[i] = x;\n      vy[i] = y;\n      vg[i] = gamma;\n      vBirth[i] = nowS;\n      vKind[i] = kind;\n    };\n\n    // ---- driver: real pointer when fresh, Lissajous orbit otherwise -------\n    let pointerX = 0;\n    let pointerY = 0;\n    let lastPointerT = -Infinity; // s\n    let driverX = 0;\n    let driverY = 0;\n    let driverInit = false;\n    let shedSign = 1;\n    let travel = 0;\n\n    const lissajousX = (t: number) => w * (0.5 + 0.36 * Math.sin(0.5 * t + 1.3));\n    const lissajousY = (t: number) => h * (0.5 + 0.34 * Math.sin(0.34 * t));\n\n    // ---- per-frame step ---------------------------------------------------\n    const step = (nowS: number, dt: number) => {\n      // decay + prune vortices, compact active indices\n      let nActive = 0;\n      for (let i = 0; i < MAX_VORTICES; i++) {\n        if (!vActive[i]) continue;\n        const age = nowS - vBirth[i];\n        let eff: number;\n        if (vKind[i] === 1) {\n          // click vortex: gone at exactly CLICK_LIFE, quadratic ease-out\n          if (age >= CLICK_LIFE) {\n            vActive[i] = 0;\n            continue;\n          }\n          const k = 1 - age / CLICK_LIFE;\n          eff = vg[i] * k * k;\n        } else {\n          eff = vg[i] * Math.exp(-age / SHED_TAU);\n        }\n        if (eff > -GAMMA_PRUNE && eff < GAMMA_PRUNE) {\n          vActive[i] = 0;\n          continue;\n        }\n        vEff[i] = eff;\n        activeIdx[nActive++] = i;\n      }\n\n      // drive: ease toward pointer (if fresh) or the idle orbit\n      const usePointer = nowS - lastPointerT < POINTER_HOLD;\n      const targetX = usePointer ? pointerX : lissajousX(nowS);\n      const targetY = usePointer ? pointerY : lissajousY(nowS);\n      if (!driverInit) {\n        driverX = targetX;\n        driverY = targetY;\n        driverInit = true;\n      }\n      const ease = 1 - Math.exp(-DRIVER_EASE * dt);\n      const px = driverX;\n      const py = driverY;\n      driverX += (targetX - driverX) * ease;\n      driverY += (targetY - driverY) * ease;\n      const dvx = driverX - px;\n      const dvy = driverY - py;\n      const dist = Math.sqrt(dvx * dvx + dvy * dvy);\n      const speed = dt > 0 ? dist / dt : 0;\n\n      // shed: accumulate travel above the speed threshold, alternate sign\n      if (speed > SHED_MIN_SPEED) {\n        travel += dist;\n        if (travel >= SHED_SPACING) {\n          travel -= SHED_SPACING;\n          const inv = dist > 0 ? 1 / dist : 0;\n          const ux = dvx * inv;\n          const uy = dvy * inv;\n          const s = speed < SHED_SPEED_LO ? SHED_SPEED_LO : speed > SHED_SPEED_HI ? SHED_SPEED_HI : speed;\n          // behind the driver, offset to alternating sides — the two rows\n          // of the street\n          spawnVortex(\n            driverX - ux * 12 - uy * 10 * shedSign,\n            driverY - uy * 12 + ux * 10 * shedSign,\n            shedSign * SHED_GAMMA_PER_SPEED * s,\n            0,\n            nowS\n          );\n          shedSign = -shedSign;\n        }\n      } else {\n        travel = 0;\n      }\n\n      // paint: translucent background fill leaves short-lived trails\n      ctx.globalAlpha = 1;\n      ctx.fillStyle = fadeStyle;\n      ctx.fillRect(0, 0, w, h);\n\n      // faint rotating glyph rings hint at the invisible vortex centers\n      ctx.strokeStyle = mutedStyle;\n      ctx.lineWidth = 1;\n      for (let a = 0; a < nActive; a++) {\n        const i = activeIdx[a];\n        const eff = vEff[i];\n        const mag = eff < 0 ? -eff : eff;\n        const alpha = 0.1 * (mag > 40000 ? 1 : mag / 40000);\n        if (alpha < 0.008) continue;\n        const r = CORE_R * 0.85;\n        ctx.globalAlpha = alpha;\n        ctx.beginPath();\n        ctx.arc(vx[i], vy[i], r, 0, TWO_PI);\n        ctx.stroke();\n        const ang = (eff > 0 ? 1 : -1) * nowS * 1.4 + i * 1.7;\n        const ca = Math.cos(ang);\n        const sa = Math.sin(ang);\n        ctx.beginPath();\n        ctx.moveTo(vx[i] + ca * r, vy[i] + sa * r);\n        ctx.lineTo(vx[i] + ca * (r + 5), vy[i] + sa * (r + 5));\n        ctx.stroke();\n      }\n\n      // advect + draw tracers as velocity-aligned streaks\n      const coreR2 = CORE_R * CORE_R;\n      ctx.lineWidth = 1.1;\n      for (let p = 0; p < TRACER_COUNT; p++) {\n        const x = tx[p];\n        const y = ty[p];\n        let velX = AMBIENT_X;\n        let velY = AMBIENT_Y;\n        for (let a = 0; a < nActive; a++) {\n          const i = activeIdx[a];\n          const dx = x - vx[i];\n          const dy = y - vy[i];\n          let r2 = dx * dx + dy * dy;\n          if (r2 < 1e-6) r2 = 1e-6;\n          // Lamb-Oseen: v_theta = (Γ / 2πr)(1 − e^{−(r/rc)²}), tangential —\n          // as a multiplier on (−dy, dx) that's Γ(1 − e)/(2πr²)\n          const f = (vEff[i] * (1 - Math.exp(-r2 / coreR2))) / (TWO_PI * r2);\n          velX += -dy * f;\n          velY += dx * f;\n        }\n        let nx = x + velX * dt;\n        let ny = y + velY * dt;\n        // wrap with a small margin\n        if (nx < -6) nx = w + 6;\n        else if (nx > w + 6) nx = -6;\n        if (ny < -6) ny = h + 6;\n        else if (ny > h + 6) ny = -6;\n        tx[p] = nx;\n        ty[p] = ny;\n\n        const sp = Math.sqrt(velX * velX + velY * velY);\n        const alpha = sp * 0.003 + 0.08;\n        const x0 = nx - velX * STREAK_K;\n        const y0 = ny - velY * STREAK_K;\n        if (sp > GLOW_SPEED) {\n          // blue-white glow pass under fast streaks\n          ctx.strokeStyle = glowStyle;\n          ctx.lineWidth = 3;\n          ctx.globalAlpha = Math.min(0.4, (sp - GLOW_SPEED) * 0.0016 + 0.12);\n          ctx.beginPath();\n          ctx.moveTo(x0, y0);\n          ctx.lineTo(nx, ny);\n          ctx.stroke();\n          ctx.lineWidth = 1.1;\n        }\n        ctx.strokeStyle = fgStyle;\n        ctx.globalAlpha = alpha > 0.85 ? 0.85 : alpha;\n        ctx.beginPath();\n        ctx.moveTo(x0, y0);\n        ctx.lineTo(nx, ny);\n        ctx.stroke();\n      }\n      ctx.globalAlpha = 1;\n    };\n\n    const loop = (t: number) => {\n      const dt = Math.min((t - (last || t)) / 1000, 1 / 30);\n      last = t;\n      step(t / 1000, dt);\n      raf = requestAnimationFrame(loop);\n    };\n    const wake = () => {\n      if (!running) {\n        running = true;\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    };\n    const sleep = () => {\n      cancelAnimationFrame(raf);\n      running = false;\n    };\n\n    // ---- reduced motion: one static frame of integrated streamlines -------\n    const drawStatic = () => {\n      ctx.fillStyle = bgStyle;\n      ctx.fillRect(0, 0, w, h);\n      // fixed vortex street: four alternating vortices along the midline\n      const sx = [0.3, 0.44, 0.58, 0.72];\n      const sg = [52000, -52000, 52000, -52000];\n      const coreR2 = CORE_R * CORE_R;\n      const vel = (x: number, y: number): [number, number] => {\n        let ax = AMBIENT_X * 2.4;\n        let ay = 0;\n        for (let i = 0; i < 4; i++) {\n          const cx0 = sx[i] * w;\n          const cy0 = h * (i % 2 === 0 ? 0.44 : 0.56);\n          const dx = x - cx0;\n          const dy = y - cy0;\n          let r2 = dx * dx + dy * dy;\n          if (r2 < 1e-6) r2 = 1e-6;\n          const f = (sg[i] * (1 - Math.exp(-r2 / coreR2))) / (TWO_PI * r2);\n          ax += -dy * f;\n          ay += dx * f;\n        }\n        return [ax, ay];\n      };\n      // integrate seed points through the frozen field, stroke as curves\n      ctx.strokeStyle = fgStyle;\n      ctx.lineWidth = 1;\n      ctx.globalAlpha = 0.32;\n      const seeds = 26;\n      for (let s = 0; s < seeds; s++) {\n        let x = -4;\n        let y = (h * (s + 0.5)) / seeds;\n        ctx.beginPath();\n        ctx.moveTo(x, y);\n        for (let k = 0; k < 300; k++) {\n          const [ux, uy] = vel(x, y);\n          x += ux * 0.02;\n          y += uy * 0.02;\n          if (x < -8 || x > w + 8 || y < -8 || y > h + 8) break;\n          ctx.lineTo(x, y);\n        }\n        ctx.stroke();\n      }\n      // ring hints at the vortex centers\n      ctx.strokeStyle = mutedStyle;\n      ctx.globalAlpha = 0.3;\n      for (let i = 0; i < 4; i++) {\n        ctx.beginPath();\n        ctx.arc(sx[i] * w, h * (i % 2 === 0 ? 0.44 : 0.56), CORE_R * 0.85, 0, TWO_PI);\n        ctx.stroke();\n      }\n      ctx.globalAlpha = 1;\n    };\n\n    // ---- pointer (attached only when motion is allowed) -------------------\n    const onPointerMove = (e: PointerEvent) => {\n      const rect = container.getBoundingClientRect();\n      pointerX = e.clientX - rect.left;\n      pointerY = e.clientY - rect.top;\n      lastPointerT = performance.now() / 1000;\n    };\n    const onPointerDown = (e: PointerEvent) => {\n      const rect = container.getBoundingClientRect();\n      const cx = e.clientX - rect.left;\n      const cy = e.clientY - rect.top;\n      const nowS = performance.now() / 1000;\n      // standing counter-rotating pair — stirs the field for 3s\n      spawnVortex(cx - CLICK_PAIR_OFFSET, cy, CLICK_GAMMA, 1, nowS);\n      spawnVortex(cx + CLICK_PAIR_OFFSET, cy, -CLICK_GAMMA, 1, nowS);\n      pointerX = cx;\n      pointerY = cy;\n      lastPointerT = nowS;\n    };\n\n    // ---- mode plumbing ----------------------------------------------------\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    let reduced = mq.matches;\n    let listening = false;\n    const applyMode = () => {\n      if (reduced) {\n        sleep();\n        if (listening) {\n          container.removeEventListener(\"pointermove\", onPointerMove);\n          container.removeEventListener(\"pointerdown\", onPointerDown);\n          listening = false;\n        }\n        if (w > 0 && h > 0) drawStatic();\n      } else {\n        if (!listening) {\n          container.addEventListener(\"pointermove\", onPointerMove);\n          container.addEventListener(\"pointerdown\", onPointerDown);\n          listening = true;\n        }\n        if (w > 0 && h > 0) wake();\n      }\n    };\n    const onMq = () => {\n      reduced = mq.matches;\n      applyMode();\n    };\n    mq.addEventListener(\"change\", onMq);\n\n    const resize = () => {\n      const rect = container.getBoundingClientRect();\n      if (rect.width < 4 || rect.height < 4) return; // zero-size guard\n      w = rect.width;\n      h = rect.height;\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      canvas.width = Math.round(w * dpr);\n      canvas.height = Math.round(h * dpr);\n      canvas.style.width = `${w}px`;\n      canvas.style.height = `${h}px`;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      if (!seeded) {\n        for (let p = 0; p < TRACER_COUNT; p++) {\n          tx[p] = Math.random() * w;\n          ty[p] = Math.random() * h;\n        }\n        seeded = true;\n      }\n      // resizing clears the canvas — repaint an opaque base so trails don't\n      // start from transparency\n      ctx.fillStyle = bgStyle;\n      ctx.fillRect(0, 0, w, h);\n      applyMode();\n    };\n    const ro = new ResizeObserver(resize);\n    ro.observe(container);\n    resize();\n\n    const themeObserver = new MutationObserver(() => {\n      readInk();\n      if (w <= 0 || h <= 0) return;\n      // flush old-theme trail pixels immediately\n      ctx.fillStyle = bgStyle;\n      ctx.fillRect(0, 0, w, h);\n      if (reduced) drawStatic();\n    });\n    themeObserver.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    const onVis = () => {\n      if (document.hidden) sleep();\n      else if (!reduced) wake();\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    return () => {\n      ro.disconnect();\n      themeObserver.disconnect();\n      mq.removeEventListener(\"change\", onMq);\n      document.removeEventListener(\"visibilitychange\", onVis);\n      if (listening) {\n        container.removeEventListener(\"pointermove\", onPointerMove);\n        container.removeEventListener(\"pointerdown\", onPointerDown);\n      }\n      sleep();\n    };\n  }, []);\n\n  return (\n    <div\n      ref={containerRef}\n      className={`relative h-full w-full overflow-hidden bg-background ${className}`}\n    >\n      <canvas\n        ref={canvasRef}\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-0\"\n      />\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/hero-vortex-street.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": "loud",
    "tags": [
      "fluid",
      "vortex",
      "cursor",
      "canvas",
      "particles",
      "physics",
      "hero",
      "decorative"
    ],
    "instruction": "Build a full-bleed decorative Canvas 2D fluid field with zero interactive controls (canvas aria-hidden, pointer-events-none on the canvas itself with pointer handlers on its container — the direct analogue of a cursor-reactive particle hero). Backing store is dpr-clamped to 2 with a ResizeObserver-driven resize and a zero-size guard (skip when the container is under 4px). SIMULATION: 600 tracer particles live in two Float32Arrays (x and y) mutated in place — no allocation, .map, .filter, or object creation anywhere in the per-frame path. Each frame every tracer is advected by dt (clamped to 1/30 s) under the SUM of (a) a constant gentle ambient drift of (22, −7) px/s so the field is never static even with zero vortices, and (b) the tangential Lamb-Oseen velocity of every active vortex: v_theta = (Γ / 2πr)(1 − exp(−(r/r_core)²)) with r_core = 34 px, applied as the multiplier Γ(1 − exp(−r²/r_core²))/(2πr²) on the perpendicular vector (−dy, dx) from vortex center to tracer (regular at r→0, guard r² with 1e-6). Tracers wrap at the edges with a 6 px margin. VORTEX STORE: fixed capacity 24 in parallel typed arrays (x, y, signed initial Γ, birth time, kind flag, active flag, plus a per-frame effective-Γ scratch array and a compacted active-index Int32Array); slot allocation reuses any inactive slot else evicts the oldest. Shed vortices decay as Γ·exp(−age/2.2 s) and are pruned when |Γ_eff| < 1500; click vortices decay as Γ·(1 − age/3)² and are removed at exactly age = 3 s. SHEDDING DRIVER: a driver position eases exponentially (rate 6 s⁻¹, factor 1 − exp(−6·dt)) toward the real pointer position if the pointer moved within the last 1.5 s, otherwise toward an internal slow Lissajous orbit x = w(0.5 + 0.36·sin(0.5t + 1.3)), y = h(0.5 + 0.34·sin(0.34t)) — this single mechanism gives idle ambient shedding (self-driving demo, no synthetic events) and seamless pointer takeover. When driver speed exceeds 90 px/s, accumulate distance traveled; every 44 px spawn one vortex with ALTERNATING sign, Γ = ±140 × speed (speed clamped to 120–800 px/s), positioned 12 px behind the driver along −velocity plus 10 px perpendicular offset to the alternating side, producing the characteristic two-row zig-zag street. CLICK: a real pointerdown spawns a standing counter-rotating pair at the click point, ±16 px apart horizontally, Γ = ±90000 (elevated), gone after exactly 3 s. RENDER: each frame first fill the whole canvas with the --background token at alpha 0.32 (short-lived motion trails), then draw faint rotating glyph rings on each active vortex (a 1px --ns-muted circle of radius 0.85·r_core plus a 5 px radial tick rotating at 1.4 rad/s signed by rotation direction, alpha 0.1 scaled by |Γ_eff|/40000, capped), then draw every tracer as a streak from (x − vx·k, y − vy·k) to (x, y) with k = 0.055 s in --foreground, alpha = 0.08 + speed·0.003 capped at 0.85; tracers faster than 150 px/s first get a glow under-stroke 3 px wide in a blue-white derived by mixing the --ns-accent token 50% toward white, alpha 0.12 + (speed − 150)·0.0016 capped at 0.4. ALL canvas ink comes from CSS custom properties read via getComputedStyle(document.documentElement) at mount and re-read through a MutationObserver on documentElement's class attribute (repaint an opaque background flush on theme flip so old-theme trail pixels vanish). The rAF loop pauses on visibilitychange. REDUCED MOTION (matchMedia, tracked live): no rAF loop and no pointer listeners at all — render ONE static frame: 26 seed points spaced down the left edge, each integrated 300 Euler steps of 0.02 s through a frozen field of four alternating ±52000 vortices at x = 0.30/0.44/0.58/0.72 of width alternating between 0.44 h and 0.56 h (ambient boosted 2.4×), stroked as thin 1 px --foreground curves at alpha 0.32 with --ns-muted ring hints at the four centers. No SVG, no dependencies."
  },
  "type": "registry:ui"
}