{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "progress-narrated",
  "title": "Progress Narrated",
  "description": "Determinate progress bar whose leading edge narrates each phase in typed mono, then docks it below the track as a timestamped milestone ledger.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/progress-narrated/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\nconst CHARSET = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#$%&@*+=\";\nconst CHAR_MS = 1000 / 24; // 24 chars/s typing cadence\nconst FILL_MS = 450; // fill glide per value change\nconst DOCK_MS = 220; // caption shrink-and-fade on phase completion\n\n// cubic-bezier(0.22, 1, 0.36, 1) solved via Newton–Raphson (house glideEase)\nfunction makeBezier(p1x: number, p1y: number, p2x: number, p2y: number) {\n  const cx = 3 * p1x;\n  const bx = 3 * (p2x - p1x) - cx;\n  const ax = 1 - cx - bx;\n  const cy = 3 * p1y;\n  const by = 3 * (p2y - p1y) - cy;\n  const ay = 1 - cy - by;\n  const sampleX = (t: number) => ((ax * t + bx) * t + cx) * t;\n  const sampleY = (t: number) => ((ay * t + by) * t + cy) * t;\n  const slopeX = (t: number) => (3 * ax * t + 2 * bx) * t + cx;\n  return (x: number) => {\n    if (x <= 0) return 0;\n    if (x >= 1) return 1;\n    let t = x;\n    for (let i = 0; i < 6; i++) {\n      const s = slopeX(t);\n      if (Math.abs(s) < 1e-6) break;\n      t -= (sampleX(t) - x) / s;\n    }\n    return sampleY(Math.min(1, Math.max(0, t)));\n  };\n}\nconst glideEase = makeBezier(0.22, 1, 0.36, 1);\nconst easeOutCubic = (p: number) => 1 - (1 - p) ** 3;\n\nfunction formatElapsed(ms: number) {\n  const total = Math.max(0, ms) / 1000;\n  const m = Math.floor(total / 60);\n  const s = total - m * 60;\n  return `${String(m).padStart(2, \"0\")}:${s < 10 ? \"0\" : \"\"}${s.toFixed(1)}`;\n}\n\nexport type ChroniclePhase = {\n  /** progress percent (0-100] at which this phase completes */\n  at: number;\n  /** mono caption typed at the leading edge, docked below on completion */\n  label: string;\n};\n\ntype Milestone = { at: number; label: string; elapsed: number };\n\nconst DEFAULT_PHASES: ChroniclePhase[] = [\n  { at: 25, label: \"ASSETS\" },\n  { at: 60, label: \"COMPILE\" },\n  { at: 100, label: \"READY\" },\n];\n\n// ---------------------------------------------------------------------------\n// ChronicleBar — determinate progress bar whose fill front is a live\n// narrator: a mono caption types itself at the leading edge (first three\n// glyphs scramble-decode before locking), then shrinks and docks below the\n// track as a timestamped milestone tick. A finished bar reads as a ledger of\n// everything the loader did. Fill/cursor/caption are direct-DOM writes from\n// one rAF loop that sleeps whenever nothing is easing or typing; a\n// decreasing `value` resets the ledger for a new run.\n// ---------------------------------------------------------------------------\nexport function ChronicleBar({\n  value = 0,\n  phases = DEFAULT_PHASES,\n  className = \"\",\n  \"aria-label\": ariaLabel = \"Progress\",\n}: {\n  /** progress 0-100 (controlled); a decrease resets the ledger */\n  value?: number;\n  /** phases in ascending order of `at` */\n  phases?: ChroniclePhase[];\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n  /** accessible name for the progress bar. Default \"Progress\". */\n  \"aria-label\"?: string;\n}) {\n  const trackRef = useRef<HTMLDivElement>(null);\n  const fillRef = useRef<HTMLDivElement>(null);\n  const cursorRef = useRef<HTMLDivElement>(null);\n  const captionRef = useRef<HTMLSpanElement>(null);\n  const pushRef = useRef<((v: number, reset: boolean) => void) | null>(null);\n  const prevRef = useRef<number | null>(null);\n  const [milestones, setMilestones] = useState<Milestone[]>([]);\n\n  // content-derived key: rebuild the narrator only when the phase sequence\n  // itself changes, not on every inline-array re-render from the consumer\n  const phasesKey = phases.map((p) => `${p.at}:${p.label}`).join(\"|\");\n\n  useEffect(() => {\n    const track = trackRef.current;\n    const fill = fillRef.current;\n    const cursor = cursorRef.current;\n    const caption = captionRef.current;\n    if (!track || !fill || !cursor || !caption) return;\n\n    const reduced = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches;\n    const list = [...phases].sort((a, b) => a.at - b.at);\n\n    // hot-path state — locals only, never React state\n    let raf = 0;\n    let display = 0; // eased fill percent\n    let from = 0;\n    let to = 0;\n    let animStart = -1; // -1 = width settled\n    let pi = 0; // index of the phase currently narrating\n    let capState: \"typing\" | \"settled\" | \"docking\" | \"done\" = \"done\";\n    let capScale = 1;\n    let typeStart = 0;\n    let dockStart = 0;\n    let pending: Milestone | null = null;\n    let clockStart = performance.now();\n    let trackW = track.clientWidth;\n    let captionW = 0;\n    let lastLen = -1;\n\n    const paintPositions = () => {\n      fill.style.width = `${display}%`;\n      const px = (display / 100) * trackW;\n      cursor.style.transform = `translate3d(${Math.min(\n        Math.max(0, px - 1),\n        Math.max(0, trackW - 2)\n      )}px,0,0)`;\n      // narrator done: the cursor block has nothing left to say\n      cursor.style.opacity = display >= 100 && animStart < 0 ? \"0\" : \"1\";\n      const cx = Math.min(Math.max(0, px), Math.max(0, trackW - captionW));\n      caption.style.transform = `translate3d(${cx}px,0,0) scale(${capScale})`;\n    };\n\n    const resetRun = (v: number, now: number) => {\n      display = Math.min(100, Math.max(0, v));\n      animStart = -1;\n      pi = 0;\n      clockStart = now;\n      pending = null;\n      capState = list.length ? \"typing\" : \"done\";\n      capScale = 1;\n      typeStart = now;\n      lastLen = -1;\n      captionW = 0;\n      caption.textContent = \"\";\n      caption.style.opacity = \"1\";\n      setMilestones([]);\n    };\n\n    const loop = (now: number) => {\n      let busy = false;\n\n      if (animStart >= 0) {\n        const p = (now - animStart) / FILL_MS;\n        if (p >= 1) {\n          display = to;\n          animStart = -1;\n        } else {\n          display = from + (to - from) * glideEase(p);\n          busy = true;\n        }\n      }\n\n      let ph = list[pi];\n\n      // phase completion: the fill front crossed the phase mark\n      if (ph && capState !== \"docking\" && capState !== \"done\" && display >= ph.at) {\n        capState = \"docking\";\n        dockStart = now;\n        pending = { at: ph.at, label: ph.label, elapsed: now - clockStart };\n      }\n\n      if (capState === \"typing\" && ph) {\n        const n = Math.min(\n          ph.label.length,\n          Math.floor((now - typeStart) / CHAR_MS)\n        );\n        let text = ph.label.slice(0, n);\n        if (n < ph.label.length) {\n          busy = true;\n          // decrypt dose: each of the first 3 glyphs churns ~2 frames, locks\n          if (n < 3) {\n            text += CHARSET[Math.floor(Math.random() * CHARSET.length)];\n          }\n        } else {\n          capState = \"settled\";\n        }\n        caption.textContent = text;\n        if (text.length !== lastLen) {\n          lastLen = text.length;\n          captionW = caption.offsetWidth;\n        }\n      } else if (capState === \"docking\") {\n        const p = Math.min(1, (now - dockStart) / DOCK_MS);\n        const e = easeOutCubic(p);\n        capScale = 1 - 0.08 * e;\n        caption.style.opacity = String(1 - e);\n        if (p < 1) {\n          busy = true;\n        } else {\n          const m = pending;\n          pending = null;\n          if (m) setMilestones((prev) => [...prev, m]);\n          pi += 1;\n          ph = list[pi];\n          capScale = 1;\n          if (ph) {\n            capState = \"typing\";\n            typeStart = now;\n            lastLen = -1;\n            captionW = 0;\n            caption.textContent = \"\";\n            caption.style.opacity = \"1\";\n            busy = true;\n          } else {\n            capState = \"done\";\n            caption.textContent = \"\";\n          }\n        }\n      }\n\n      paintPositions();\n      raf = busy ? requestAnimationFrame(loop) : 0; // sleep when settled\n    };\n    const wake = () => {\n      if (!raf) raf = requestAnimationFrame(loop);\n    };\n\n    const push = (v: number, reset: boolean) => {\n      const now = performance.now();\n      const cl = Math.min(100, Math.max(0, v));\n      if (reset) {\n        resetRun(cl, now);\n      } else if (cl !== display) {\n        from = display;\n        to = cl;\n        animStart = now;\n      }\n      wake();\n    };\n\n    // reduced motion: fill jumps, captions render fully typed, ticks land now\n    const pushReduced = (v: number, reset: boolean) => {\n      const now = performance.now();\n      if (reset) resetRun(v, now);\n      display = Math.min(100, Math.max(0, v));\n      const done: Milestone[] = [];\n      while (pi < list.length) {\n        const ph = list[pi];\n        if (!ph || display < ph.at) break;\n        done.push({ at: ph.at, label: ph.label, elapsed: now - clockStart });\n        pi += 1;\n      }\n      if (done.length) setMilestones((prev) => [...prev, ...done]);\n      const active = list[pi];\n      caption.textContent = active ? active.label : \"\";\n      caption.style.opacity = \"1\";\n      capScale = 1;\n      capState = active ? \"settled\" : \"done\";\n      captionW = caption.offsetWidth;\n      paintPositions();\n    };\n\n    pushRef.current = reduced ? pushReduced : push;\n    resetRun(0, performance.now());\n    pushRef.current(prevRef.current ?? 0, false);\n\n    const ro = new ResizeObserver(() => {\n      trackW = track.clientWidth;\n      if (reduced) paintPositions();\n      else wake(); // one paint pass, then back to sleep\n    });\n    ro.observe(track);\n\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      pushRef.current = null;\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps -- keyed on phasesKey, not the phases reference\n  }, [phasesKey]);\n\n  useEffect(() => {\n    const prev = prevRef.current;\n    prevRef.current = value;\n    pushRef.current?.(value, prev !== null && value < prev);\n  }, [value]);\n\n  const clamped = Math.min(100, Math.max(0, value));\n\n  return (\n    <div className={className}>\n      <style>{`@keyframes ns-chronicle-in{from{opacity:0}to{opacity:1}}\n.ns-chronicle-in{animation:ns-chronicle-in 260ms ease-out both}\n@media (prefers-reduced-motion: reduce){.ns-chronicle-in{animation:none}}`}</style>\n\n      {/* narrator caption, anchored to the leading edge */}\n      <div className=\"relative h-6 mb-1\" aria-hidden>\n        <span\n          ref={captionRef}\n          className=\"absolute bottom-0 left-0 origin-bottom-left whitespace-nowrap font-mono text-sm text-foreground will-change-transform\"\n        />\n      </div>\n\n      {/* track: hairline border, transparent bed */}\n      <div\n        ref={trackRef}\n        role=\"progressbar\"\n        aria-valuemin={0}\n        aria-valuemax={100}\n        aria-valuenow={Math.round(clamped)}\n        aria-label={ariaLabel}\n        className=\"relative h-1 rounded-full border border-border\"\n      >\n        <div\n          ref={fillRef}\n          className=\"absolute inset-y-0 left-0 w-0 rounded-full bg-foreground\"\n        />\n        {/* leading-edge cursor block — the only accent on the page */}\n        <div\n          ref={cursorRef}\n          className=\"absolute -top-0.5 left-0 h-2 w-0.5 bg-ns-accent transition-opacity duration-300\"\n        />\n      </div>\n\n      {/* docked milestone ledger */}\n      <div className=\"relative mt-2 h-7\" aria-hidden>\n        {milestones.map((m) => (\n          <div key={`${m.at}-${m.label}`} className=\"ns-chronicle-in\">\n            <span\n              className=\"absolute top-0 h-2 w-0.5 -translate-x-1/2 bg-ns-muted\"\n              style={{ left: `${m.at}%` }}\n            />\n            <span\n              className=\"absolute top-3 whitespace-nowrap font-mono text-[11px] tracking-wide text-ns-muted\"\n              style={{\n                left: `${m.at}%`,\n                transform:\n                  m.at >= 90\n                    ? \"translateX(-100%)\"\n                    : m.at > 10\n                      ? \"translateX(-50%)\"\n                      : undefined,\n              }}\n            >\n              {m.label} {formatElapsed(m.elapsed)}\n            </span>\n          </div>\n        ))}\n      </div>\n\n      {/* screen readers hear the ledger as it accrues */}\n      <div aria-live=\"polite\" className=\"sr-only\">\n        {milestones\n          .map((m) => `${m.label} complete at ${formatElapsed(m.elapsed)}`)\n          .join(\", \")}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/progress-narrated.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": "core",
    "tags": [
      "progress",
      "loader",
      "typography",
      "mono",
      "ledger",
      "micro-interaction"
    ],
    "instruction": "A determinate progress bar whose fill front is a live narrator, pure DOM and zero deps. Track: 4px tall, transparent bed with a hairline border; fill #ededed; the leading edge is a 2px #006bff cursor block (the only accent), which fades out once the run completes. Fill width eases toward the controlled value (0-100) with cubic-bezier(0.22,1,0.36,1) over 450ms per value change, written direct-DOM (style.width/transform) from a single rAF loop that runs only while easing, typing, or docking, then sleeps. A font-mono text-sm caption is anchored to the leading edge (clamped inside track bounds, with a small gap below its baseline before the track) and types the current phase label at 24 chars/s; the first 3 glyphs scramble-decode from an A-Z/0-9/symbol charset for ~2 frames each before locking (a small text-decrypt dose). When the fill crosses a phase's `at` percent, the caption scales to 0.92 over 220ms ease-out (origin bottom-left) and fades, and a milestone docks below the track: a 2px x 8px vertical tick at the phase's percent position plus label and elapsed mm:ss.s in font-mono text-[11px] text-ns-muted, edge-clamped so labels never overflow; milestones fade in over 260ms. A finished bar reads as a ledger of everything the loader did. Props: value 0-100 (controlled, a decrease resets the ledger and elapsed clock), phases {at, label}[]. Accessibility: progressbar role with live aria-valuenow, milestones announced through a visually-hidden polite live region. Under prefers-reduced-motion the fill jumps instantly, captions render fully typed, and ticks appear immediately."
  },
  "type": "registry:ui"
}