# ChapterScrubber

> A rail of ticks, one per chapter, that swell like a dock under the pointer, with a card previewing the hovered chapter.

## Facts

- **Product**: Max
- **Family**: Motion
- **Global**: `window.DigitalCrew.ChapterScrubber`
- **Source**: `max-agent: src/components/ui/chapter-scrubber.tsx`
- **Import in the app**: `import { ChapterScrubber } from "@/components/ui/chapter-scrubber";`
- **Live preview**: /design/components/chapter-scrubber/preview.html
- **Page**: /design/components/chapter-scrubber

## Guidelines

**Use it for** jumping through a recording's chapters or a long agent run; `currentIndex` marks where playback or the agent is now.

**What you provide** (defaults in brackets):
- `chapters: { id, title, description?, meta? }[]`; `meta` is a small label such as a timestamp.
- `side` [`"right"`]: flips automatically near the viewport edge.
- `peakLength` [56], `restLength` [14] and `rowHeight` [10], in px; `radius` [4] rows.
- `currentIndex`, `onActiveChange(chapter | null, index)`, `onSelect(chapter, index)`, `label` [`"Chapters"`], `className`.

**Anatomy**:
- Ticks are `h-[2px] rounded-full bg-foreground`, or `bg-primary` for the current one. They rest at 22% opacity (55% current) and reach 100% at the crest, thickening to 1.4×.
- The card is `w-[260px] rounded-2xl border bg-popover px-4 py-3.5`, 20px from the rail, with the description clamped to 3 lines.

**Motion**: the swell falls off along a raised-cosine curve. The pointer is tracked with a spring (stiffness 700, damping 52, mass 0.5), and the swell rises and settles with a softer one (260/30/0.6). Under reduced motion the springs are skipped and the swell appears instantly.

**Accessibility**: the rail is a `listbox` with one tabbable option at a time. Arrow keys, Home and End move focus; Enter or Space selects.

**Don't** pass essential text as a non-string `description`: the card is `aria-hidden`, and only string descriptions reach each option's accessible label.

## API

```ts
export interface Chapter {
    /** Stable, unique identifier for the chapter. */
    id: string;
    /** Bold heading shown at the top of the preview card. */
    title: string;
    /** Supporting copy shown under the title (clamped to three lines). */
    description?: React.ReactNode;
    /** Small muted label rendered above the title (e.g. a timestamp or step no.). */
    meta?: React.ReactNode;
}
export interface ChapterScrubberProps {
    /** Chapters rendered top-to-bottom, one uniform tick each. */
    chapters: Chapter[];
    /** Which side the preview card opens toward. Auto-flips near a viewport edge. Default `"right"`. */
    side?: "left" | "right";
    /** Length a tick reaches at the crest of the magnification, in pixels. Default `56`. */
    peakLength?: number;
    /** Resting length of every tick, in pixels. Keep it small. Default `14`. */
    restLength?: number;
    /** Height of each row in pixels; the gap between ticks. Smaller = denser. Default `10`. */
    rowHeight?: number;
    /** Radius of the magnification wave, in rows — how far the rise reaches from the pointer. Default `4`. */
    radius?: number;
    /** Marks one chapter as the persistent "current" position (e.g. where an agent is now). */
    currentIndex?: number;
    /** Fires when the active (hovered/focused) chapter changes. */
    onActiveChange?: (chapter: Chapter | null, index: number) => void;
    /** Fires when a chapter is chosen via click, Enter or Space. */
    onSelect?: (chapter: Chapter, index: number) => void;
    /** Accessible name for the rail. Default `"Chapters"`. */
    label?: string;
    className?: string;
}
export declare function ChapterScrubber({ chapters, side, peakLength, restLength, rowHeight, radius, currentIndex, onActiveChange, onSelect, label, className, }: ChapterScrubberProps): any;
export default ChapterScrubber;
```

## Example

