{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "warp-knit-tricot-lapping",
  "title": "Warp Knit Tricot Lapping",
  "description": "An ambient divider/background texture whose guide bar shogs a short lateral distance every course, lapping each needle's thread diagonally across its neighbours to build a continuously-scrolling zigzag chevron field. The diagonal wale structure of warp knitting, distinct from a weft shuttle's full-width crossing.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/warp-knit-tricot-lapping/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\n// ---------------------------------------------------------------------------\n// WarpKnitTricotLapping — an ambient warp-knit (tricot) guide-bar lapping\n// texture. Unlike weft knitting, every needle has its own yarn fed from a\n// guide bar that holds ALL yarns in a row; between courses the whole bar\n// SHOGS (shifts) a short lateral distance before swinging back to the\n// needles, so each thread laps diagonally across neighbouring needles\n// course after course rather than staying in one column. That diagonal\n// shog-and-lap is what gives tricot its run-resistant diagonal wale\n// structure (unlike weft knit, no single dropped loop can ladder the whole\n// column) — the direct opposite motion of a weft-insertion shuttle pass\n// (loader-loom-weave), which crosses the FULL width of a fixed warp.\n//\n// Every needle in a row shares the SAME guide-bar offset, so drawing one\n// diagonal segment per needle per course — from its previous shogged\n// position to its new one — produces a field of parallel diagonal lines\n// that reverse direction in lockstep every course: the classic tricot\n// herringbone/chevron. A basic \"2 and 2\" lapping chain: the bar shogs 2\n// needle-spaces right, then 2 needle-spaces left, alternating every course,\n// so the offset itself is a triangle wave (0, +2, 0, +2, ...) rather than\n// an unbounded drift.\n//\n// One course completes every 500ms: first a 200ms eased lateral slide of\n// the whole guide row (a bright tick per needle, \"where the work is\n// happening\"), then the lap segments for that course commit in a single\n// instant redraw — never a gradually-growing line. A separate, decoupled\n// clock scrolls the accumulated field upward at one course-height per\n// 500ms, so completed courses feed off the top edge while the guide bar\n// itself stays anchored at a fixed frontier near the bottom. The field is\n// seeded with several courses synchronously at mount so it reads as\n// mid-build immediately rather than growing from nothing.\n// ---------------------------------------------------------------------------\n\nconst COURSE_PERIOD_MS = 500; // one course (shog + lap) per 500ms\nconst SHOG_SLIDE_MS = 200; // eased lateral slide before the lap segment lands\nconst SHOG_SPACES = 2; // \"2 and 2\" tricot chain: 2 needle-spaces per shog\nconst PITCH_DIVISOR = 27; // needle pitch derives from the smaller dimension\nconst PITCH_MIN = 8;\nconst PITCH_MAX = 14;\nconst COURSE_HEIGHT_RATIO = 9 / 11; // ~9px course height at ~11px pitch\nconst SETTLED_ALPHA = 0.62;\nconst GUIDE_ALPHA = 0.95;\nconst LINE_WIDTH = 1.15;\n\nfunction easeOutCubic(x: number): number {\n  const p = 1 - x;\n  return 1 - p * p * p;\n}\n\ninterface CourseRecord {\n  k: number; // course index, worldY = k * courseHeight\n  offsetPx: number; // committed lateral offset of the guide bar for this course\n}\n\nexport interface WarpKnitTricotLappingProps {\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nexport function WarpKnitTricotLapping({\n  className = \"\",\n}: WarpKnitTricotLappingProps) {\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 width = 0;\n    let height = 0;\n    let sized = false;\n    let disposed = false;\n    let visible = true;\n\n    let pitch = 11;\n    let courseHeight = 9;\n    let cols = 0;\n    let frontierY = 0; // fixed y of the active guide bar / newest course\n\n    let history: CourseRecord[] = [];\n    let k = 0; // next course index to commit\n    let offsetPrev = 0; // committed offset (px) at the last committed course\n    let offsetTarget = 0; // offset (px) this course is shogging toward\n    let direction = 1; // +1 = right, -1 = left, flips every course\n    let courseStart = 0; // ms timestamp the current course cycle began\n    let committed = false; // has this course's segment already been drawn?\n    let baseScroll = 0; // ms-based accumulated scroll at last course boundary\n\n    const readTokens = () => {\n      fg = getComputedStyle(canvas).color;\n    };\n\n    const resize = () => {\n      const rect = canvas.getBoundingClientRect();\n      if (rect.width < 4 || rect.height < 4) {\n        sized = false;\n        return;\n      }\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      canvas.width = Math.max(1, Math.round(rect.width * dpr));\n      canvas.height = Math.max(1, Math.round(rect.height * dpr));\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      width = rect.width;\n      height = rect.height;\n\n      const smaller = Math.min(width, height);\n      pitch = Math.min(PITCH_MAX, Math.max(PITCH_MIN, smaller / PITCH_DIVISOR));\n      courseHeight = pitch * COURSE_HEIGHT_RATIO;\n      cols = Math.max(6, Math.round(width / pitch) + 2);\n      frontierY = height - courseHeight;\n      sized = true;\n    };\n\n    // world-y of a committed course (k=0 is the oldest ever seen) mapped to\n    // screen space given how far the field has scrolled since it landed.\n    const screenY = (courseK: number, scrollNow: number): number =>\n      frontierY - (scrollNow - courseK * courseHeight);\n\n    const scrollAt = (nowMs: number): number =>\n      baseScroll + ((nowMs - courseStart) / COURSE_PERIOD_MS) * courseHeight;\n\n    const commitCourse = () => {\n      history.push({ k, offsetPx: offsetTarget });\n      k += 1;\n      committed = true;\n      // cull courses once they've scrolled fully off the top\n      const scrollNow = scrollAt(courseStart + COURSE_PERIOD_MS);\n      while (history.length > 1) {\n        const first = history[0];\n        if (!first) break;\n        if (screenY(first.k, scrollNow) < -courseHeight) history.shift();\n        else break;\n      }\n    };\n\n    const advanceCourse = () => {\n      baseScroll = scrollAt(courseStart + COURSE_PERIOD_MS);\n      courseStart += COURSE_PERIOD_MS;\n      offsetPrev = offsetTarget;\n      direction = -direction;\n      offsetTarget = offsetPrev + direction * SHOG_SPACES * pitch;\n      committed = false;\n    };\n\n    const draw = (nowMs: number) => {\n      if (!sized) return;\n      ctx.clearRect(0, 0, width, height);\n\n      const scrollNow = scrollAt(nowMs);\n      const tLocal = nowMs - courseStart;\n      const slideT = Math.min(1, Math.max(0, tLocal / SHOG_SLIDE_MS));\n      const guideOffset = committed\n        ? offsetTarget\n        : offsetPrev + (offsetTarget - offsetPrev) * easeOutCubic(slideT);\n\n      ctx.strokeStyle = fg;\n      ctx.lineWidth = LINE_WIDTH;\n      ctx.lineCap = \"round\";\n\n      // settled lap texture — every committed course-to-course pair\n      ctx.globalAlpha = SETTLED_ALPHA;\n      ctx.beginPath();\n      for (let i = 1; i < history.length; i++) {\n        const a = history[i - 1];\n        const b = history[i];\n        if (!a || !b) continue;\n        const ay = screenY(a.k, scrollNow);\n        const by = screenY(b.k, scrollNow);\n        if (ay < -courseHeight && by < -courseHeight) continue;\n        for (let n = 0; n < cols; n++) {\n          const baseX = (n + 0.5) * pitch;\n          ctx.moveTo(baseX + a.offsetPx, ay);\n          ctx.lineTo(baseX + b.offsetPx, by);\n        }\n      }\n      ctx.stroke();\n\n      // guide bar indicator — one short tick per needle at the fixed\n      // frontier, laterally offset by the guide's current (possibly still\n      // sliding) position; this is the one thing to visually track each\n      // course, moving right/left before the next commit lands the lap.\n      const tick = courseHeight * 0.4;\n      ctx.globalAlpha = GUIDE_ALPHA;\n      ctx.beginPath();\n      for (let n = 0; n < cols; n++) {\n        const x = (n + 0.5) * pitch + guideOffset;\n        ctx.moveTo(x, frontierY - tick);\n        ctx.lineTo(x, frontierY + tick);\n      }\n      ctx.stroke();\n      ctx.globalAlpha = 1;\n    };\n\n    // Synchronously build `count` committed courses ending in a settled\n    // (just-landed, never mid-slide) state, then anchor the clock so the\n    // newest course sits exactly at the frontier. Used both to seed the\n    // live field to \"mid-build, filling most of the panel\" at t0 (rather\n    // than growing from nothing over ~15s) and to produce the reduced-motion\n    // LAP_SETTLED freeze frame — the same construction, just never ticked.\n    const seedField = (nowMs: number, count: number) => {\n      history = [];\n      k = 0;\n      offsetPrev = 0;\n      offsetTarget = SHOG_SPACES * pitch;\n      direction = 1;\n      for (let c = 0; c < count; c++) {\n        history.push({ k, offsetPx: offsetTarget });\n        k += 1;\n        offsetPrev = offsetTarget;\n        direction = -direction;\n        offsetTarget = offsetPrev + direction * SHOG_SPACES * pitch;\n      }\n      committed = true;\n      courseStart = nowMs;\n      baseScroll = (count - 1) * courseHeight;\n    };\n\n    const coursesToFill = () => Math.ceil(height / courseHeight) + 2;\n\n    let raf = 0;\n    const GAP_RESET_MS = COURSE_PERIOD_MS * 20; // long tab-hidden gap: resync instead of replay\n\n    const loop = (now: number) => {\n      raf = 0;\n      if (!visible || !sized) return;\n\n      if (now - courseStart > GAP_RESET_MS) {\n        seedField(now, coursesToFill());\n      } else {\n        // commit the course being left BEFORE advancing off it, every time,\n        // so `k`/`baseScroll` can never skew apart on a multi-course catch-up\n        while (now - courseStart >= COURSE_PERIOD_MS) {\n          if (!committed) commitCourse();\n          advanceCourse();\n        }\n        if (now - courseStart >= SHOG_SLIDE_MS && !committed) commitCourse();\n      }\n\n      draw(now);\n      raf = requestAnimationFrame(loop);\n    };\n\n    const start = () => {\n      resize();\n      if (!sized) return;\n      const nowMs = performance.now();\n      seedField(nowMs, coursesToFill());\n      if (reduced) {\n        draw(nowMs);\n        return;\n      }\n      cancelAnimationFrame(raf);\n      raf = requestAnimationFrame(loop);\n    };\n\n    const mo = new MutationObserver(() => {\n      readTokens();\n      if (reduced) {\n        // re-anchor the frozen clock so scrollAt() still maps the same\n        // settled field onto the frontier after an arbitrary real-time gap\n        courseStart = performance.now();\n        draw(courseStart);\n      }\n    });\n    mo.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    let resizeTimer = 0;\n    const ro = new ResizeObserver(() => {\n      window.clearTimeout(resizeTimer);\n      resizeTimer = window.setTimeout(() => {\n        if (disposed) return;\n        cancelAnimationFrame(raf);\n        raf = 0;\n        start();\n      }, 80);\n    });\n    ro.observe(canvas);\n\n    const io = new IntersectionObserver((entries) => {\n      visible = entries[0]?.isIntersecting ?? true;\n      if (visible && !reduced && sized && !raf) {\n        raf = requestAnimationFrame(loop);\n      }\n    });\n    io.observe(canvas);\n\n    readTokens();\n    start();\n\n    return () => {\n      disposed = true;\n      cancelAnimationFrame(raf);\n      window.clearTimeout(resizeTimer);\n      mo.disconnect();\n      ro.disconnect();\n      io.disconnect();\n    };\n  }, []);\n\n  return (\n    <canvas\n      ref={canvasRef}\n      aria-hidden\n      className={`block h-full w-full text-foreground ${className}`}\n    />\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/warp-knit-tricot-lapping.tsx"
    }
  ],
  "meta": {
    "collection": "core",
    "tags": [
      "background",
      "divider",
      "canvas",
      "knit",
      "textile",
      "generative",
      "chevron",
      "ambient"
    ],
    "instruction": "Build <WarpKnitTricotLapping className?> as a card-scale <canvas w-full h-full>, ambient/autoplay only with no pointer interaction. GEOMETRY: needle pitch is derived from the container's smaller dimension, clamped to 8-14px around a ~11px target (pitch = clamp(min(w,h)/27, 8, 14)); needle columns = round(w/pitch)+2 so the field slightly overhangs both edges; course height = pitch * 9/11 (~9px at ~11px pitch), matching the spec's real-numbers ratio. STATE MACHINE: every needle in a row shares one guide-bar offset (in px), tracked as offsetPrev/offsetTarget. A course cycle is exactly 500ms: for the first 200ms the guide bar (a bright full-width horizontal line pinned at a fixed frontier y = height - courseHeight, the 'where the work is happening' indicator) eases from offsetPrev to offsetTarget via easeOutCubic; at the 200ms mark the course COMMITS in one instant redraw (never a gradually-growing line) — a CourseRecord {k, offsetPx: offsetTarget} is pushed to history and every needle's diagonal lap segment for that course becomes part of the settled texture. At the 500ms mark the course advances: offsetPrev = offsetTarget, direction flips sign, offsetTarget = offsetPrev + direction * 2 * pitch — a fixed 'shog 2 needle-spaces right, then 2 left' tricot chain, so offset is a triangle wave (0, +2*pitch, 0, +2*pitch, ...) rather than unbounded drift. RENDER: settled lap texture is one stroked path built by iterating consecutive history pairs and, for every needle column n, drawing a line from (baseX_n + courseA.offsetPx, screenY(courseA)) to (baseX_n + courseB.offsetPx, screenY(courseB)) — this produces the parallel-line chevron field, reversing direction every course. A field-scroll clock, decoupled from the course-commit cadence but running at the same real rate (one course-height of upward scroll per 500ms, via a continuous ms-based formula rather than a discrete jump), maps each committed course's fixed world position to a screen y; courses are culled from history once they scroll more than one course-height above the top edge. Stroke is `--foreground` via `currentColor` (canvas className carries text-foreground, colour read once with getComputedStyle(canvas).color and re-read on a MutationObserver watching documentElement's class attribute — no colour literal anywhere), uniform 1.15px line weight, no fill; the settled texture strokes at 0.62 alpha and the guide-bar indicator strokes at 0.95 alpha so the eye can find the active course at a glance in both themes. No `--border` on the lap/chevron itself and no `--ns-accent` anywhere. RESTING LOOP: at any instant the canvas shows a zigzag chevron mid-build with the guide bar mid-shog or freshly settled; over 2.5s roughly 5 courses commit with at least 2 visible chevron reversals, and by 5s the chevron pattern visible at t0 has scrolled off the top edge and been culled from history — alive and unbounded with zero input. LIFECYCLE: dpr clamped to 2, a debounced ResizeObserver re-measures pitch/courseHeight/frontier and restarts the course clock, an IntersectionObserver pauses the rAF loop when the canvas scrolls off-screen and resumes it (re-checking the elapsed-gap guard) when it re-enters, and a >20-course real-time gap (e.g. a long-backgrounded tab) triggers a silent resync of the field instead of replaying every missed course. prefers-reduced-motion synchronously replays 8 course commits (guaranteeing at least 2 direction reversals) at mount and freezes on LAP_SETTLED — the frame immediately after the 8th shog's lap segments have landed, guide bar at rest, never mid-slide — with no further ticking. Cleanup cancels the rAF, disconnects the MutationObserver/ResizeObserver/IntersectionObserver, and clears the resize debounce timer. The canvas is aria-hidden — it is decoration, no content a screen reader should announce. Props: className only."
  },
  "type": "registry:ui"
}