{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "knit-ladder-run",
  "title": "Knit Ladder Run",
  "description": "An ambient status/health card background: a field of knit stitches builds downward course by course, and at random intervals a dropped stitch opens a ladder that runs a few rows before a latch-hook repair catches it and reknits the column. Structurally sound, with occasional self-corrected faults, in place of a static health badge.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/knit-ladder-run/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\n// ---------------------------------------------------------------------------\n// KnitLadderRun — an ambient status/health feedback card background: a field\n// of knit stitches builds downward course by course, and at random intervals\n// a dropped stitch opens a ladder (\"run\") that unravels a few rows before a\n// latch-hook repair catches it and reknits the column. Source: weft-knit\n// loop formation + latch-hook ladder repair (textile/knitting).\n//\n// Distinct from text-stitch-unpick (pointer-driven, letter-by-letter,\n// irreversible per letter) and optimistic-stitch (a single row's write\n// lifecycle, never loops): this is fully ambient, builds continuously, and\n// its ladder-run is a RECURRING, SELF-HEALING fault, not a one-shot\n// lifecycle or a pointer gesture.\n//\n// TIMELINE — two clocks, both derived from one elapsed-ms counter, no React\n// state:\n//   continuousCourse = elapsedMs / COURSE_MS   (smooth fractional scroll)\n//   currentCourse    = floor(continuousCourse)  (newest row that exists)\n// New courses enter at the bottom edge; existing courses drift upward and\n// eventually exit the top (the \"continuous fabric take-down\" feed) — never\n// a fixed-length pass.\n//\n// LADDER STATE MACHINE (one plain ref object, at most one ladder live):\n//   idle -> trigger (random 7-10s gap) picks a column, a length (4-7 rows)\n//   and an anchor `dropRow` = currentCourse - length (so the whole span\n//   already exists and propagation never has to wait on new courses).\n//   OPEN phase: row (dropRow + k) collapses from a stitch to a bare rail\n//   at openAt(k) = openStartTime + k*350ms, animated over a 120ms window.\n//   REPAIR phase starts once every row has opened (repairStart = openStart\n//   + length*350ms) and closes rows bottom-up (the newest/last-opened row\n//   closes FIRST) one rung every 300ms, each with a brief latch-hook glyph\n//   and a 150ms close animation, until the topmost (drop) row reseals and\n//   the column matches its neighbors again — the ladder always finishes.\n// ---------------------------------------------------------------------------\n\nconst COURSE_MS = 900; // one full course (row) build event\nconst PITCH_TARGET = 10; // px, target stitch pitch\nconst MIN_COLS = 14;\nconst MAX_COLS = 28;\n\nconst LADDER_MIN_INTERVAL_MS = 7000;\nconst LADDER_MAX_INTERVAL_MS = 10000;\nconst LADDER_MIN_LEN = 4;\nconst LADDER_MAX_LEN = 7;\nconst OPEN_STEP_MS = 350;\nconst OPEN_ANIM_MS = 120;\nconst CLOSE_STEP_MS = 300;\nconst CLOSE_ANIM_MS = 150;\n\nconst AGE_FADE_ROWS = 14; // rows over which a fresh stitch recedes to the muted baseline\nconst MUTED_BASE_ALPHA = 0.55;\nconst FRESH_MAX_ALPHA = 0.75; // \"moderate\", never full-strength per spec\n\n// reduced-motion freeze: a static ladder frozen mid-run, not derived from RNG\nconst STATIC_CURRENT_COURSE = 16;\nconst STATIC_DROP_ROW = 11;\nconst STATIC_LADDER_LEN = 3; // \"2-3 rows open\"\n\nfunction easeOutCubic(x: number): number {\n  const t = 1 - x;\n  return 1 - t * t * t;\n}\n\ninterface LadderState {\n  active: boolean;\n  column: number;\n  dropRow: number;\n  length: number;\n  openStart: number; // elapsedMs\n  repairStart: number; // elapsedMs, derived at trigger time\n  nextTriggerAt: number; // elapsedMs\n}\n\nexport interface KnitLadderRunProps {\n  className?: string;\n}\n\nexport function KnitLadderRun({ className = \"\" }: KnitLadderRunProps) {\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(\"(prefers-reduced-motion: reduce)\").matches;\n\n    let fg = \"currentColor\";\n    let muted = \"currentColor\";\n    let width = 0;\n    let height = 0;\n    let dpr = 1;\n    let sized = false;\n    let visible = true;\n\n    let pitch = PITCH_TARGET;\n    let cols = MIN_COLS;\n\n    const ladder: LadderState = {\n      active: false,\n      column: 0,\n      dropRow: 0,\n      length: 0,\n      openStart: 0,\n      repairStart: 0,\n      nextTriggerAt: LADDER_MIN_INTERVAL_MS + Math.random() * (LADDER_MAX_INTERVAL_MS - LADDER_MIN_INTERVAL_MS),\n    };\n\n    const readTokens = () => {\n      const s = getComputedStyle(document.documentElement);\n      fg = s.getPropertyValue(\"--foreground\").trim() || \"currentColor\";\n      muted = s.getPropertyValue(\"--ns-muted\").trim() || fg;\n    };\n\n    const resize = () => {\n      const rect = canvas.getBoundingClientRect();\n      width = rect.width;\n      height = rect.height;\n      if (width < 2 || height < 2) {\n        sized = false;\n        return;\n      }\n      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\n      cols = Math.min(MAX_COLS, Math.max(MIN_COLS, Math.round(width / PITCH_TARGET)));\n      pitch = width / cols;\n      sized = true;\n    };\n\n    // small interlocked loop glyph — two overlapping arcs, the \"stitch\" unit\n    const drawStitchGlyph = (cx: number, cy: number, color: string, alpha: number) => {\n      if (alpha <= 0.01) return;\n      const r = pitch * 0.28;\n      ctx.globalAlpha = alpha;\n      ctx.strokeStyle = color;\n      ctx.lineWidth = 1;\n      ctx.lineCap = \"round\";\n      ctx.beginPath();\n      ctx.arc(cx, cy - r * 0.32, r, Math.PI * 0.15, Math.PI * 0.85);\n      ctx.stroke();\n      ctx.beginPath();\n      ctx.arc(cx, cy + r * 0.32, r, Math.PI * 1.15, Math.PI * 1.85);\n      ctx.stroke();\n      ctx.globalAlpha = 1;\n    };\n\n    const drawNormalStitch = (cx: number, cy: number, age: number, alphaMul: number = 1) => {\n      if (alphaMul <= 0.01) return;\n      const fresh = Math.min(1, Math.max(0, 1 - age / AGE_FADE_ROWS));\n      drawStitchGlyph(cx, cy, muted, MUTED_BASE_ALPHA * alphaMul);\n      if (fresh > 0.01) drawStitchGlyph(cx, cy, fg, fresh * FRESH_MAX_ALPHA * alphaMul);\n    };\n\n    // a bare vertical rail marking an open rung, plus an optional latch-hook\n    // glyph while that rung is actively closing.\n    const drawRail = (cx: number, cy: number, alpha: number) => {\n      if (alpha <= 0.01) return;\n      ctx.globalAlpha = alpha;\n      ctx.strokeStyle = fg;\n      ctx.lineWidth = 1;\n      ctx.beginPath();\n      ctx.moveTo(cx, cy - pitch / 2);\n      ctx.lineTo(cx, cy + pitch / 2);\n      ctx.stroke();\n      ctx.globalAlpha = 1;\n    };\n\n    const drawHookGlyph = (cx: number, cy: number, alpha: number) => {\n      if (alpha <= 0.01) return;\n      const s = pitch * 0.24;\n      ctx.globalAlpha = alpha;\n      ctx.strokeStyle = fg;\n      ctx.lineWidth = 1.2;\n      ctx.lineCap = \"round\";\n      ctx.beginPath();\n      ctx.moveTo(cx, cy + s);\n      ctx.lineTo(cx, cy - s * 0.2);\n      ctx.arc(cx - s * 0.35, cy - s * 0.2, s * 0.35, 0, Math.PI * 1.4);\n      ctx.stroke();\n      ctx.globalAlpha = 1;\n    };\n\n    // renders one cell that belongs to the currently active ladder column,\n    // returning true if it drew a ladder-specific state (caller skips the\n    // normal stitch render in that case).\n    const drawLadderCell = (rowIndex: number, cx: number, cy: number, elapsedMs: number, age: number): boolean => {\n      if (!ladder.active) return false;\n      const rowOffset = rowIndex - ladder.dropRow;\n      if (rowOffset < 0 || rowOffset >= ladder.length) return false;\n\n      const openAt = ladder.openStart + rowOffset * OPEN_STEP_MS;\n      // rungs close bottom-up: the newest (highest offset) rung closes first\n      const closeAt = ladder.repairStart + (ladder.length - 1 - rowOffset) * CLOSE_STEP_MS;\n\n      if (elapsedMs < openAt) {\n        drawNormalStitch(cx, cy, age);\n        return true;\n      }\n      if (elapsedMs < openAt + OPEN_ANIM_MS) {\n        // the loop glyph visibly collapses to a bare gap: fade the stitch\n        // out while the rail fades in over the same 120ms window\n        const eased = easeOutCubic((elapsedMs - openAt) / OPEN_ANIM_MS);\n        drawNormalStitch(cx, cy, age, 1 - eased);\n        drawRail(cx, cy, eased);\n        return true;\n      }\n      if (elapsedMs < closeAt) {\n        drawRail(cx, cy, 1);\n        return true;\n      }\n      if (elapsedMs < closeAt + CLOSE_ANIM_MS) {\n        // the latch-hook reseals the rung: stitch fades back in as the rail\n        // fades out, with the hook glyph visible for the transition\n        const p = (elapsedMs - closeAt) / CLOSE_ANIM_MS;\n        const eased = easeOutCubic(p);\n        drawRail(cx, cy, 1 - eased);\n        drawNormalStitch(cx, cy, age, eased);\n        drawHookGlyph(cx, cy, 1 - p);\n        return true;\n      }\n      // fully repaired — render as a normal stitch and let this row age out\n      // of ladder tracking on the next active-ladder check naturally\n      return false;\n    };\n\n    const maybeAdvanceLadder = (elapsedMs: number, currentCourse: number) => {\n      if (ladder.active) {\n        const lastCloseAt = ladder.repairStart + (ladder.length - 1) * CLOSE_STEP_MS + CLOSE_ANIM_MS;\n        if (elapsedMs >= lastCloseAt) {\n          ladder.active = false;\n          ladder.nextTriggerAt =\n            elapsedMs + LADDER_MIN_INTERVAL_MS + Math.random() * (LADDER_MAX_INTERVAL_MS - LADDER_MIN_INTERVAL_MS);\n        }\n        return;\n      }\n      if (elapsedMs < ladder.nextTriggerAt) return;\n      const length = LADDER_MIN_LEN + Math.floor(Math.random() * (LADDER_MAX_LEN - LADDER_MIN_LEN + 1));\n      if (currentCourse - length < 0) return; // not enough built history yet, wait\n      ladder.active = true;\n      ladder.column = Math.floor(Math.random() * cols);\n      ladder.length = length;\n      ladder.dropRow = currentCourse - length;\n      ladder.openStart = elapsedMs;\n      ladder.repairStart = elapsedMs + length * OPEN_STEP_MS;\n    };\n\n    const drawField = (elapsedMs: number, staticLadder: { column: number; dropRow: number; length: number } | null) => {\n      if (!sized) return;\n      ctx.clearRect(0, 0, width, height);\n\n      const continuousCourse = elapsedMs / COURSE_MS;\n      const currentCourse = Math.floor(continuousCourse);\n      const buildEdgeY = height - pitch / 2;\n\n      const minRow = Math.floor(continuousCourse - height / pitch) - 1;\n\n      for (let rowIndex = Math.max(0, minRow); rowIndex <= currentCourse; rowIndex++) {\n        const cy = buildEdgeY - (continuousCourse - rowIndex) * pitch;\n        if (cy < -pitch || cy > height + pitch) continue;\n        const age = currentCourse - rowIndex;\n\n        for (let col = 0; col < cols; col++) {\n          const cx = col * pitch + pitch / 2;\n\n          if (staticLadder) {\n            const rowOffset = rowIndex - staticLadder.dropRow;\n            if (col === staticLadder.column && rowOffset >= 0 && rowOffset < staticLadder.length) {\n              drawRail(cx, cy, 1);\n              continue;\n            }\n            drawNormalStitch(cx, cy, age);\n            continue;\n          }\n\n          if (col === ladder.column && drawLadderCell(rowIndex, cx, cy, elapsedMs, age)) continue;\n          drawNormalStitch(cx, cy, age);\n        }\n      }\n    };\n\n    let raf = 0;\n    let startTime = 0;\n\n    const loop = (now: number) => {\n      if (!startTime) startTime = now;\n      const elapsedMs = now - startTime;\n      const currentCourse = Math.floor(elapsedMs / COURSE_MS);\n      maybeAdvanceLadder(elapsedMs, currentCourse);\n      drawField(elapsedMs, null);\n      if (visible && !document.hidden) raf = requestAnimationFrame(loop);\n    };\n\n    const drawReducedStatic = () => {\n      const elapsedMs = STATIC_CURRENT_COURSE * COURSE_MS + COURSE_MS * 0.5;\n      drawField(elapsedMs, { column: Math.floor(cols / 2), dropRow: STATIC_DROP_ROW, length: STATIC_LADDER_LEN });\n    };\n\n    const start = () => {\n      if (reduced) {\n        drawReducedStatic();\n        return;\n      }\n      startTime = 0;\n      cancelAnimationFrame(raf);\n      raf = requestAnimationFrame(loop);\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) drawReducedStatic();\n      }, 120);\n    };\n\n    const ro = new ResizeObserver(onResize);\n    ro.observe(canvas);\n\n    const io = new IntersectionObserver(\n      (entries) => {\n        visible = entries[0]?.isIntersecting ?? true;\n        if (visible && !reduced) start();\n      },\n      { threshold: 0 }\n    );\n    io.observe(canvas);\n\n    const onVis = () => {\n      if (!document.hidden && visible && !reduced) start();\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    const mo = new MutationObserver(() => {\n      readTokens();\n      if (reduced && sized) drawReducedStatic();\n    });\n    mo.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    readTokens();\n    resize();\n    start();\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 <canvas ref={canvasRef} aria-hidden=\"true\" className={`block h-full w-full ${className}`} />;\n}\n",
      "type": "registry:ui",
      "target": "components/ui/knit-ladder-run.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)"
    },
    "light": {
      "ns-muted": "#4d4d4d"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "status",
      "health",
      "background",
      "ambient",
      "canvas",
      "knit",
      "textile",
      "mono"
    ],
    "instruction": "Build <KnitLadderRun className?> as a self-contained, aria-hidden 2D canvas (w-full h-full) that renders a knit-stitch field building continuously from the bottom edge upward. GRID: stitch pitch targets 10px; cols = clamp(round(width / 10), 14, 28) so the pattern reads at card scale on both small and large containers, pitch = width / cols (exact fit); every stitch is a small interlocked-loop glyph (two overlapping arcs, one opening down, one opening up, drawn as two canvas arc() strokes offset vertically by roughly a third of the stitch radius) in --foreground/--ns-muted, never text or an icon font. BUILD CLOCK: one continuous elapsed-ms counter drives continuousCourse = elapsedMs / 900 (COURSE_MS) and currentCourse = Math.floor(continuousCourse); a row's screen y = (height - pitch/2) - (continuousCourse - rowIndex) * pitch, so the newest course sits fixed near the bottom edge and every existing course drifts smoothly upward, eventually exiting past the top — an unbounded feed, never a fixed-length pass, matching a knitting machine's continuous take-down. Only rows with rowIndex <= currentCourse are ever drawn (future rows simply don't exist yet). AGE FADE (two-pass alpha overlay, same convention as other ascii/dot-grid siblings in this registry — never color-string arithmetic): every stitch is drawn once in --ns-muted at a fixed moderate base alpha (~0.55), then again in --foreground at alpha = clamp(1 - age/14, 0, 1) * ~0.75 where age = currentCourse - rowIndex — so stitches at the build edge read brightest (still only moderate opacity, per spec, never full-strength) and recede to the flat muted baseline by about 14 rows back, value only, no hue change. LADDER STATE MACHINE: one plain mutable object (never React state), gated to at most one active ladder at a time. Idle: nextTriggerAt is scheduled `elapsedMs + random(7000, 10000)`ms after the previous ladder's very last close animation finishes (or from mount for the first one). On trigger (only once currentCourse - length >= 0, i.e. enough history already exists — this naturally keeps t0 ladder-free per the spec's resting-loop description): pick length = randomInt(4,7), column = randomInt(cols), dropRow = currentCourse - length (the whole span already exists, so propagation never has to wait on new courses), openStart = elapsedMs, repairStart = openStart + length * 350ms (OPEN_STEP_MS). OPEN phase: for rowOffset k in [0, length), row (dropRow+k) collapses from a normal stitch to a bare gap starting at openAt = openStart + k*350ms, animated over a 120ms window (OPEN_ANIM_MS) — crossfade the stitch glyph's alpha down to 0 while a single --foreground vertical hairline (the run's rail, drawn as one moveTo/lineTo spanning the cell height) fades in to alpha 1; between openAt+120ms and its close time the cell is a pure rail, no glyph, so the run reads as absence, not a colored alert. REPAIR phase closes rungs bottom-up (the newest/last-opened rung closes FIRST, the original drop row reseals LAST) one rung every 300ms (CLOSE_STEP_MS): closeAt(k) = repairStart + (length-1-k)*300ms; over a 150ms window (CLOSE_ANIM_MS) starting at closeAt, crossfade the rail back to 0 while the stitch glyph fades back in, and draw a small latch-hook glyph (a short vertical stroke plus a partial arc, --foreground, same weight as normal stitches, never --ns-accent) at full-then-fading alpha marking the rung actively resealing — successive rungs closing bottom-up at a steady 300ms cadence reads as the hook glyph working its way upward. The ladder is marked inactive once the topmost (drop) row's close animation finishes; repair always completes well before a column could scroll off the top edge, so no explicit deadline logic is needed beyond the fixed cadence. TOKENS: only --foreground and --ns-muted are read, once via getComputedStyle(document.documentElement) and re-read on a MutationObserver watching documentElement's class (no paint before the first read); --ns-accent never appears anywhere, including on the hook glyph or the rail. CANVAS HOST: DPR-aware backing store capped at 2, ResizeObserver on the canvas element recomputing cols/pitch on a 120ms debounce, IntersectionObserver (threshold 0) and visibilitychange both pausing/resuming the rAF loop, full cleanup (cancelAnimationFrame, disconnect all observers) on unmount. REDUCED MOTION: prefers-reduced-motion renders one deterministic static frame (ROW_SETTLED) — NOT derived from the random ladder scheduler — with currentCourse fixed at 16 and a synthetic ladder fixed at column = floor(cols/2), dropRow = 11, length = 3, rendered as three bare open rails among otherwise-complete, normally-aged courses (no repair glyph, no rail/stitch crossfade animation, no rAF loop): a small ladder gap frozen mid-run, 2-3 rows open, not yet repaired, sitting among finished knit structure, redrawn only on a token change or a debounced resize. Purely decorative: canvas is aria-hidden, no pointer handling of any kind — a hover-driven 'picking' gesture would collide with text-stitch-unpick's established grammar. Props: className only."
  },
  "type": "registry:ui"
}