```html
<div id="root" class="p-6"></div>
<script>
(function () {
  var h = React.createElement, DC = window.DigitalCrew, I = DC.Icons;
  // Chapters of a recorded discovery call.
  var CHAPTERS = [
    ["00:00", "Introductions", "Sarah opens; Claire and Tom join from Lyon."],
    ["01:40", "Why now", "SDR team doubled, reply rates fell; board wants 3× coverage."],
    ["04:05", "Current stack", "HubSpot sequences, 40 live; LinkedIn done by hand."],
    ["06:30", "How Max works", "Research, first touches, warm hand-off with context."],
    ["09:10", "Digital Workers", "Two workers on one segment to start."],
    ["11:45", "HubSpot sync", "Enrolments are read so nobody gets a double touch."],
    ["14:20", "Deliverability", "Warm-up, sending windows and bounce protection."],
    ["16:50", "Pricing", "Growth plan, monthly billing for the pilot."],
    ["19:15", "Objections", "January budget freeze; CEO sign-off for annual."],
    ["21:30", "Pilot scope", "DACH logistics, CFO and COO personas."],
    ["23:40", "Success metrics", "Reply rate and meetings booked per 100 leads."],
    ["25:55", "Security review", "SOC 2 report and DPA requested."],
    ["27:10", "Next steps", "Technical session Thursday at 10:00 with RevOps."],
    ["28:30", "Wrap-up", "Sarah sends pricing and benchmarks today."]
  ].map(function (c, i) { return { id: "ch-" + i, meta: c[0], title: c[1], description: c[2] }; });
  function App() {
    var ref = React.useRef(null);
    var s = React.useState(null), active = s[0], setActive = s[1];
    React.useEffect(function () {
      // Hover over chapter 8 once so the card shows the preview.
      var t = setTimeout(function () {
        var list = ref.current && ref.current.querySelector("[role=listbox]");
        if (!list) return;
        var r = list.getBoundingClientRect();
        list.dispatchEvent(new PointerEvent("pointermove", { bubbles: true, clientX: r.left + 10, clientY: r.top + 7.5 * 10, pointerType: "mouse" }));
      }, 200);
      return function () { clearTimeout(t); };
    }, []);
    return h("div", { ref: ref, className: "flex items-start gap-10" },
      h("div", { className: "w-48 shrink-0 space-y-1" },
        h("p", { className: "text-sm font-semibold" }, "Northwind Traders intro call"),
        h("p", { className: "text-xs text-muted-foreground" }, "29 min · 14 chapters · Max is on chapter 5"),
        h("p", { className: "pt-2 text-xs text-muted-foreground" }, active ? "Hovering: " + active : "Hover or tab through the rail")),
      h(DC.ChapterScrubber, {
        chapters: CHAPTERS, currentIndex: 4, label: "Meeting chapters",
        onActiveChange: function (c) { setActive(c ? c.title : null); },
        onSelect: function () {}
      }));
  }
  ReactDOM.createRoot(document.getElementById("root")).render(h(DC.TooltipProvider, { delayDuration: 80 }, h(App)));
})();
</script>
```

## More in Motion

- [ActionFocusSession](/design/components/action-focus-session.md): The focus timebox for one task in the actions cockpit: a live billable clock with approve, start, pause, run and done, and a burst of sparkles on completion.
- [CardResize](/design/components/card-resize.md): A container that tweens its width and height when its content changes (`.t-resize`).
- [ContainerScroll](/design/components/container-scroll.md): A hero block where a device-like frame starts tilted back in 3D and straightens as the page scrolls, with the title drifting up.
- [FeatureSlideshow](/design/components/feature-slideshow.md): A step-by-step feature explainer: an accordion of steps on the left that advances on a timer, a progress line on the open step, and a large media slot on the right.
- [FirstBentoAnimation](/design/components/first-bento-animation.md): A self-playing chat vignette for the bento grid: a user asks for a meeting slot, the Digital Crew avatar shows typing dots, then the reply streams in as a `Reasoning` block.
- [FourthBentoAnimation](/design/components/fourth-bento-animation.md): A scheduling vignette for the bento grid: a week ruler, three task bars that spring into place, and a time cursor that follows the pointer.
- [HyperText](/design/components/hyper-text.md): A monospaced, uppercase text effect: letters scramble through random characters, then lock in from left to right.
- [InfiniteSlider](/design/components/infinite-slider.md): A JavaScript-driven endless strip (Framer Motion) that renders its children twice and slides them in a loop, optionally slowing on hover.
- [JoyfulTaskCheckbox](/design/components/joyful-task-checkbox.md): A task checkbox with a small, restrained completion moment: one soft bloom, two sparks, then stillness.
- [Lottie](/design/components/lottie.md): The lottie-react player, exported as-is, for the Lottie animations that ship with Crew OS (`animations/*.json`).
- [Marquee](/design/components/marquee.md): An endless CSS scroll of repeated content, horizontal or vertical, speed set with two CSS variables.
- [MotionScale](/design/components/motion-scale.md): The shared motion scale made visible: the same distance travelled on each duration token and each easing, with a Replay button.
- [NumberFlowCounter](/design/components/number-flow-counter.md): A small dark pill that shows one number at a time and rolls its digits to the next value every two seconds (NumberFlow).
- [OrbitingCircles](/design/components/orbiting-circles.md): Places its children on a circular orbit around the centre of the parent and spins them, with an optional faint ring for the path.
- [ShimmerText](/design/components/shimmer-text.md): A status line with a light band sweeping across its glyphs — Max's "Thinking…" and live tool lines (`.t-shimmer`).
- [SignatureGradient](/design/components/signature-gradient.md): The signature Digital Crew gradient — indigo → pink → amber (`dc-andre`, `dc-camille`, `dc-amber`) — in its four forms.
- [SkeletonReveal](/design/components/skeleton-reveal.md): A skeleton that cross-blurs into the loaded content, so the swap reads as one motion (`.t-skel`).
- [StreamingText](/design/components/streaming-text.md): Streamed words resolving through a soft cross-blur, one after another (`.t-stream-w` → `.is-in`).
- [ThirdBentoAnimation](/design/components/third-bento-animation.md): An insight vignette for the bento grid: a smooth area line draws itself, a dot pulses at its midpoint and a dark pill above it ticks through values.
- [UniboxMotion](/design/components/unibox-motion.md): The Unibox's shared motion vocabulary: every surface moves on the same few springs, so the whole inbox feels like one physical material.
