# Tote — Animated Data Visualization for React

> A ranked bar race — the racetrack tote board. Rows that change rank physically slide past each other FLIP-style, the overtaker lifting over its neighbor with a soft shadow before seating flush; bars re-measure with a spring, values roll to their new numbers, and the leader's value carries a subtle brightness edge. Feed it frames and it plays the classic bar-chart race on an interval (suspended offscreen), politely announcing lead changes. Includes a segmented category-proportion variant.

- Page: https://crucibleui.com/components/data/tote
- Category: [data](https://crucibleui.com/components/data)
- Tier: pro (one-time purchase, see https://crucibleui.com/pricing)
- Engine: motion
- npm dependencies: motion

## Install

```bash
npx shadcn@latest add @crucible/tote
```

Pro install requires adding the token-bearing `@crucible` registry to components.json (issued at https://crucibleui.com/account after purchase).

Import from `@/components/crucible/tote`.

## Props (`ToteProps`)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items?` | `ToteItem[]` | `a built-in six-row sample (paired with a sample race when `frames` is also omitted)` | Rows of the board. Input order also assigns ramp colors and breaks ties. |
| `frames?` | `ToteFrame[]` | `a seeded ten-frame sample race when `items` is also omitted` | Race mode: a time series of value snapshots the board steps through automatically. When provided, frame values override `items[].value`. |
| `interval?` | `number` | `2` | Seconds each race frame holds before the next lands. |
| `loop?` | `boolean` | `true` | Restart the race from frame 0 after the last frame. |
| `sort?` | `"desc" \| "asc"` | `"desc"` | Rank order: highest value first, or lowest first. |
| `reorder?` | `boolean` | `true` | Animate rank changes FLIP-style so rows slide past each other. When false the board still re-sorts and bars still spring, but rows swap instantly. |
| `maxRows?` | `number` | — | Show at most this many rows; movers animate in and out at the cutoff. |
| `spring?` | `ToteSpring` | — | Spring used for row FLIP slides and bar width re-measures. |
| `format?` | `(value: number) => string` | — | Formats a value for display (also used mid-roll on interpolated values). |
| `rollDuration?` | `number` | `0.6` | Seconds a value takes to roll to its new number. `0` snaps. |
| `colors?` | `string[]` | `a cool analogous family` | Color ramp assigned to items by input order (cycled) when an item carries no `color` of its own. |
| `rowHeight?` | `number` | `36` | Row height in px (also sizes the category bar at a third). |
| `gap?` | `number` | `8` | Gap between rows in px. |
| `labelWidth?` | `number` | `96` | Width of the label column in px. |
| `variant?` | `"bars" \| "category"` | `"bars"` | Render one segmented proportion bar (segments re-negotiate widths on change) instead of the ranked bar list. Segments keep input order. |
| `showFrameLabel?` | `boolean` | `true` | Show the current frame's caption above the board while racing. |
| `paused?` | `boolean` | `false` | Suspends race playback. |

Also accepts: `Omit<React.ComponentPropsWithoutRef<"div">, "children">`.

## Usage example

```tsx
import { Tote, type ToteFrame, type ToteItem } from "@/components/crucible/tote";

/** Deterministic PRNG so the demo race is identical on server and client. */
function mulberry32(seed: number) {
  return function () {
    seed |= 0;
    seed = (seed + 0x6d2b79f5) | 0;
    let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

const ITEMS: ToteItem[] = [
  { id: "alpha", label: "Alpha", value: 0 },
  { id: "bravo", label: "Bravo", value: 0 },
  { id: "cascade", label: "Cascade", value: 0 },
  { id: "delphi", label: "Delphi", value: 0 },
  { id: "ember", label: "Ember", value: 0 },
  { id: "fathom", label: "Fathom", value: 0 },
];

/** Seeded random-walk time series — plenty of overtakes across ten laps. */
const FRAMES: ToteFrame[] = (() => {
  const rand = mulberry32(7);
  const values: Record<string, number> = {
    alpha: 4200,
    bravo: 3900,
    cascade: 3400,
    delphi: 3000,
    ember: 2600,
    fathom: 2300,
  };
  const frames: ToteFrame[] = [];
  for (let lap = 1; lap <= 10; lap++) {
    for (const id of Object.keys(values)) {
      values[id] = Math.round(values[id] + rand() * 900);
    }
    frames.push({ label: `Lap ${lap}`, values: { ...values } });
  }
  return frames;
})();

const CATEGORY_ITEMS = ITEMS.slice(0, 4);

export default function ToteDemo() {
  return (
    <div className="relative isolate flex h-full min-h-[360px] w-full items-center justify-center overflow-hidden rounded-xl bg-neutral-950">
      <div className="relative z-10 flex w-full max-w-sm flex-col gap-8 px-6 py-10">
        <div>
          <Tote items={ITEMS} frames={FRAMES} interval={1.6} maxRows={5} rowHeight={32} />
          <p className="mt-3 text-[11px] font-medium uppercase tracking-[0.2em] text-neutral-500">
            Watch rows overtake as each lap lands
          </p>
        </div>
        <div>
          <Tote
            variant="category"
            items={CATEGORY_ITEMS}
            frames={FRAMES}
            interval={1.6}
            showFrameLabel={false}
          />
          <p className="mt-3 text-[10px] font-medium uppercase tracking-[0.2em] text-neutral-600">
            Category variant — segments re-negotiate
          </p>
        </div>
      </div>
    </div>
  );
}
```

## Accessibility & performance

Honors `prefers-reduced-motion` with a designed static frame or fade (never a blank box) and passes `className` through.
