# How the Global Step Counter Works in the Presentation Skill

> Learn how the global step counter flattens presentation structure for linear navigation and progress tracking. Understand its role in the ConardLi garden skills.

- Repository: [ConardLi/garden-skills](https://github.com/ConardLi/garden-skills)
- Tags: internals
- Published: 2026-08-29

---

**The global step counter flattens the hierarchical chapter-and-step structure of a presentation into a single linear index, enabling seamless navigation and progress tracking across the entire scripted talk.**

In the `ConardLi/garden-skills` repository, the **web-video-presentation** skill manages scripted presentations as a collection of chapters, each containing ordered narration steps. To allow the UI to treat the entire presentation as a flat sequence—supporting features like progress bars and global seeking—the `useStepper` hook implements a **global step counter** that maps two-dimensional coordinates (chapter, step) to a single integer. This derivation enables consistent state persistence and navigation regardless of chapter boundaries.

## Understanding the Hierarchical Step Structure

A presentation in this system consists of an array of `ChapterDef` objects (defined in [`skills/web-video-presentation/templates/src/registry/chapters.ts`](https://github.com/ConardLi/garden-skills/blob/main/skills/web-video-presentation/templates/src/registry/chapters.ts)), where each chapter contains a `narrations` array. The internal state tracks the current position using a `cursor` object with `chapter` and `step` indices. However, for UI components like progress indicators or seekable timelines, exposing this as a single number simplifies rendering and interaction logic significantly.

## Computing the Global Step Counter

The `useStepper` hook (located at [`skills/web-video-presentation/templates/src/hooks/useStepper.ts`](https://github.com/ConardLi/garden-skills/blob/main/skills/web-video-presentation/templates/src/hooks/useStepper.ts)) derives the global counter through three coordinated calculations that execute during initialization and whenever the chapter configuration changes.

### Calculating Chapter Offsets

First, the hook builds an **offsets array** that records the cumulative number of steps preceding each chapter. This allows O(1) lookup of a chapter's starting position in the global sequence.

```typescript
const offsets = useMemo(() => {
  const arr: number[] = [];
  let acc = 0;
  for (const c of chapters) {
    arr.push(acc);
    acc += c.narrations.length;
  }
  return arr;
}, [chapters]);

```

As implemented in lines 74-81, `offsets[i]` represents the global index of the first step in chapter `i`. For example, if chapter 0 has 5 narrations and chapter 1 has 3, the offsets array becomes `[0, 5, 8]`.

### Determining the Total Step Count

The total number of steps across the entire presentation is computed via a simple reduction (lines 84-86):

```typescript
const totalGlobal = useMemo(
  () => chapters.reduce((s, c) => s + c.narrations.length, 0),
  [chapters],
);

```

This `totalGlobal` value serves as the upper bound for the global index and drives percentage-based calculations in progress indicators.

### Deriving the Current Global Index

With the offsets established, the hook calculates the current global index by adding the chapter's offset to the intra-chapter step index (line 87):

```typescript
const globalIndex = (offsets[cursor.chapter] ?? 0) + cursor.step;

```

This value is exposed as part of the `StepperState` return object, allowing consumers to display "Step X of Y" labels or calculate completion percentages without understanding the underlying chapter structure.

## Navigating to Arbitrary Global Steps

The counter enables bidirectional navigation through the `jumpToGlobal` function (lines 123-136). This method accepts a global index, clamps it to valid bounds, then traverses the chapters to locate the corresponding (chapter, step) pair:

```typescript
const jumpToGlobal = useCallback(
  (g: number) => {
    const target = clamp(g, 0, totalGlobal - 1);
    let acc = 0;
    for (let i = 0; i < chapters.length; i++) {
      const t = chapters[i]!.narrations.length;
      if (target < acc + t) {
        setCursor({ chapter: i, step: target - acc });
        return;
      }
      acc += t;
    }
  },
  [chapters, totalGlobal],
);

```

This linear search efficiently maps the flat index back to the hierarchical cursor format. When the cursor updates, the hook persists the state to `localStorage` and recomputes `globalIndex`, ensuring the UI remains synchronized across browser refreshes.

## Practical Usage Examples

Components consuming the `useStepper` hook can leverage `globalIndex` and `totalGlobal` to build intuitive navigation controls.

### Implementing a Seekable Progress Bar

The following component renders a clickable progress bar that jumps to the corresponding global step when clicked:

```tsx
import { useStepper } from "./hooks/useStepper";

function ProgressBar({ chapters }) {
  const {
    globalIndex,
    totalGlobal,
    jumpToGlobal,
  } = useStepper(chapters);

  const percent = (globalIndex / Math.max(1, totalGlobal)) * 100;

  return (
    <div style={{ width: "100%", background: "#eee" }}>
      <div
        style={{
          width: `${percent}%`,
          background: "#4caf50",
          height: "8px",
        }}
        onClick={e => {
          const rect = (e.target as HTMLElement).getBoundingClientRect();
          const clickPos = (e.clientX - rect.left) / rect.width;
          jumpToGlobal(Math.floor(clickPos * totalGlobal));
        }}
      />
    </div>
  );
}

```

### Displaying Global Step Labels

For simple progress indication, consume the global values directly:

```tsx
function StepLabel({ chapters }) {
  const { globalIndex, totalGlobal } = useStepper(chapters);
  return <p>Step {globalIndex + 1} of {totalGlobal}</p>;
}

```

Both patterns rely on the flattened coordinate system provided by the `useStepper` hook, abstracting away the complexity of multi-chapter boundary calculations.

## Summary

- The **global step counter** transforms the two-dimensional (chapter, step) structure into a single linear index for simplified UI state management.
- The `offsets` array caches cumulative step counts per chapter, enabling O(1) global index calculation via `offsets[cursor.chapter] + cursor.step`.
- The `totalGlobal` value aggregates all narration steps across chapters, supporting percentage-based progress calculations.
- The `jumpToGlobal` function provides reverse mapping from a flat index back to the hierarchical cursor, enabling seekable navigation controls.
- State synchronization with `localStorage` ensures the global position persists across page reloads, maintaining user context within the presentation flow.

## Frequently Asked Questions

### What is the purpose of the global step counter in the presentation skill?

The global step counter serves as a **flattened coordinate system** that allows UI components to treat the entire multi-chapter presentation as a single linear sequence. This abstraction simplifies the implementation of progress bars, step counters, and seek functionality by eliminating the need to handle chapter boundaries manually.

### How does `jumpToGlobal` handle out-of-bounds indices?

The function clamps the requested index between `0` and `totalGlobal - 1` using a `clamp` utility before processing. This ensures that navigation requests—whether from user interactions like clicking a progress bar or programmatic calls—always resolve to valid presentation steps without throwing errors.

### Where is the current step state persisted?

The `useStepper` hook synchronizes the `cursor` state (containing chapter and step indices) to `localStorage` whenever it changes. Because the global index is derived from the cursor rather than stored independently, the system maintains consistency between the hierarchical state and the flattened representation across browser sessions.

### Why use an offsets array instead of calculating positions on demand?

Pre-computing the **offsets array** with `useMemo` optimizes performance for presentations with many chapters. Since the global index calculation `offsets[cursor.chapter] + cursor.step` occurs frequently—whenever the cursor changes or components re-render—the O(1) array lookup prevents repeated iteration through preceding chapters that would occur with on-demand calculation.