{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "jam-kickout-loop",
  "title": "Jam Kickout Loop",
  "description": "An ambient background-job status line built as bulk-mail sortation conveyor jam handling: most tiles ride the main line straight through, but a fixed 1-in-6 tile is diverted by a swinging arm onto a lower side loop and re-merged one cycle later for a second attempt, never stopping the line to clear it by hand.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/jam-kickout-loop/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\n// ---------------------------------------------------------------------------\n// JamKickoutLoop — an ambient \"background job with retries\" status line,\n// sourced from bulk-mail sortation conveyor jam handling: a photo-eye pair\n// spanning the belt flags a mis-timed piece, a solenoid-driven diverter arm\n// kicks that single piece off the main line onto a recirculation loop before\n// it reaches the sort point, and the loop re-merges it one cycle later for\n// another pass — the line never stops to clear it by hand.\n//\n// Geometry derives from the container's SMALLER dimension: loop depth =\n// 0.3 * min(w,h); the main line and the loop's diverter/re-entry points\n// scale off the container's full width. Every tile's position is a pure\n// function of elapsed time since its own spawn (t=0 at the left edge),\n// evaluated fresh each rAF tick and written straight to that tile's\n// transform — nothing is animated with CSS transitions or React state.\n//\n// Timing (all fixed, none randomised):\n//  - spawn: one tile every 900ms at the left edge.\n//  - main line rate: reaches the diverter point (72% of track width) at\n//    1.6s after spawn — this rate is held constant for the whole tile\n//    lifetime, before and after the loop.\n//  - kickout: exactly every 6th tile (a counter, never Math.random()) is\n//    diverted. ~16.7% is a deliberate legibility amplification over the\n//    real ~2-5% recirculation rate, documented, not a claim about real\n//    jam frequency.\n//  - diverter swing: 150ms, the tile visibly leaves the main line onto the\n//    loop path (not a fade) as the arm swings out and re-seats.\n//  - loop transit: 1.9s around the side loop, re-entering the main line at\n//    a fixed point 18% of track width (behind the diverter) — a second\n//    full attempt, not a respawn — then continuing to the right edge at\n//    the same constant rate as any other tile.\n//  - exit: both first-pass and recirculated tiles fade over 250ms on\n//    reaching the right edge.\n//\n// Legibility: the one thing to follow is the diverter arm swinging a\n// single tile off the line onto the loop and back — cadence is one\n// kickout roughly every 5.4s (900ms * 6), well inside \"watch it happen\"\n// pacing, and the swing is a visible mechanical gesture rather than an\n// instant cut so the eye can track departure and arrival, not a blink.\n// ---------------------------------------------------------------------------\n\nconst SPAWN_MS = 900;\nconst KICK_EVERY = 6; // 1-in-6 tiles diverted, fixed counter\nconst T_MAIN1 = 1600; // ms, left edge -> diverter point (72% of track)\nconst DIVERT_FRAC = 0.72;\nconst REENTRY_FRAC = 0.18;\nconst T_SWING = 150; // ms, diverter arm swing\nconst T_LOOP = 1900; // ms, loop transit\nconst T_FADE = 250; // ms, exit fade\nconst TILE_W = 13;\nconst TILE_H = 7;\nconst POOL_SIZE = 20; // headroom above max concurrently-alive tiles\n\ninterface Item {\n  id: number;\n  spawnTime: number;\n  kicked: boolean;\n}\n\ninterface Geometry {\n  w: number;\n  h: number;\n  x0: number; // left edge (spawn x)\n  trackW: number; // usable track width\n  mainY: number;\n  loopY: number; // bottom of the side loop\n  dx: number; // diverter x (px)\n  rx: number; // re-entry x (px)\n  arcR: number; // loop corner radius, for path + tile keyframe match\n  rate: number; // px/ms, constant main-line rate\n  mainDurFull: number; // ms, non-kicked left edge -> right edge\n  mainDur2: number; // ms, kicked re-entry -> right edge\n}\n\nfunction computeGeometry(w: number, h: number): Geometry {\n  const loopY_depth = 0.3 * Math.min(w, h);\n  const x0 = Math.min(10, w * 0.03);\n  const trackW = Math.max(1, w - x0 * 2);\n  const mainY = Math.max(18, Math.min(h - loopY_depth - 14, h * 0.42));\n  const loopY = mainY + loopY_depth;\n  const dx = x0 + DIVERT_FRAC * trackW;\n  const rx = x0 + REENTRY_FRAC * trackW;\n  const arcR = Math.max(4, Math.min(loopY_depth * 0.5, (dx - rx) * 0.22));\n  const rate = (DIVERT_FRAC * trackW) / T_MAIN1;\n  const mainDurFull = trackW / rate;\n  const mainDur2 = (x0 + trackW - rx) / rate;\n  return { w, h, x0, trackW, mainY, loopY, dx, rx, arcR, rate, mainDurFull, mainDur2 };\n}\n\n/** Loop-transit keyframe: swing-end point -> bottom-right corner ->\n * bottom-left corner -> re-entry point, matching the static loop <path>. */\nfunction loopPosition(f: number, g: Geometry, swingEndX: number, swingEndY: number): { x: number; y: number } {\n  const p0 = { x: swingEndX, y: swingEndY };\n  const p1 = { x: g.dx - g.arcR, y: g.loopY };\n  const p2 = { x: g.rx + g.arcR, y: g.loopY };\n  const p3 = { x: g.rx, y: g.mainY };\n  if (f < 0.2) {\n    const t = f / 0.2;\n    return { x: p0.x + (p1.x - p0.x) * t, y: p0.y + (p1.y - p0.y) * t };\n  }\n  if (f < 0.8) {\n    const t = (f - 0.2) / 0.6;\n    return { x: p1.x + (p2.x - p1.x) * t, y: p1.y };\n  }\n  const t = (f - 0.8) / 0.2;\n  return { x: p2.x + (p3.x - p2.x) * t, y: p2.y + (p3.y - p2.y) * t };\n}\n\ninterface Pose {\n  x: number;\n  y: number;\n  opacity: number;\n  alive: boolean;\n  swingActive: boolean;\n  swingFrac: number; // 0..1 while swingActive, for the arm\n}\n\nfunction poseAt(t: number, kicked: boolean, g: Geometry): Pose {\n  if (!kicked) {\n    if (t < g.mainDurFull) {\n      return { x: g.x0 + g.rate * t, y: g.mainY, opacity: 1, alive: true, swingActive: false, swingFrac: 0 };\n    }\n    if (t < g.mainDurFull + T_FADE) {\n      const f = (t - g.mainDurFull) / T_FADE;\n      return { x: g.x0 + g.trackW, y: g.mainY, opacity: 1 - f, alive: true, swingActive: false, swingFrac: 0 };\n    }\n    return { x: 0, y: 0, opacity: 0, alive: false, swingActive: false, swingFrac: 0 };\n  }\n\n  if (t < T_MAIN1) {\n    return { x: g.x0 + g.rate * t, y: g.mainY, opacity: 1, alive: true, swingActive: false, swingFrac: 0 };\n  }\n  const swingEndX = g.dx - g.arcR * 0.35;\n  const swingEndY = g.mainY + (g.loopY - g.mainY) * 0.4;\n  if (t < T_MAIN1 + T_SWING) {\n    const f = (t - T_MAIN1) / T_SWING;\n    return {\n      x: g.dx + (swingEndX - g.dx) * f,\n      y: g.mainY + (swingEndY - g.mainY) * f,\n      opacity: 1,\n      alive: true,\n      swingActive: true,\n      swingFrac: f,\n    };\n  }\n  if (t < T_MAIN1 + T_SWING + T_LOOP) {\n    const f = (t - T_MAIN1 - T_SWING) / T_LOOP;\n    const p = loopPosition(f, g, swingEndX, swingEndY);\n    return { x: p.x, y: p.y, opacity: 1, alive: true, swingActive: false, swingFrac: 0 };\n  }\n  const t3 = T_MAIN1 + T_SWING + T_LOOP;\n  if (t < t3 + g.mainDur2) {\n    const local = t - t3;\n    return { x: g.rx + g.rate * local, y: g.mainY, opacity: 1, alive: true, swingActive: false, swingFrac: 0 };\n  }\n  if (t < t3 + g.mainDur2 + T_FADE) {\n    const f = (t - t3 - g.mainDur2) / T_FADE;\n    return { x: g.x0 + g.trackW, y: g.mainY, opacity: 1 - f, alive: true, swingActive: false, swingFrac: 0 };\n  }\n  return { x: 0, y: 0, opacity: 0, alive: false, swingActive: false, swingFrac: 0 };\n}\n\nfunction loopPathD(g: Geometry): string {\n  return [\n    `M${g.dx.toFixed(1)} ${g.mainY.toFixed(1)}`,\n    `C ${g.dx.toFixed(1)} ${(g.mainY + (g.loopY - g.mainY) * 0.7).toFixed(1)}, ${g.dx.toFixed(1)} ${g.loopY.toFixed(1)}, ${(g.dx - g.arcR).toFixed(1)} ${g.loopY.toFixed(1)}`,\n    `L ${(g.rx + g.arcR).toFixed(1)} ${g.loopY.toFixed(1)}`,\n    `C ${g.rx.toFixed(1)} ${g.loopY.toFixed(1)}, ${g.rx.toFixed(1)} ${(g.mainY + (g.loopY - g.mainY) * 0.7).toFixed(1)}, ${g.rx.toFixed(1)} ${g.mainY.toFixed(1)}`,\n  ].join(\" \");\n}\n\n// Fixed reduced-motion freeze frame — named MID_KICKOUT: one tile mid\n// diverter-swing (still partially over the main line, already angled onto\n// the loop), one tile further along the loop, one ordinary tile mid main\n// line — normal transit, the kickout event, and loop transit in one frame.\nconst FREEZE_SWING_T = T_MAIN1 + T_SWING * 0.5;\nconst FREEZE_LOOP_T = T_MAIN1 + T_SWING + T_LOOP * 0.72;\nconst FREEZE_MAIN_T = 800;\n\nexport interface JamKickoutLoopProps {\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nexport function JamKickoutLoop({ className = \"\" }: JamKickoutLoopProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const pathRef = useRef<SVGPathElement>(null);\n  const lineRef = useRef<SVGLineElement>(null);\n  const armRef = useRef<SVGLineElement>(null);\n  const tileRefs = useRef<(HTMLDivElement | null)[]>([]);\n\n  useEffect(() => {\n    const root = rootRef.current;\n    if (!root) return;\n\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n    let disposed = false;\n    let geo = computeGeometry(root.clientWidth || 1, root.clientHeight || 1);\n    let visible = true;\n    let raf = 0;\n    let last = 0;\n    let spawnAcc = 0;\n    let spawnCounter = 0;\n    let idSeq = 0;\n    let items: Item[] = [];\n    let armSwingStart = -Infinity;\n\n    const applyGeometry = () => {\n      pathRef.current?.setAttribute(\"d\", loopPathD(geo));\n      lineRef.current?.setAttribute(\"x1\", String(geo.x0));\n      lineRef.current?.setAttribute(\"x2\", String(geo.x0 + geo.trackW));\n      lineRef.current?.setAttribute(\"y1\", String(geo.mainY));\n      lineRef.current?.setAttribute(\"y2\", String(geo.mainY));\n      const armLen = Math.max(10, geo.arcR * 0.9);\n      armRef.current?.setAttribute(\"x1\", String(geo.dx));\n      armRef.current?.setAttribute(\"y1\", String(geo.mainY));\n      armRef.current?.setAttribute(\"x2\", String(geo.dx + armLen));\n      armRef.current?.setAttribute(\"y2\", String(geo.mainY));\n    };\n\n    const setTile = (el: HTMLDivElement | null, x: number, y: number, opacity: number) => {\n      if (!el) return;\n      el.style.transform = `translate3d(${(x - TILE_W / 2).toFixed(1)}px, ${(y - TILE_H / 2).toFixed(1)}px, 0)`;\n      el.style.opacity = String(opacity);\n    };\n\n    const setArm = (angleDeg: number, emphasis: number) => {\n      const el = armRef.current;\n      if (!el) return;\n      el.setAttribute(\"transform\", `rotate(${angleDeg.toFixed(2)} ${geo.dx.toFixed(1)} ${geo.mainY.toFixed(1)})`);\n      el.style.opacity = String(0.55 + 0.45 * emphasis);\n    };\n\n    const renderFrame = (now: number) => {\n      let i = 0;\n      for (; i < items.length && i < POOL_SIZE; i++) {\n        const item = items[i];\n        if (!item) continue;\n        const pose = poseAt(now - item.spawnTime, item.kicked, geo);\n        setTile(tileRefs.current[i] ?? null, pose.x, pose.y, pose.opacity);\n        if (item.kicked && pose.swingActive) {\n          armSwingStart = item.spawnTime + T_MAIN1;\n        }\n      }\n      for (; i < POOL_SIZE; i++) setTile(tileRefs.current[i] ?? null, geo.x0, geo.mainY, 0);\n\n      // Arm: swing out over T_SWING, ease back over the following ~250ms;\n      // resting otherwise. Driven off the most recent kick's own timeline,\n      // not a separate clock, so it always matches the tile that triggered it.\n      const sinceSwing = now - armSwingStart;\n      if (sinceSwing >= 0 && sinceSwing < T_SWING) {\n        const f = sinceSwing / T_SWING;\n        setArm(108 * f, f);\n      } else if (sinceSwing >= T_SWING && sinceSwing < T_SWING + 260) {\n        const f = 1 - (sinceSwing - T_SWING) / 260;\n        setArm(108 * f, f);\n      } else {\n        setArm(0, 0);\n      }\n    };\n\n    const loop = (now: number) => {\n      raf = 0;\n      if (!visible) return;\n      if (last === 0) last = now;\n      const dt = Math.min(100, now - last);\n      last = now;\n\n      spawnAcc += dt;\n      while (spawnAcc >= SPAWN_MS) {\n        spawnAcc -= SPAWN_MS;\n        const kicked = spawnCounter % KICK_EVERY === KICK_EVERY - 1;\n        spawnCounter += 1;\n        items.push({ id: idSeq++, spawnTime: now - spawnAcc, kicked });\n      }\n      items = items.filter((it) => poseAt(now - it.spawnTime, it.kicked, geo).alive);\n\n      renderFrame(now);\n      raf = requestAnimationFrame(loop);\n    };\n\n    const seedResting = () => {\n      // Populate a believable \"already running\" resting state instead of an\n      // empty track on first paint: back-date a handful of spawn times.\n      const now = performance.now();\n      items = [];\n      spawnCounter = 0;\n      const seedSpawns = [-5100, -4200, -3300, -2400, -1500, -600];\n      for (const offset of seedSpawns) {\n        const kicked = spawnCounter % KICK_EVERY === KICK_EVERY - 1;\n        spawnCounter += 1;\n        items.push({ id: idSeq++, spawnTime: now + offset, kicked });\n      }\n      items = items.filter((it) => poseAt(now - it.spawnTime, it.kicked, geo).alive);\n      spawnAcc = 600; // next spawn due in 300ms, matching the 900ms cadence\n      renderFrame(now);\n    };\n\n    const renderFreezeFrame = () => {\n      applyGeometry();\n      const swing = poseAt(FREEZE_SWING_T, true, geo);\n      const loopTile = poseAt(FREEZE_LOOP_T, true, geo);\n      const mainTile = poseAt(FREEZE_MAIN_T, false, geo);\n      setTile(tileRefs.current[0] ?? null, swing.x, swing.y, 1);\n      setTile(tileRefs.current[1] ?? null, loopTile.x, loopTile.y, 1);\n      setTile(tileRefs.current[2] ?? null, mainTile.x, mainTile.y, 1);\n      for (let i = 3; i < POOL_SIZE; i++) setTile(tileRefs.current[i] ?? null, geo.x0, geo.mainY, 0);\n      setArm(108 * swing.swingFrac, swing.swingFrac);\n    };\n\n    const start = () => {\n      applyGeometry();\n      if (reduced) {\n        renderFreezeFrame();\n        return;\n      }\n      seedResting();\n      last = 0;\n      if (!raf) raf = requestAnimationFrame(loop);\n    };\n\n    let resizeTimer = 0;\n    const onResize = () => {\n      window.clearTimeout(resizeTimer);\n      resizeTimer = window.setTimeout(() => {\n        if (disposed || !root) return;\n        geo = computeGeometry(root.clientWidth || 1, root.clientHeight || 1);\n        applyGeometry();\n        if (reduced) renderFreezeFrame();\n      }, 100);\n    };\n    const ro = new ResizeObserver(onResize);\n    ro.observe(root);\n\n    const io = new IntersectionObserver((entries) => {\n      visible = entries[0]?.isIntersecting ?? true;\n      if (visible && !reduced && !raf) {\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    });\n    io.observe(root);\n\n    start();\n\n    return () => {\n      disposed = true;\n      cancelAnimationFrame(raf);\n      raf = 0;\n      window.clearTimeout(resizeTimer);\n      ro.disconnect();\n      io.disconnect();\n    };\n  }, []);\n\n  return (\n    <div\n      ref={rootRef}\n      className={`relative isolate overflow-hidden bg-background ${className}`}\n    >\n      <svg aria-hidden=\"true\" focusable=\"false\" className=\"pointer-events-none absolute inset-0 h-full w-full overflow-visible\">\n        <line ref={lineRef} stroke=\"var(--border)\" strokeWidth={1} />\n        <path ref={pathRef} fill=\"none\" stroke=\"var(--border)\" strokeWidth={1} />\n        <line\n          ref={armRef}\n          stroke=\"var(--ns-muted)\"\n          strokeWidth={2}\n          strokeLinecap=\"round\"\n          style={{ transition: \"opacity 150ms ease-out\" }}\n        />\n      </svg>\n      <div className=\"pointer-events-none absolute inset-0\">\n        {Array.from({ length: POOL_SIZE }).map((_, i) => (\n          <div\n            key={i}\n            ref={(el) => {\n              tileRefs.current[i] = el;\n            }}\n            className=\"absolute left-0 top-0 rounded-[1px] bg-foreground\"\n            style={{ width: TILE_W, height: TILE_H, opacity: 0, willChange: \"transform\" }}\n          />\n        ))}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/jam-kickout-loop.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)"
    },
    "light": {
      "ns-muted": "#4d4d4d"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "loader",
      "status",
      "ambient",
      "dom",
      "raf",
      "conveyor",
      "retry",
      "queue",
      "indeterminate"
    ],
    "instruction": "A passive, always-on ambient status indicator modelled on bulk-mail sortation conveyor jam handling: a photo-eye pair flags a mis-timed or stalled piece, a solenoid-driven diverter arm kicks that single piece off the main line onto a lower recirculation loop before it reaches the sort point, and the loop re-merges it one belt-cycle later for another pass — the line itself never stops. Rendering is DOM + CSS: a horizontal main track plus one lower side loop rendered as a single SVG `<path>` (a rounded-rect diversion path built from two cubic-bezier corners and a flat bottom span), with small `<div>` tiles animated purely via `transform: translate3d` written each rAF tick from a shared driver — no CSS transitions, no per-tile React state, positions come out of a pure `poseAt(elapsedSinceSpawn, kicked, geometry)` function re-evaluated every frame. Geometry derives from the container's SMALLER dimension (loop depth = 0.3 * min(w,h)); the main line and the loop's diverter (72% of track width) and re-entry (18% of track width, i.e. behind the diverter) points scale off the container's full width, recomputed on a debounced ResizeObserver. Timing is fixed, never randomised: one tile spawns every 900ms at the left edge; the main-line rate is constant and calibrated so a tile reaches the diverter point at 1.6s after spawn, and that same rate governs the tile both before the loop and after re-entry; exactly every 6th tile (a plain counter, never Math.random()) is diverted — real bulk-mail recirculation runs roughly 2-5%, so 1-in-6 (~16.7%) is a documented legibility amplification for a card-scale viewing window, not a claim about real jam frequency. A diverted tile's kick is rendered as a 150ms arm-swing (a separate `<line>` pivoted with an SVG `rotate()` transform about the diverter point, driven off the same elapsed-time clock as the tile that triggered it, swinging out and re-seating over roughly 400ms total) during which the tile visibly leaves the main line onto the loop's first keyframe — never a fade or cut. Loop transit takes 1.9s along three keyframed spans (a short curve down to the loop's bottom-right corner, a flat traversal to the bottom-left corner, a short curve up to the re-entry point) matching the static path's own corner radius, after which the tile continues from the re-entry point to the right edge at the ordinary main-line rate like any other tile — a documented full second attempt, not a respawn. Every tile, first-pass or recirculated, fades over 250ms on reaching the right edge; spawning is continuous and unbounded, the loop never visibly drains to empty. On mount, a handful of back-dated spawn times seed an already-in-progress resting state (staggered mid-transit tiles, possibly one mid-loop) instead of an empty track on first paint. A fixed pool of tiles (`POOL_SIZE = 20`) is rendered once and reused, hidden at opacity 0 when unused, rather than mounting/unmounting DOM nodes per tile. No colour literals: the loop path and main line are `var(--border)` (structural, whisper contrast, never a fill), the diverter arm is `var(--ns-muted)` resting and brightens toward full opacity only while actively swinging (a luminance step, not a colour or `--ns-accent` — that token is interaction chrome only and this component has none), and every tile is `bg-foreground` regardless of whether it is ever diverted — the diverted tile's distinction comes entirely from its path leaving the main line, never from hue. Interaction: none, this is a passive ambient loop, not a button; per-click retry-with-cooldown is `button-retry-backoff`'s territory. Under `prefers-reduced-motion` the rAF loop and spawn cadence never start; instead a single named freeze frame, `MID_KICKOUT`, is rendered once from the same `poseAt` function evaluated at three fixed elapsed times — one tile mid diverter-swing (still partially over the main line, already angled onto the loop), one tile further along the loop's transit, and one ordinary tile mid main-line — showing normal transit, the kickout event, and loop transit together in one frame, with the arm frozen mid-swing to match. Cleans up on unmount: cancels the rAF, disconnects the ResizeObserver and IntersectionObserver (which pauses/resumes the rAF loop when the container scrolls off/on screen). Zero dependencies, DOM + SVG + CSS only, no canvas."
  },
  "type": "registry:ui"
}