{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "spiral-chute-accrete",
  "title": "Spiral Chute Accrete",
  "description": "A full-bleed ambient background reproducing a gravity spiral chute, the helical slide used to move bulk parcels/mail sacks between floors in sorting facilities and department-store stockrooms without powered conveyance. Parcels drop in at the top, descend the helix in continuous view, and pile up in a landing area at the base until the pile is periodically swept clear.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/loud/spiral-chute-accrete/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport type { ReactNode } from \"react\";\n\n// ---------------------------------------------------------------------------\n// SpiralChuteAccrete — a full-bleed ambient background reproducing a gravity\n// spiral chute: the helical slide sorting facilities and department-store\n// stockrooms use to move parcels/mail sacks between floors without powered\n// conveyance. Small parcels enter at the top, slide down a continuous\n// helical ramp under gravity alone (friction + the helix's banking keeps\n// speed roughly bounded rather than accelerating), and pile up in a landing\n// area at the base until the pile is swept clear.\n//\n// EVERYTHING IS A PURE FUNCTION OF ONE CLOCK. Rather than mutating an array\n// of live parcel/tile objects frame to frame, every quantity drawn — which\n// parcels are in flight, where each sits on the helix, which tiles are in\n// the current pile, which are mid-sweep-out — is derived straight from the\n// elapsed clock t (ms) and a fixed spawn index k. That is what makes the\n// state reproducible for prefers-reduced-motion: freezing the clock at a\n// named STATIC_T freezes the exact same layout every time, with no history\n// to serialize.\n//\n//   spawn(k)   = k * SPAWN_INTERVAL_MS                      (top of chute)\n//   land(k)    = spawn(k) + TRANSIT_MS                      (base of chute)\n//   in flight  = spawn(k) <= t < land(k)   -> theta = (t-spawn(k))/TRANSIT_MS * THETA_MAX\n//   landed     = land(k) <= t              -> becomes one pile tile\n//\n// PILE / SWEEP: the base landing area fills with one tile per landed\n// parcel, positioned by a period-9 deterministic jitter sequence keyed off\n// k % 9 (never per-frame randomness, so the pile shape is exactly\n// reproducible). Every SWEEP_PERIOD_MS the chute's current 9s fill cycle\n// ends and the NEXT cycle's landings start a fresh pile; the tiles that\n// belong to the cycle which just ended slide off to one side over\n// SWEEP_DURATION_MS, staggered SWEEP_STAGGER_MS apart, for the first\n// SWEEP_DURATION_MS of the new cycle only. This is derived directly from\n// `t % SWEEP_PERIOD_MS`, so a pile can never accumulate past one fill\n// cycle's worth of tiles and never breaches the canvas edge.\n//\n// REAL NUMBERS (all fixed, real-world-derived, not tuned per frame): a\n// parcel takes TRANSIT_MS to cross THE FULL 3.5 turns of the helix — this\n// is one of the rare cases where the legible rate and the real rate are\n// close, so descent renders close to real proportion instead of being\n// artificially decoupled. New parcels spawn every SPAWN_INTERVAL_MS, which\n// at a 3.6s transit keeps 2-3 parcels in flight simultaneously, each at a\n// different theta so they never overlap on the same winding — this\n// simultaneous-parcel count, not a faster descent, is the intended legible\n// signal for \"alive,\" per the spec's own kill criteria.\n//\n// TOKENS: the ramp track itself is the least important thing on screen —\n// it renders at --ns-muted, a low, border-like contrast step, so it reads\n// as track rather than subject. Parcels and pile tiles are the moving\n// subject and render at --foreground so they clear the ramp with a real\n// luminance step in both themes. --ns-accent never appears; nothing here\n// is interaction chrome, and the pile-sweep moment (this component's one\n// climactic event) is a --foreground fade/slide only.\n// ---------------------------------------------------------------------------\n\nconst TRANSIT_MS = 3600; // one parcel's full top-to-bottom transit\nconst SPAWN_INTERVAL_MS = 1300; // cadence of new parcels entering at the top\nconst TURNS = 3.5; // total revolutions top -> bottom\nconst THETA_MAX = TURNS * Math.PI * 2;\nconst SWEEP_PERIOD_MS = 9000; // pile fill/clear cycle length\nconst SWEEP_DURATION_MS = 500; // how long a sweep-out takes\nconst SWEEP_STAGGER_MS = 30; // stagger between tiles sliding away\nconst RADIUS_FACTOR = 0.32; // spiral radius, as a fraction of min(w,h)\nconst RAMP_TOP_FRAC = 0.1; // top margin, fraction of container height\nconst RAMP_HEIGHT_FRAC = 0.68; // vertical extent of the helix, fraction of height\nconst RAMP_SAMPLES = 240; // points used to build the cached ramp path\nconst PARCEL_SIZE_FRAC = 0.05; // parcel edge length, fraction of min(w,h)\nconst PILE_JITTER_FRAC = 0.075; // pile scatter radius, fraction of min(w,h)\n\n// deterministic period-9 jitter sequence (unit-ish offsets), keyed by\n// spawn index k % 9 — fixed, not per-frame random, so the pile shape is\n// reproducible frame to frame and across prefers-reduced-motion runs.\nconst PILE_OFFSETS: readonly [number, number][] = [\n  [0.12, -0.22],\n  [-0.38, 0.14],\n  [0.44, 0.3],\n  [-0.14, -0.42],\n  [0.05, 0.42],\n  [-0.46, -0.08],\n  [0.26, -0.36],\n  [-0.2, 0.36],\n  [0.4, 0.04],\n];\n\n// prefers-reduced-motion freeze frame: named MID_DESCENT. Chosen mid-cycle\n// (71.7% into a 9s fill) so the pile is roughly half of a full cycle's\n// worth of tiles (3 landed of ~5 max) rather than freshly swept or about\n// to overflow, with two parcels still in flight at clearly different theta\n// — spiral, motion-implying spacing and an in-progress pile all visible\n// in one static composition.\nconst STATIC_T = 6450;\n\n// live-mode warm start: t=0 on a bare clock is a near-empty chute (one\n// parcel just spawned, pile empty) — the spec's own t0 requirement is\n// \"2-3 parcels visible at different points on the spiral, plus a partial\n// pile.\" Starting the live clock here instead of at 0 makes t0 already\n// alive without changing any rate: k=2/k=3 in flight (theta .71/.35),\n// pile at 2 tiles. Deliberately a different phase than STATIC_T so the\n// reduced-motion freeze is never mistaken for \"just t0.\"\nconst WARM_START_MS = 5150;\n\nfunction landTimeForIndex(k: number): number {\n  return k * SPAWN_INTERVAL_MS + TRANSIT_MS;\n}\n\ninterface FlightParcel {\n  k: number;\n  progress: number; // 0..1 along the helix\n}\n\ninterface PileTile {\n  k: number;\n  sweepProgress: number; // 0 = resting in pile, 1 = fully slid away\n}\n\n/** Parcels currently in flight (spawned, not yet landed) at clock t. */\nfunction computeFlight(t: number): FlightParcel[] {\n  const out: FlightParcel[] = [];\n  const maxK = Math.floor(t / SPAWN_INTERVAL_MS);\n  for (let k = Math.max(0, maxK - 4); k <= maxK; k++) {\n    const spawnAt = k * SPAWN_INTERVAL_MS;\n    if (spawnAt < 0 || spawnAt > t) continue;\n    const landAt = landTimeForIndex(k);\n    if (t >= landAt) continue;\n    out.push({ k, progress: (t - spawnAt) / TRANSIT_MS });\n  }\n  return out;\n}\n\n/**\n * The pile as it should render at clock t: tiles landed in the CURRENT\n * fill cycle (sweepProgress 0, resting), plus — for the first\n * SWEEP_DURATION_MS of a new cycle only — the previous cycle's tiles,\n * staggered mid-slide-away.\n */\nfunction computePile(t: number): PileTile[] {\n  if (t < 0) return [];\n  const cycleIndex = Math.floor(t / SWEEP_PERIOD_MS);\n  const cycleStart = cycleIndex * SWEEP_PERIOD_MS;\n  const sinceCycleStart = t - cycleStart;\n\n  const out: PileTile[] = [];\n\n  // current cycle's own landings so far\n  const currentMaxK = Math.floor((t - TRANSIT_MS) / SPAWN_INTERVAL_MS);\n  for (let k = Math.max(0, currentMaxK - 8); k <= currentMaxK; k++) {\n    const landAt = landTimeForIndex(k);\n    if (landAt > cycleStart && landAt <= t) {\n      out.push({ k, sweepProgress: 0 });\n    }\n  }\n\n  // previous cycle's tiles, still sliding away in the first\n  // SWEEP_DURATION_MS of this cycle\n  if (cycleIndex > 0 && sinceCycleStart < SWEEP_DURATION_MS) {\n    const prevCycleStart = cycleStart - SWEEP_PERIOD_MS;\n    const prevMaxK = Math.floor((cycleStart - TRANSIT_MS) / SPAWN_INTERVAL_MS);\n    const prevTiles: number[] = [];\n    for (let k = Math.max(0, prevMaxK - 8); k <= prevMaxK; k++) {\n      const landAt = landTimeForIndex(k);\n      if (landAt > prevCycleStart && landAt <= cycleStart) prevTiles.push(k);\n    }\n    prevTiles.sort((a, b) => a - b);\n    prevTiles.forEach((k, i) => {\n      const localStart = i * SWEEP_STAGGER_MS;\n      const local = sinceCycleStart - localStart;\n      const span = SWEEP_DURATION_MS - localStart;\n      const p = span <= 0 ? 1 : Math.max(0, Math.min(1, local / span));\n      if (p < 1) out.push({ k, sweepProgress: p });\n    });\n  }\n\n  return out;\n}\n\nfunction easeOut(p: number): number {\n  return 1 - (1 - p) * (1 - p);\n}\n\nexport interface SpiralChuteAccreteProps {\n  /** headline / CTA rendered centered over the chute */\n  children?: ReactNode;\n  /** extra classes merged onto the root element */\n  className?: string;\n  /** inline styles merged onto the root element */\n  style?: React.CSSProperties;\n}\n\nexport function SpiralChuteAccrete({\n  children,\n  className = \"\",\n  style,\n}: SpiralChuteAccreteProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n\n  useEffect(() => {\n    const root = rootRef.current;\n    const canvas = canvasRef.current;\n    if (!root || !canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n    // token fields start empty; nothing paints until readTokens() has run\n    // at least once (guarded in draw() below) — closes every path (rAF\n    // start, resize, IntersectionObserver resume) that could otherwise\n    // paint a literal color before the first token read.\n    let mutedColor = \"\";\n    let fgColor = \"\";\n    const readTokens = () => {\n      const cs = getComputedStyle(document.documentElement);\n      mutedColor = cs.getPropertyValue(\"--ns-muted\").trim();\n      fgColor = cs.getPropertyValue(\"--foreground\").trim();\n    };\n\n    let w = 0;\n    let h = 0;\n    let cx = 0;\n    let radius = 0;\n    let topY = 0;\n    let rampHeight = 0;\n    let baseX = 0;\n    let baseY = 0;\n    let parcelSize = 0;\n    let pileJitter = 0;\n    let sized = false;\n    let visible = true;\n    let raf = 0;\n    let last = 0;\n    let t = reduced ? STATIC_T : WARM_START_MS;\n\n    // cached ramp path samples, rebuilt only on resize (geometry never\n    // changes per frame — only scale/position do).\n    let rampPath: Path2D | null = null;\n\n    const pointOnHelix = (theta: number) => {\n      const x = cx + radius * Math.sin(theta);\n      const y = topY + (theta / THETA_MAX) * rampHeight;\n      return [x, y] as const;\n    };\n\n    const buildRampPath = () => {\n      const path = new Path2D();\n      for (let i = 0; i <= RAMP_SAMPLES; i++) {\n        const theta = (i / RAMP_SAMPLES) * THETA_MAX;\n        const [x, y] = pointOnHelix(theta);\n        if (i === 0) path.moveTo(x, y);\n        else path.lineTo(x, y);\n      }\n      rampPath = path;\n      const [bx, by] = pointOnHelix(THETA_MAX);\n      baseX = bx;\n      baseY = by;\n    };\n\n    const resize = () => {\n      const rect = root.getBoundingClientRect();\n      if (rect.width < 2 || rect.height < 2) {\n        sized = false;\n        return;\n      }\n      const isCard = !!canvas.closest(\"[data-autoplay-root]\");\n      const dpr = isCard\n        ? Math.min(0.75, window.devicePixelRatio || 1)\n        : Math.min(window.devicePixelRatio || 1, 1.5);\n      w = rect.width;\n      h = rect.height;\n      canvas.width = Math.max(1, Math.round(w * dpr));\n      canvas.height = Math.max(1, Math.round(h * dpr));\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\n      const minDim = Math.min(w, h);\n      cx = w / 2;\n      // clamp against w/2 as well as min(w,h) — a container taller than\n      // wide would otherwise let the helix clip both side edges.\n      radius = Math.min(RADIUS_FACTOR * minDim, w * 0.4);\n      topY = h * RAMP_TOP_FRAC;\n      rampHeight = h * RAMP_HEIGHT_FRAC;\n      parcelSize = Math.max(3, minDim * PARCEL_SIZE_FRAC);\n      pileJitter = minDim * PILE_JITTER_FRAC;\n      sized = true;\n      buildRampPath();\n    };\n\n    const drawSquare = (x: number, y: number, size: number, alpha: number) => {\n      ctx.globalAlpha = alpha;\n      ctx.fillRect(x - size / 2, y - size / 2, size, size);\n    };\n\n    const draw = () => {\n      if (!sized || !fgColor || !mutedColor || !rampPath) return;\n      ctx.clearRect(0, 0, w, h);\n\n      // ramp track: the track, not the subject — low, border-like contrast\n      ctx.globalAlpha = 1;\n      ctx.strokeStyle = mutedColor;\n      // floored well above the raw radius*0.02 scale — at card-preview\n      // size (~400px, dpr 0.75) that scale alone thins under 2px and the\n      // ramp risks disappearing on a light background, the exact failure\n      // the spec calls out. Alpha raised to match; parcels/pile still\n      // dominate at 0.92-0.95 --foreground.\n      ctx.globalAlpha = 0.55;\n      ctx.lineWidth = Math.max(1.5, radius * 0.02);\n      ctx.lineJoin = \"round\";\n      ctx.lineCap = \"round\";\n      ctx.stroke(rampPath);\n\n      // pile: current cycle's tiles plus any mid-sweep leftovers\n      ctx.fillStyle = fgColor;\n      const pile = computePile(t);\n      for (const tile of pile) {\n        const [ox, oy] = PILE_OFFSETS[((tile.k % 9) + 9) % 9];\n        const slideX = tile.sweepProgress > 0 ? easeOut(tile.sweepProgress) * (w * 0.7) : 0;\n        const x = baseX + ox * pileJitter + slideX;\n        const y = baseY + oy * pileJitter * 0.6;\n        const alpha = 0.92 * (1 - tile.sweepProgress);\n        if (alpha > 0.01) drawSquare(x, y, parcelSize * 0.9, alpha);\n      }\n\n      // in-flight parcels on the helix\n      const flight = computeFlight(t);\n      for (const parcel of flight) {\n        const theta = parcel.progress * THETA_MAX;\n        const [x, y] = pointOnHelix(theta);\n        drawSquare(x, y, parcelSize, 0.95);\n      }\n\n      ctx.globalAlpha = 1;\n    };\n\n    const loop = (now: number) => {\n      const dt = last ? Math.min(50, now - last) : 1000 / 60;\n      last = now;\n      t += dt;\n      draw();\n      if (visible && !document.hidden) raf = requestAnimationFrame(loop);\n    };\n\n    let resizeTimer: ReturnType<typeof setTimeout> | null = null;\n    const ro = new ResizeObserver(() => {\n      if (resizeTimer) clearTimeout(resizeTimer);\n      resizeTimer = setTimeout(() => {\n        resizeTimer = null;\n        resize();\n        if (reduced) draw();\n      }, 120);\n    });\n    ro.observe(root);\n\n    const io = new IntersectionObserver(\n      (entries) => {\n        visible = entries[0]?.isIntersecting ?? true;\n        if (visible && !reduced && !raf) {\n          last = 0;\n          raf = requestAnimationFrame(loop);\n        }\n      },\n      { threshold: 0 }\n    );\n    io.observe(root);\n\n    const onVis = () => {\n      if (!document.hidden && visible && !reduced && !raf) {\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    const mo = new MutationObserver(() => {\n      readTokens();\n      if (reduced || !raf) draw();\n    });\n    mo.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\"] });\n\n    readTokens();\n    resize();\n\n    if (reduced) {\n      draw();\n    } else {\n      raf = requestAnimationFrame(loop);\n    }\n\n    return () => {\n      cancelAnimationFrame(raf);\n      if (resizeTimer) clearTimeout(resizeTimer);\n      ro.disconnect();\n      io.disconnect();\n      mo.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVis);\n    };\n  }, []);\n\n  return (\n    <div\n      ref={rootRef}\n      className={`relative isolate min-h-screen w-full overflow-hidden bg-background ${className}`}\n      style={style}\n    >\n      <canvas\n        ref={canvasRef}\n        aria-hidden\n        className=\"pointer-events-none absolute inset-0 block h-full w-full\"\n      />\n      {children ? (\n        <div className=\"relative z-10 flex min-h-screen w-full flex-col items-center justify-center gap-4 px-6 text-center\">\n          {children}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/spiral-chute-accrete.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": [
      "background",
      "canvas",
      "chute",
      "spiral",
      "helix",
      "logistics",
      "parcel",
      "ambient"
    ],
    "instruction": "Build <SpiralChuteAccrete className? style?><...headline/CTA/></SpiralChuteAccrete> as a full-bleed Canvas 2D background reproducing a real gravity spiral chute: the helical slide sorting facilities and department-store stockrooms use to move parcels/mail sacks between floors under gravity alone, no powered conveyance. GEOMETRY: the helix is a cached Path2D built once per resize from a parametric function evaluated at RAMP_SAMPLES (240) points, x(theta) = cx + radius*sin(theta), y(theta) = topY + (theta/THETA_MAX)*rampHeight, with radius = 0.32*min(width,height) and THETA_MAX = 3.5 full turns (TURNS*2*PI) — the container's SMALLER dimension drives radius so the chute reads correctly at both hero and card scale. Every parcel's on-screen position is this SAME cached function evaluated at the parcel's own progress, never a per-frame re-derivation of the curve. STATE IS A PURE FUNCTION OF ONE CLOCK: rather than mutating a live array of parcel/tile objects, every drawn quantity — which parcels are in flight and at what theta, which tiles are in the current pile, which are mid-sweep-out — is derived directly from the elapsed clock t (ms) and a fixed spawn index k, via spawn(k)=k*SPAWN_INTERVAL_MS and land(k)=spawn(k)+TRANSIT_MS. This is what makes prefers-reduced-motion reproducible: freezing the clock at a named STATIC_T reproduces the exact same layout deterministically, no history to serialize. REAL NUMBERS: TRANSIT_MS is 3600ms for one parcel's FULL 3.5-turn transit — this is one of the rare cases where the real-world rate (roughly 1-3 m/s on a shallow chute) and the legible rate are close, so descent renders near real proportion rather than being artificially decoupled. SPAWN_INTERVAL_MS is a fixed 1300ms cadence, which at a 3600ms transit keeps 2-3 parcels in flight simultaneously, each at a different theta so they never visually overlap on the same winding — per the spec's own kill criteria, THIS simultaneous-parcel count (not a faster descent speed) is the intended fix if the motion ever reads as too sparse to register as alive, because a faster descent would break the close-to-real-world proportion the concept was chosen to preserve. PILE / SWEEP: every landed parcel becomes one pile tile positioned near the helix's base point at a jittered offset drawn from a fixed 9-entry deterministic sequence keyed by k % 9 (never per-frame randomness), so the pile shape is exactly reproducible. Every SWEEP_PERIOD_MS (9000ms, fixed) the chute's current fill cycle ends; the tiles that belong to the cycle which just ended slide off-canvas to one side over SWEEP_DURATION_MS (500ms), staggered SWEEP_STAGGER_MS (30ms) apart, entirely derived from t % SWEEP_PERIOD_MS — this bounds the pile to at most one fill cycle's worth of tiles (about 5 at these rates) so it can never visually breach the canvas edge, which the spec calls out explicitly as a bug-read to avoid. Live mode's clock starts at WARM_START_MS = 5150 rather than 0 — a bare t=0 clock is a near-empty chute (one parcel just spawned, pile empty), which fails the spec's own t0 requirement of 2-3 visible parcels plus a partial pile; WARM_START_MS is a deliberately different phase than the reduced-motion STATIC_T so the two never read as the same frame. TOKENS: the ramp track renders at --ns-muted with globalAlpha 0.55 and a lineWidth floored at 1.5 CSS px (radius*0.02 alone thins under 2px at card-preview scale and risks disappearing on a light background) — it is the track, not the subject, and deliberately sits at a low, --border-like contrast step without literally using --border as a canvas stroke (--border stays a separator-only token per the binding rule). Parcels and pile tiles render at --foreground, both read via getComputedStyle(document.documentElement) at mount and re-read on a MutationObserver watching documentElement's class, closing the mutation-observer/resize/rAF-start paint-before-token-read gap. --ns-accent never appears anywhere in this component; the pile-sweep is this component's one climactic moment and is a plain --foreground fade-and-slide, never accent-tinted. prefers-reduced-motion (and initial mount before any observer fires) freezes the clock at STATIC_T = 6450ms, named MID_DESCENT — chosen 71.7% into a 9s fill cycle so the pile shows 3 of a possible ~5 tiles (roughly half full, neither freshly swept nor about to overflow) while two parcels remain visibly mid-spiral at clearly different theta, the single frame that shows track, motion-implying parcel spacing and an in-progress pile all at once. HOST: DPR capped at 1.5 full-bleed / 0.75 inside an autoplay preview card, ResizeObserver-driven resize (rebuilds the cached ramp Path2D and base landing point), IntersectionObserver (threshold 0) and visibilitychange both pausing the single rAF loop off-viewport or tab-hidden, dt clamped to 50ms per frame to avoid a pile/spawn-count jump after a long background tab stall. No interaction: this is a pure ambient background with no pointer state, per spec. Decorative canvas is aria-hidden and pointer-events-none; children render as real DOM in normal focus order above it, centered, with the same scrim convention as sibling full-bleed hero components. Props: children, className, style."
  },
  "type": "registry:ui"
}