{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "context-compaction-river",
  "title": "Context Compaction River",
  "description": "Context compaction drawn as a meandering river: live turns are points on one curving channel, and a compacted run of turns necks off into a reopenable, re-injectable oxbow lake sitting 12px off the channel.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/context-compaction-river/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useMemo, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from \"react\";\n\n// OxbowTurn — the live conversation drawn as one meandering channel in a\n// side rail. Every live turn is a point the channel passes through; the\n// curve itself is decorative (aria-hidden SVG) and carries no information a\n// screen reader needs, because the same facts live as plain text in a real\n// list right below it (\"Live context, N turns\" / \"Compacted: turns 3-9,\n// summarized 2 minutes ago\").\n//\n// When turns get folded into a summary, the component doesn't just drop a\n// row — the compaction keeps its own place in the sequence, and the channel\n// simply stops routing through it: the surviving points ease toward their\n// new (tighter) spacing over ~300ms, which is what \"the river gets shorter\"\n// looks like here, while the folded turns settle 12px off the channel as a\n// closed oxbow-lake shape carrying a token-count chip. That chip is a real,\n// focusable button (never aria-hidden) — opening it reveals the summary and\n// a re-inject action in a role=menu popover. Re-inject animates the lake\n// back onto the channel before the prop change actually removes it, so nothing\n// is left to imagination between \"click\" and \"the turns are back\".\n//\n// The trigger is deliberately open-only (a second click does not close it —\n// only Escape, an outside click, or choosing the menu item does). That is\n// what keeps a synthetic \"press\" pass (which clicks the first control once,\n// unconditionally) and a later scripted \"open this\" check from fighting over\n// the same toggle.\n//\n// Positions are computed from each item's index in the full sequence (turn\n// or compaction alike), so a compaction never needs its own bookkeeping of\n// \"where it used to be\" — it already has a slot. Turn dots and the channel\n// path animate via a direct rAF loop (setAttribute only, no React state on\n// the hot path, sleeps once settled) exactly like this repo's other\n// spring-driven diagrams. Everything else (the oxbow's 12px settle, its\n// entrance/exit) is a plain CSS transform+opacity transition, which reduced\n// motion turns off wholesale in favor of an instant fade-and-move.\n\nexport type OxbowTurnEntry = {\n  id: string;\n  /** short label, e.g. a turn number — used to build \"turns 3-9\" range text */\n  label: string;\n};\n\nexport type OxbowTurnItem =\n  | { kind: \"turn\"; id: string; label: string; preview?: string }\n  | {\n      kind: \"compaction\";\n      id: string;\n      /** the turns folded into this oxbow, oldest -> newest */\n      turns: OxbowTurnEntry[];\n      summary: string;\n      tokenCount: number;\n      /** precomputed, e.g. \"2 minutes ago\" — this component never touches the clock */\n      compactedAgo: string;\n    };\n\nexport interface OxbowTurnProps {\n  /** the full ordered sequence: live turns and compaction (oxbow) entries, interleaved */\n  items: OxbowTurnItem[];\n  /** fired once a re-inject has finished animating back onto the channel */\n  onReinject?: (compactionId: string) => void;\n  ariaLabel?: string;\n  className?: string;\n}\n\nconst ROW_H = 38;\nconst PAD_Y = 18;\nconst CX = 30; // channel centerline, px from the rail's left edge\nconst AMP1 = 11;\nconst AMP2 = 5;\nconst OFFSET = 12; // brief's \"12px off-channel\"\nconst DOT_R = 3;\nconst EASE_RATE = 0.24; // per-frame lerp toward target, ~300ms settle at 60fps\nconst SETTLE_EPS = 0.04;\nconst RAIL_W = 236;\n\ntype Pt = { x: number; y: number };\n\nfunction channelX(row: number): number {\n  return CX + AMP1 * Math.sin(row * 0.85) + AMP2 * Math.sin(row * 1.9 + 1.3);\n}\n\nfunction rowY(row: number): number {\n  return PAD_Y + row * ROW_H;\n}\n\n/** Catmull-Rom through `pts`, converted to cubic beziers (tension/6 form). */\nfunction smoothPath(pts: Pt[]): string {\n  if (pts.length === 0) return \"\";\n  if (pts.length === 1) return `M ${pts[0].x} ${pts[0].y}`;\n  let d = `M ${pts[0].x} ${pts[0].y}`;\n  for (let i = 0; i < pts.length - 1; i++) {\n    const p0 = pts[i - 1] ?? pts[i];\n    const p1 = pts[i];\n    const p2 = pts[i + 1];\n    const p3 = pts[i + 2] ?? p2;\n    const c1x = p1.x + (p2.x - p0.x) / 6;\n    const c1y = p1.y + (p2.y - p0.y) / 6;\n    const c2x = p2.x - (p3.x - p1.x) / 6;\n    const c2y = p2.y - (p3.y - p1.y) / 6;\n    d += ` C ${c1x} ${c1y}, ${c2x} ${c2y}, ${p2.x} ${p2.y}`;\n  }\n  return d;\n}\n\nfunction formatTok(n: number): string {\n  return Math.round(Math.max(0, n)).toLocaleString(\"en-US\");\n}\n\nfunction turnsLabel(turns: OxbowTurnEntry[]): string {\n  if (turns.length === 0) return \"no turns\";\n  if (turns.length === 1) return `turn ${turns[0].label}`;\n  return `turns ${turns[0].label}–${turns[turns.length - 1].label}`;\n}\n\ntype TurnPoint = { id: string; x: number; y: number; label: string; preview?: string; isLast: boolean };\ntype LakeRow = { id: string; item: Extract<OxbowTurnItem, { kind: \"compaction\" }>; x: number; y: number };\n\nfunction layout(items: OxbowTurnItem[]): { turns: TurnPoint[]; lakes: LakeRow[] } {\n  const turns: TurnPoint[] = [];\n  const lakes: LakeRow[] = [];\n  items.forEach((it, row) => {\n    const x = channelX(row);\n    const y = rowY(row);\n    if (it.kind === \"turn\") {\n      turns.push({ id: it.id, x, y, label: it.label, preview: it.preview, isLast: false });\n    } else {\n      lakes.push({ id: it.id, item: it, x, y });\n    }\n  });\n  if (turns.length > 0) turns[turns.length - 1].isLast = true;\n  return { turns, lakes };\n}\n\nexport function OxbowTurn({\n  items,\n  onReinject,\n  ariaLabel = \"Context compaction history\",\n  className = \"\",\n}: OxbowTurnProps) {\n  const rawId = useId();\n  const fid = rawId.replace(/[^a-zA-Z0-9-]/g, \"\");\n\n  const [reduced, setReduced] = useState(false);\n  const [openId, setOpenId] = useState<string | null>(null);\n  const [enterState, setEnterState] = useState<Record<string, \"start\" | \"end\">>({});\n  const [exitingIds, setExitingIds] = useState<Record<string, true>>({});\n  const [snap, setSnap] = useState(false);\n  const [liveMsg, setLiveMsg] = useState(\"\");\n\n  const wrapRef = useRef<HTMLDivElement | null>(null);\n  const popRef = useRef<HTMLDivElement | null>(null);\n  const triggerRefs = useRef<Map<string, HTMLButtonElement>>(new Map());\n  const pathElRef = useRef<SVGPathElement | null>(null);\n  const dotElsRef = useRef<Map<string, SVGCircleElement>>(new Map());\n  const dotWrapElsRef = useRef<Map<string, HTMLDivElement>>(new Map());\n  const posRef = useRef<Map<string, Pt>>(new Map());\n  const prevCompactionIdsRef = useRef<Set<string>>(new Set());\n  const cleanupsRef = useRef<Array<() => void>>([]);\n\n  useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    setReduced(mq.matches);\n    const onChange = () => setReduced(mq.matches);\n    mq.addEventListener(\"change\", onChange);\n    return () => mq.removeEventListener(\"change\", onChange);\n  }, []);\n\n  useEffect(() => () => {\n    for (const cancel of cleanupsRef.current) cancel();\n    cleanupsRef.current = [];\n  }, []);\n\n  const { turns: turnTargets, lakes: lakeRows } = useMemo(() => layout(items), [items]);\n  const H = Math.max(PAD_Y * 2, PAD_Y * 2 + Math.max(0, items.length - 1) * ROW_H);\n\n  const [initialD] = useState(() => smoothPath(layout(items).turns.map((t) => ({ x: t.x, y: t.y }))));\n\n  // channel + turn-dot animation: direct DOM writes, no React state on the hot path\n  useEffect(() => {\n    const targetMap = new Map(turnTargets.map((t) => [t.id, { x: t.x, y: t.y }]));\n    for (const id of Array.from(posRef.current.keys())) {\n      if (!targetMap.has(id)) posRef.current.delete(id);\n    }\n\n    if (reduced) {\n      for (const t of turnTargets) {\n        posRef.current.set(t.id, { x: t.x, y: t.y });\n        const el = dotElsRef.current.get(t.id);\n        el?.setAttribute(\"cx\", String(t.x));\n        el?.setAttribute(\"cy\", String(t.y));\n        const wrapEl = dotWrapElsRef.current.get(t.id);\n        if (wrapEl) {\n          wrapEl.style.left = `${t.x}px`;\n          wrapEl.style.top = `${t.y}px`;\n        }\n      }\n      pathElRef.current?.setAttribute(\"d\", smoothPath(turnTargets.map((t) => ({ x: t.x, y: t.y }))));\n      return;\n    }\n\n    let raf = 0;\n    const step = () => {\n      let moving = false;\n      const pts: Pt[] = [];\n      for (const t of turnTargets) {\n        const cur = posRef.current.get(t.id) ?? { x: t.x, y: t.y };\n        const nx = cur.x + (t.x - cur.x) * EASE_RATE;\n        const ny = cur.y + (t.y - cur.y) * EASE_RATE;\n        if (Math.abs(t.x - nx) > SETTLE_EPS || Math.abs(t.y - ny) > SETTLE_EPS) moving = true;\n        posRef.current.set(t.id, { x: nx, y: ny });\n        pts.push({ x: nx, y: ny });\n        const el = dotElsRef.current.get(t.id);\n        el?.setAttribute(\"cx\", String(nx));\n        el?.setAttribute(\"cy\", String(ny));\n        const wrapEl = dotWrapElsRef.current.get(t.id);\n        if (wrapEl) {\n          wrapEl.style.left = `${nx}px`;\n          wrapEl.style.top = `${ny}px`;\n        }\n      }\n      pathElRef.current?.setAttribute(\"d\", smoothPath(pts));\n      if (moving) raf = requestAnimationFrame(step);\n    };\n    raf = requestAnimationFrame(step);\n    return () => cancelAnimationFrame(raf);\n  }, [turnTargets, reduced]);\n\n  // compaction enter + aria-live announcements\n  useEffect(() => {\n    const curIds = new Set(lakeRows.map((l) => l.id));\n    const prevIds = prevCompactionIdsRef.current;\n    const added = lakeRows.filter((l) => !prevIds.has(l.id));\n    const removedCount = Array.from(prevIds).filter((id) => !curIds.has(id)).length;\n\n    if (added.length > 0) {\n      if (!reduced) {\n        setEnterState((s) => {\n          const next = { ...s };\n          for (const l of added) next[l.id] = \"start\";\n          return next;\n        });\n        const raf1 = requestAnimationFrame(() => {\n          const raf2 = requestAnimationFrame(() => {\n            setEnterState((s) => {\n              const next = { ...s };\n              for (const l of added) if (next[l.id] === \"start\") next[l.id] = \"end\";\n              return next;\n            });\n          });\n          cleanupsRef.current.push(() => cancelAnimationFrame(raf2));\n        });\n        cleanupsRef.current.push(() => cancelAnimationFrame(raf1));\n      }\n      const first = added[0];\n      setLiveMsg(\n        `Compacted ${turnsLabel(first.item.turns)} into a ${formatTok(first.item.tokenCount)}-token summary.`\n      );\n      if (!reduced) {\n        setSnap(true);\n        const t = window.setTimeout(() => setSnap(false), 320);\n        cleanupsRef.current.push(() => window.clearTimeout(t));\n      }\n    } else if (removedCount > 0 && added.length === 0) {\n      // a removal this component didn't itself animate (e.g. reset) — still announce\n      setLiveMsg(\"Re-injected turns back into live context.\");\n    }\n    prevCompactionIdsRef.current = curIds;\n  }, [lakeRows, reduced]);\n\n  // outside click closes the open popover\n  useEffect(() => {\n    if (!openId) return;\n    const onDown = (e: PointerEvent) => {\n      const target = e.target as Node;\n      if (popRef.current?.contains(target)) return;\n      if (triggerRefs.current.get(openId)?.contains(target)) return;\n      setOpenId(null);\n    };\n    document.addEventListener(\"pointerdown\", onDown);\n    return () => document.removeEventListener(\"pointerdown\", onDown);\n  }, [openId]);\n\n  // focus the summary when a popover opens\n  useEffect(() => {\n    if (!openId) return;\n    const raf = requestAnimationFrame(() => {\n      popRef.current?.querySelector<HTMLElement>(\"[data-oxbow-summary]\")?.focus({ preventScroll: true });\n    });\n    return () => cancelAnimationFrame(raf);\n  }, [openId]);\n\n  const closeAndReturnFocus = (id: string) => {\n    setOpenId(null);\n    triggerRefs.current.get(id)?.focus({ preventScroll: true });\n  };\n\n  const handleReinject = (l: LakeRow) => {\n    if (reduced) {\n      setOpenId(null);\n      onReinject?.(l.id);\n      return;\n    }\n    setOpenId(null);\n    setExitingIds((s) => ({ ...s, [l.id]: true }));\n    const t = window.setTimeout(() => {\n      onReinject?.(l.id);\n      setExitingIds((s) => {\n        const next = { ...s };\n        delete next[l.id];\n        return next;\n      });\n    }, 260);\n    cleanupsRef.current.push(() => window.clearTimeout(t));\n  };\n\n  const onTriggerKeyDown = (l: LakeRow) => (e: ReactKeyboardEvent<HTMLButtonElement>) => {\n    if (e.key === \"ArrowDown\" && openId !== l.id) {\n      e.preventDefault();\n      setOpenId(l.id);\n    }\n  };\n\n  const onMenuKeyDown = (l: LakeRow) => (e: ReactKeyboardEvent<HTMLDivElement>) => {\n    if (e.key === \"Escape\") {\n      e.preventDefault();\n      closeAndReturnFocus(l.id);\n      return;\n    }\n    if (e.key === \"Tab\") {\n      setOpenId(null);\n      return;\n    }\n    const items_ = Array.from(e.currentTarget.querySelectorAll<HTMLElement>('[role=\"menuitem\"]'));\n    if (items_.length === 0) return;\n    const idx = items_.indexOf(document.activeElement as HTMLElement);\n    let next = -1;\n    if (e.key === \"ArrowDown\") next = (idx + 1) % items_.length;\n    else if (e.key === \"ArrowUp\") next = idx < 0 ? items_.length - 1 : (idx - 1 + items_.length) % items_.length;\n    else if (e.key === \"Home\") next = 0;\n    else if (e.key === \"End\") next = items_.length - 1;\n    if (next >= 0) {\n      e.preventDefault();\n      items_[next]?.focus({ preventScroll: true });\n    }\n  };\n\n  const turnCount = turnTargets.length;\n\n  return (\n    <div ref={wrapRef} role=\"group\" aria-label={ariaLabel} className={`ns-oxbow relative ${className}`}>\n      <style>{`\n.ns-oxbow-dot{animation:ns-oxbow-dot-in 220ms cubic-bezier(.22,1,.36,1)}\n.ns-oxbow-dot-current{fill:var(--foreground);animation:ns-oxbow-dot-in 220ms cubic-bezier(.22,1,.36,1),ns-oxbow-breathe 2.6s ease-in-out infinite}\n.ns-oxbow-channel{transition:none}\n.ns-oxbow-channel.ns-oxbow-snap{animation:ns-oxbow-snap 320ms cubic-bezier(.22,1,.36,1)}\n.ns-oxbow-lake-anim,.ns-oxbow-chip-wrap{transition:transform 300ms cubic-bezier(.16,1,.3,1),opacity 220ms ease-out}\n.ns-oxbow-lake-anim[data-state=\"entering\"],.ns-oxbow-chip-wrap[data-state=\"entering\"]{transform:translateX(0);opacity:0}\n.ns-oxbow-lake-anim[data-state=\"settled\"],.ns-oxbow-chip-wrap[data-state=\"settled\"]{transform:translateX(${OFFSET}px);opacity:1}\n.ns-oxbow-lake-anim[data-state=\"exiting\"],.ns-oxbow-chip-wrap[data-state=\"exiting\"]{transform:translateX(0);opacity:0;transition:transform 240ms cubic-bezier(.4,0,.9,.4),opacity 200ms ease-in}\n.ns-oxbow-chip-tip,.ns-context-compaction-river-tip{opacity:0;transition:opacity 150ms ease-out}\n[data-oxbow-trigger]:hover+.ns-oxbow-chip-tip,[data-oxbow-trigger]:focus+.ns-oxbow-chip-tip,[data-oxbow-trigger]:focus-visible+.ns-oxbow-chip-tip{opacity:1}\n[data-context-compaction-river-btn]:hover+.ns-context-compaction-river-tip,[data-context-compaction-river-btn]:focus+.ns-context-compaction-river-tip,[data-context-compaction-river-btn]:focus-visible+.ns-context-compaction-river-tip{opacity:1}\n@keyframes ns-oxbow-dot-in{from{opacity:0;transform:scale(.3)}to{opacity:1;transform:scale(1)}}\n@keyframes ns-oxbow-breathe{0%,100%{opacity:.75}50%{opacity:1}}\n@keyframes ns-oxbow-snap{0%{stroke-width:1.5}35%{stroke-width:2.6}100%{stroke-width:1.5}}\n@media (prefers-reduced-motion: reduce){\n  .ns-oxbow-dot,.ns-oxbow-dot-current{animation:none !important;opacity:1 !important;transform:none !important}\n  .ns-oxbow-channel{animation:none !important}\n  .ns-oxbow-lake-anim,.ns-oxbow-chip-wrap{transition:opacity 160ms ease-out !important;transform:translateX(${OFFSET}px) !important}\n  .ns-oxbow-lake-anim[data-state=\"entering\"],.ns-oxbow-chip-wrap[data-state=\"entering\"]{opacity:0 !important}\n  .ns-oxbow-lake-anim[data-state=\"exiting\"],.ns-oxbow-chip-wrap[data-state=\"exiting\"]{opacity:0 !important}\n  .ns-oxbow-chip-tip,.ns-context-compaction-river-tip{transition:none !important}\n}\n`}</style>\n\n      <div className=\"relative\" style={{ width: RAIL_W, height: H }}>\n        <svg\n          aria-hidden\n          width={RAIL_W}\n          height={H}\n          viewBox={`0 0 ${RAIL_W} ${H}`}\n          className=\"pointer-events-none absolute inset-0 block overflow-visible\"\n        >\n          <path\n            ref={pathElRef}\n            d={initialD}\n            className={`ns-oxbow-channel${snap ? \" ns-oxbow-snap\" : \"\"}`}\n            fill=\"none\"\n            stroke=\"var(--muted)\"\n            strokeWidth={1.5}\n            strokeLinecap=\"round\"\n          />\n\n          {lakeRows.map((l) => {\n            const state = exitingIds[l.id] ? \"exiting\" : enterState[l.id] === \"start\" ? \"entering\" : \"settled\";\n            return (\n              <g key={l.id} transform={`translate(${l.x} ${l.y})`}>\n                {/* animated as one rigid unit: at rest (translateX 12) this spans exactly\n                    anchor -> ellipse edge; the stub's own local span is kept constant so\n                    sliding the group is what \"grows the neck into place\" looks like */}\n                <g className=\"ns-oxbow-lake-anim\" data-state={state}>\n                  <path\n                    d={`M ${-OFFSET} 0 Q ${-OFFSET * 0.5} -6 0 0`}\n                    fill=\"none\"\n                    stroke=\"var(--muted)\"\n                    strokeWidth={1}\n                    strokeOpacity={0.55}\n                  />\n                  <ellipse\n                    cx={15}\n                    cy={0}\n                    rx={15}\n                    ry={9}\n                    fill=\"color-mix(in srgb, var(--border) 6%, transparent)\"\n                    stroke=\"var(--border)\"\n                    strokeWidth={1}\n                  />\n                </g>\n              </g>\n            );\n          })}\n\n          {turnTargets.map((t) => (\n            <circle\n              key={t.id}\n              ref={(el) => {\n                if (el) {\n                  dotElsRef.current.set(t.id, el);\n                  if (!posRef.current.has(t.id)) {\n                    el.setAttribute(\"cx\", String(t.x));\n                    el.setAttribute(\"cy\", String(t.y));\n                    posRef.current.set(t.id, { x: t.x, y: t.y });\n                  }\n                } else {\n                  dotElsRef.current.delete(t.id);\n                }\n              }}\n              r={t.isLast ? DOT_R + 1 : DOT_R}\n              className={t.isLast ? \"ns-oxbow-dot-current\" : \"ns-oxbow-dot\"}\n              fill={t.isLast ? undefined : \"var(--muted)\"}\n            >\n              <title>{t.preview ? `${t.label}: ${t.preview}` : t.label}</title>\n            </circle>\n          ))}\n        </svg>\n\n        {/* real interactive targets for the (decorative, aria-hidden) turn dots above:\n            positioned by the same rAF loop that drives the SVG circles (dotWrapElsRef),\n            so the hit target and its tooltip track the easing motion instead of a\n            React-controlled left/top that would snap ahead of the circle it sits on */}\n        {turnTargets.map((t) => {\n          const turnTipId = `${fid}-turn-tip-${t.id}`;\n          const hasPreview = Boolean(t.preview);\n          const accName = hasPreview ? `Turn ${t.label}: ${t.preview}` : `Turn ${t.label}`;\n          return (\n            <div\n              key={t.id}\n              ref={(el) => {\n                if (el) {\n                  dotWrapElsRef.current.set(t.id, el);\n                  const cur = posRef.current.get(t.id) ?? { x: t.x, y: t.y };\n                  el.style.left = `${cur.x}px`;\n                  el.style.top = `${cur.y}px`;\n                } else {\n                  dotWrapElsRef.current.delete(t.id);\n                }\n              }}\n              className=\"absolute -translate-x-1/2 -translate-y-1/2\"\n            >\n              <button\n                type=\"button\"\n                data-context-compaction-river-btn\n                aria-label={accName}\n                aria-describedby={hasPreview ? turnTipId : undefined}\n                className=\"block h-4 w-4 cursor-default rounded-full bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent\"\n              />\n              {hasPreview && (\n                <span\n                  id={turnTipId}\n                  role=\"tooltip\"\n                  className=\"ns-context-compaction-river-tip pointer-events-none absolute left-[calc(100%+8px)] top-1/2 z-20 block w-max max-w-[200px] -translate-y-1/2 whitespace-normal rounded-md border border-border px-2 py-1 font-mono text-[10px] leading-relaxed text-foreground shadow-lg\"\n                  style={{ background: \"color-mix(in srgb, var(--foreground) 4%, var(--background))\" }}\n                >\n                  Turn {t.label}: {t.preview}\n                </span>\n              )}\n            </div>\n          );\n        })}\n\n        {lakeRows.map((l) => {\n          const state = exitingIds[l.id] ? \"exiting\" : enterState[l.id] === \"start\" ? \"entering\" : \"settled\";\n          const isOpen = openId === l.id;\n          const menuId = `${fid}-menu-${l.id}`;\n          const tipId = `${fid}-tip-${l.id}`;\n          return (\n            <div\n              key={l.id}\n              className=\"ns-oxbow-chip-wrap absolute\"\n              data-state={state}\n              style={{ left: l.x, top: l.y - 12 }}\n            >\n              <button\n                ref={(el) => {\n                  if (el) triggerRefs.current.set(l.id, el);\n                  else triggerRefs.current.delete(l.id);\n                }}\n                type=\"button\"\n                data-oxbow-trigger\n                aria-haspopup=\"menu\"\n                aria-expanded={isOpen}\n                aria-controls={isOpen ? menuId : undefined}\n                aria-describedby={!isOpen ? tipId : undefined}\n                aria-label={`Compacted ${turnsLabel(l.item.turns)}, ${formatTok(l.item.tokenCount)} tokens. Open summary and re-inject.`}\n                onClick={() => setOpenId(l.id)}\n                onKeyDown={onTriggerKeyDown(l)}\n                className=\"flex cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-full border border-border px-2.5 py-1 font-mono text-[10px] tracking-wide text-muted transition-colors duration-150 hover:border-foreground/30 hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent\"\n                style={{ background: \"color-mix(in srgb, var(--foreground) 4%, var(--background))\" }}\n              >\n                <span aria-hidden>{l.item.turns.length}↩</span>\n                <span aria-hidden>{formatTok(l.item.tokenCount)} tok</span>\n              </button>\n\n              {!isOpen && (\n                <span\n                  id={tipId}\n                  role=\"tooltip\"\n                  className=\"ns-oxbow-chip-tip pointer-events-none absolute bottom-[calc(100%+8px)] left-0 z-20 block w-max max-w-[220px] whitespace-normal rounded-md border border-border px-2 py-1 font-mono text-[10px] leading-relaxed text-foreground shadow-lg\"\n                  style={{ background: \"color-mix(in srgb, var(--foreground) 4%, var(--background))\" }}\n                >\n                  {turnsLabel(l.item.turns)} pinched off the channel · re-injectable\n                </span>\n              )}\n\n              {isOpen && (\n                <div\n                  ref={popRef}\n                  id={menuId}\n                  className=\"absolute left-0 top-[calc(100%+8px)] z-10 w-60 rounded-md border border-border p-3 shadow-lg\"\n                  style={{ background: \"color-mix(in srgb, var(--foreground) 3%, var(--background))\" }}\n                >\n                  <p\n                    data-oxbow-summary\n                    tabIndex={-1}\n                    className=\"text-xs leading-relaxed text-foreground outline-none\"\n                  >\n                    {l.item.summary}\n                  </p>\n                  <dl className=\"mt-2.5 space-y-1 font-mono text-[10px] uppercase tracking-wide text-muted\">\n                    <div className=\"flex justify-between gap-2\">\n                      <dt>Turns</dt>\n                      <dd className=\"text-foreground\">{turnsLabel(l.item.turns)}</dd>\n                    </div>\n                    <div className=\"flex justify-between gap-2\">\n                      <dt>Summarized</dt>\n                      <dd className=\"text-foreground\">{l.item.compactedAgo}</dd>\n                    </div>\n                    <div className=\"flex justify-between gap-2\">\n                      <dt>Tokens</dt>\n                      <dd className=\"text-foreground\">{formatTok(l.item.tokenCount)}</dd>\n                    </div>\n                  </dl>\n                  <div\n                    role=\"menu\"\n                    aria-label={`Actions for compacted ${turnsLabel(l.item.turns)}`}\n                    onKeyDown={onMenuKeyDown(l)}\n                    className=\"mt-3 border-t border-border pt-2.5\"\n                  >\n                    <button\n                      role=\"menuitem\"\n                      type=\"button\"\n                      data-oxbow-reinject\n                      onClick={() => handleReinject(l)}\n                      className=\"w-full cursor-pointer rounded-sm px-2 py-1.5 text-left text-xs text-foreground transition-colors hover:bg-[color-mix(in_srgb,var(--foreground)_6%,transparent)] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-accent\"\n                    >\n                      Re-inject into live context\n                    </button>\n                  </div>\n                </div>\n              )}\n            </div>\n          );\n        })}\n      </div>\n\n      <ul className=\"mt-3 space-y-1 font-mono text-[11px] leading-relaxed text-muted\">\n        <li>\n          Live context, {turnCount} turn{turnCount === 1 ? \"\" : \"s\"}\n        </li>\n        {lakeRows.map((l) => (\n          <li key={l.id}>\n            Compacted: {turnsLabel(l.item.turns)}, summarized {l.item.compactedAgo}\n          </li>\n        ))}\n      </ul>\n\n      <p aria-live=\"polite\" className=\"sr-only\">\n        {liveMsg}\n      </p>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/context-compaction-river.tsx"
    }
  ],
  "meta": {
    "collection": "core",
    "tags": [
      "context-window",
      "compaction",
      "agent",
      "llm",
      "svg",
      "diagram",
      "history",
      "mono",
      "menu",
      "undo",
      "accessibility"
    ],
    "instruction": "OxbowTurn draws the live conversation as one gently meandering channel in a side rail: a controlled `items` prop is a single ordered sequence of turn entries and compaction entries, and every entry — of either kind — gets one row slot, spaced evenly down the rail. Turn entries are plotted as points on the channel (x from a deterministic two-harmonic sine so the curve reads as organic without any randomness or physics sim); a fresh Catmull-Rom-to-bezier pass through only the turn points draws the channel path. Compaction entries do not contribute a channel point at all, so when a run of turns gets folded into one compaction, the channel simply stops routing through that stretch — the surviving points ease toward their new, tighter spacing over roughly 300ms (a direct rAF loop writing cx/cy/d via setAttribute, no React state on the hot path, sleeping once settled, in the same idiom as this registry's other spring-driven diagrams), which is what 'compaction shortens the river' looks like here. The folded turns settle as a closed oxbow-lake shape — a small filled loop (`color-mix(in srgb, var(--border) 6%, transparent)` fill, `--border` stroke) plus a short connecting stub, both purely decorative and `aria-hidden` — 12px off the channel, animated in on a 300ms cubic-bezier(0.16,1,0.3,1) transform+opacity transition (ease-out-expo) the instant the compaction first appears in `items`. Carried on the lake is a real, always-focusable Geist Mono token-count chip (`{turnsFolded}↩ {tokenCount} tok`, never `aria-hidden`) — clicking it opens a small popover with the plain-language summary, a turns/summarized-at/token-count `<dl>`, and a `role=menu` containing one `role=menuitem`: 're-inject into live context'. Choosing it drifts the lake back onto the channel (reverse transition, ~260ms) before the `onReinject(id)` callback actually fires and the consumer's state update splices the folded turns back into `items` as live turns again — nothing is removed from view until the return trip has finished playing, and a brief stroke-width pulse on the channel marks the splice. The chip trigger is deliberately open-only: a second click while already open is a no-op (only Escape, an outside click, or choosing the menu item closes it), so a scripted 'click the first control' pass and a later 'now click this same control and expect it open' check never fight over the same toggle. Hovering or focusing the chip (without clicking) also reveals a small, token-styled tooltip above it — 'turns {a}–{b} pinched off the channel · re-injectable' — so a user can preview a branch point before committing to opening the full menu; it's wired via `aria-describedby` and hides once the menu is open. The drawing (channel, dots, lake shapes) is entirely `aria-hidden` and carries zero information a screen reader needs beyond redundancy — the actual facts live as plain, always-visible text in a real list right below it: 'Live context, {n} turns' plus one 'Compacted: turns {a}–{b}, summarized {when}' line per oxbow, and every compaction/re-injection additionally announces itself through an `aria-live=polite` region ('Compacted turns 3–9 into a 1,840-token summary.' / re-inject's own announcement). Each turn dot also gets a real, invisible interactive target overlaid on top of it (an unstyled focusable `<button>` positioned by the same rAF loop that eases the decorative circle, so the hit target tracks the dot through every re-layout instead of snapping ahead of it) — hovering or Tab-focusing it reveals a token-styled tooltip with that turn's preview (`aria-describedby`), and its accessible name is 'Turn {label}: {preview}' (or just 'Turn {label}' when no preview was supplied); the newest turn renders slightly larger and gently breathes to mark 'currently live'. `prefers-reduced-motion` replaces every transition with an instant fade-and-move (channel positions snap directly to target, no rAF loop runs at all, lake enter/exit is a 160ms opacity-only cross-fade already sitting at its settled 12px offset) — every state stays fully legible and functional, just static. Props: `items` (`OxbowTurnItem[]`, a union of `{kind:'turn', id, label, preview?}` and `{kind:'compaction', id, turns, summary, tokenCount, compactedAgo}` — fully controlled, the component holds no business state, only UI state for which popover is open and the enter/exit animation phase), `onReinject`, `ariaLabel`, `className`. Dragging an oxbow back onto the channel was considered as an enhancement on top of the menu path but deliberately not built for v1 — the menu's re-inject item is the only path, already fully keyboard operable, and the brief is explicit that drag must never be the *only* way in. Demo: an agent-session card seeded with 10 live turns and two already-settled oxbows (turns 3–9 and turns 14–16) so the resting screenshot already shows the channel visibly shortened at two separate folds, each carrying its own independently-openable chip — legible as multiple branches, not a one-off — plus ADD TURN (streams one more live turn), COMPACT OLDEST (folds the next 2–4 oldest live turns into a fresh oxbow and plays the pinch), and RESET SESSION controls."
  },
  "type": "registry:ui"
}