Pointer Row

A horizontal row of cells with one or more pointer overlays drawn above them. Every pointer is a record with four fields: a stable id for React identity, the index of the cell it sits on, a short label rendered above that cell, and an optional tone driving the label colour. As the caller drives the index values forward, each pointer tweens to its new cell with a single critically-damped spring. When two pointers share an index, the labels stack vertically inside the same column.

PointerRow is the geometric primitive underneath any algorithm narrative that walks indices over a sequence — two-pointer scans, sliding windows, in-place partitions, binary-search probes. The component owns the row, the pointer lane, and the responsive cell sizing; the caller owns every pointer's identity, position, and semantics.

Customize
Row
10
Display

Installation

npx shadcn@latest add https://craftbits.dev/r/pointer-row.json

Usage

import { PointerRow } from "@craft-bits/core";
 
const values = [1, 3, 5, 7, 9, 11, 13, 15];
 
<PointerRow
  values={values}
  pointers={[
    { id: "L", index: 0, label: "L", tone: "accent" },
    { id: "R", index: 7, label: "R", tone: "accent" },
    { id: "i", index: 3, label: "i", tone: "warning" },
  ]}
/>;

Drive a two-pointer scan — flip the pointer positions on each frame and the labels tween between cells:

const [l, setL] = useState(0);
const [r, setR] = useState(values.length - 1);
 
<PointerRow
  values={values}
  pointers={[
    { id: "L", index: l, label: "L", tone: "accent" },
    { id: "R", index: r, label: "R", tone: "accent" },
  ]}
/>;

Stack pointers on the same cell — when lo and hi collide at the end of a binary search, the labels render in one column:

<PointerRow
  values={[1, 2, 3, 4, 5]}
  pointers={[
    { id: "lo", index: 2, label: "lo", tone: "accent" },
    { id: "hi", index: 2, label: "hi", tone: "accent" },
    { id: "mid", index: 2, label: "mid", tone: "warning" },
  ]}
/>

Hide the numeric index lane, compact the cells, or supply a custom accessible summary:

<PointerRow
  values={values}
  pointers={pointers}
  showIndices={false}
  compact
  ariaLabel="Two-pointer scan over a sorted array."
/>

Understanding the component

  1. Cell row. Cells are rendered left to right from index zero through the last value. Width uses a responsive clamp() formula so the row shrinks on narrow viewports while capping at cellSize on wider ones — the same pattern as ArrayCells and StringCells.
  2. Pointer lane. Above the cells sits a row of one label slot per cell. Each pointer mounts inside the slot owning its index. When the index changes, AnimatePresence slides the old label out and the new one in. Slot ordering inside a column follows insertion order from the pointers array.
  3. Pointer identity. The id field is the stable React key. Keep it constant across renders for a given pointer — that is what lets the same "L" pointer tween from cell two to cell three instead of unmounting and remounting.
  4. Ring colour. Each cell carries an inset ring. Cells with no pointer use the muted border tone; cells with at least one pointer use the tone of the first pointer in that cell — driven by the order of the pointers array, earlier pointers win the ring.
  5. Stacking. When multiple pointers share an index, their labels stack vertically inside the column. There is no automatic deduplication — pass distinct labels or pre-combine upstream if you want a single rendered label.
  6. Empty row. When values is empty the row collapses to the pointer lane plus the empty cells row; out-of-range pointers are dropped silently.
  7. Reduced motion. When prefers-reduced-motion: reduce is set, pointer transitions and cell tints snap to instant.

Props

PropTypeDefaultDescription
valuesreadonly (string | number)[]requiredCell values rendered left to right.
pointersreadonly PointerRowPointer[]Pointer overlays positioned above the cells.
showIndicesbooleantrueRender numeric indices below each cell.
cellSizenumber44Ideal cell size in px.
compactbooleanfalseSmaller cells, tighter row.
ariaLabelstringderivedAccessible summary for the row.
transitionTransitionSPRINGS.smoothPointer / cell transition.
classNamestringMerged onto the root via cn().

PointerRowPointer

FieldTypeDefaultDescription
idstringrequiredStable React key. Must be unique within the row.
indexnumberrequiredCell index the pointer sits above. Out-of-range pointers are skipped.
labelstringrequiredShort label rendered above the cell.
tonePointerRowTone"accent"Tone for the label colour.

Accessibility

  • The outer container is role="img" with an aria-label summarising the row — count of cells plus every in-range pointer's label and index. Pass ariaLabel to override the generated summary.
  • The pointer label lane is decorative (aria-hidden="true"); the canonical state lives in the summary so screen readers read it once per render instead of once per label.
  • Cell ring colour, ring width, and the muted-vs-accent contrast layer multiple cues — the distinction stays legible for colour-blind users.
  • Motion respects prefers-reduced-motion: reduce — pointer transitions and cell tints collapse to instant.

Credits

  • Extracted from: algoflashcards (src/lessons/primitives/viz/PointerRow.tsx). The source baked the binary-search vocabulary in: hard-coded lo / mid / hi props, a visited[] trail, a sortedRange overlay, a found success pulse, a hiddenIndices reveal mask, and a cellStyle(idx, value) escape hatch for per-problem state. The library extract drops all of that — what remains is the geometric primitive: a row of cells with a declarative pointer list and a stable React key per pointer. Range elimination, probe rings, sorted-half tints, and visited trails all live in the caller, composed from the same pointers array or layered as separate components on top.