# Reel — Animated Card for React

> A draggable, throwable rail of media cards with momentum and detent snapping (transform-only) where off-center cards rest in a quiet duotone and the centered one blooms to full contrast. Clicking a card performs a genuine shared-element expand — media and title travel into a centered detail overlay with unbroken radii, then body copy fades up. Focus-trapped dialog with Escape, outside-click, and arrow-key rail nav; reduced motion snaps instantly.

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

## Install

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

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

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

## Props (`ReelProps`)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `ReelItem[]` | — | Cards to arrange along the rail. Each expands into a centered overlay on click. |
| `cardWidth?` | `number` | `240` | Card face width in px. |
| `cardHeight?` | `number` | `320` | Card face height in px. |
| `gap?` | `number` | `16` | Gap between cards in px. |
| `defaultIndex?` | `number` | `0` | Index centered on first paint. |
| `onIndexChange?` | `(index: number) => void` | — | Fires whenever the centered card changes. |
| `onExpand?` | `(item: ReelItem, index: number) => void` | — | Fires when a card is expanded into the overlay. |
| `onAction?` | `(item: ReelItem) => void` | — | Called when a non-link action button is pressed in the expanded card. |
| `duotone?` | `boolean` | `true` | Rest off-center cards in a quiet duotone; the centered card blooms to full color. |
| `expandedMaxWidth?` | `number` | `520` | Expanded overlay max width in px. |
| `closeOnOutsideClick?` | `boolean` | `true` | Close when the backdrop is clicked. |
| `closeOnEscape?` | `boolean` | `true` | Close on the Escape key. |
| `accent?` | `string` | `"#93c5fd"` | Single cool accent hex for focus rings. |
| `label?` | `string` | `"Card carousel"` | Accessible name for the carousel. |
| `className?` | `string` | — | Extra classes for the outer wrapper. |

## Usage example

```tsx
import { Reel, type ReelItem } from "@/components/crucible/reel";

const ITEMS: ReelItem[] = [
  {
    id: "high-country",
    title: "High country at first light",
    meta: "Landscape — 5 min read",
    accent: "#3f6b8c",
    action: "Read",
    content: (
      <p>
        Above the tree line the air thins and the colour drains from the world
        until the sun clears the ridge. For a few minutes everything is blue on
        blue, then a single warm line races down the slope and the whole basin
        wakes at once. Get there in the dark; the reward is that switch being
        thrown while you watch.
      </p>
    ),
  },
  {
    id: "paper-boats",
    title: "Paper boats",
    meta: "Studio — 3 min read",
    accent: "#6a5680",
    action: "Read",
    content: (
      <p>
        Fold, crease, unfold, refold — the same seven moves your grandparents
        knew turn a square of paper into a hull that actually floats. There is a
        quiet physics to it: the more careful the folds, the longer it stays dry.
        A gutter after rain becomes a harbour, and a whole afternoon disappears.
      </p>
    ),
  },
  {
    id: "night-market",
    title: "The night market",
    meta: "Travel — 6 min read",
    accent: "#b4552e",
    action: "Read",
    content: (
      <p>
        It runs on smoke and neon and the sizzle of a hundred griddles. You do
        not read a menu here; you follow your nose down the row, point at what the
        person ahead of you is eating, and trust the queue. By the far end you are
        full, slightly lost, and completely happy.
      </p>
    ),
  },
  {
    id: "cold-water",
    title: "Cold-water swimming",
    meta: "Field notes — 4 min read",
    accent: "#2f6b63",
    action: "Read",
    content: (
      <p>
        The first thirty seconds are a negotiation with your own nervous system —
        gasp, breathe, wait. Then the cold stops being an enemy and becomes a kind
        of clarity, the whole body reporting for duty. Swim a slow lap, get out
        while you still want more, and carry that clean feeling into the day.
      </p>
    ),
  },
  {
    id: "lantern-festival",
    title: "Lantern festival",
    meta: "Culture — 5 min read",
    accent: "#8a6d2f",
    action: "Read",
    content: (
      <p>
        One by one they lift off the water and the crowd goes quiet, every face
        turned up. There is no fireworks bang, just a slow rising tide of light
        and the small heat you can feel on your cheeks when the nearest ones drift
        past. Then they are stars, then they are gone.
      </p>
    ),
  },
  {
    id: "long-drive",
    title: "The long drive home",
    meta: "Essay — 7 min read",
    accent: "#4b5563",
    action: "Read",
    content: (
      <p>
        Some roads are just distance, and some are a decompression chamber. Empty
        highway, one good album, and the mile markers ticking down give the mind
        room to file the day away. You arrive tired in the body but oddly rested
        in the head, which is the whole point of driving instead of flying.
      </p>
    ),
  },
];

export default function ReelDemo() {
  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
        aria-hidden
        className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_center,rgba(255,255,255,0.04),transparent_70%)]"
      />
      <div className="relative z-10 flex w-full flex-col items-center gap-5">
        <Reel items={ITEMS} label="Featured stories" />
        <p className="text-sm text-neutral-500">Drag, then click a card.</p>
      </div>
    </div>
  );
}
```

## Accessibility & performance

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