# Depth stack

A deck of cards seen almost edge-on — the front one square to you, each
behind it smaller, turned further and pushed further out. One flick moves
the deck one card, however hard you throw it.

## The depth table

Everything is four rows of numbers. Each card takes the row matching its
distance from the front, mirrored for the cards on the left:

| depth | scale | rotate | x (of card width) | y | opacity |
|---|---|---|---|---|---|
| 0 | 1.00 | 0° | 0 | 0 | 1 |
| 1 | 0.90 | 5° | 0.250 | 21.1px | 1 |
| 2 | 0.75 | 10° | 0.450 | 29.6px | 1 |
| 3 | 0.60 | 15° | 0.550 | 21.1px | 0 |

The fourth row sits at zero opacity so the stack has somewhere to come from
and go to.

**Store x as a fraction of the card's width**, not in pixels, so the spread
survives being scaled to a different card size.

The vertical offsets are the odd ones — they rise and then fall again,
which is `maxY · sin(depth × 45°)`. Keep the measured values rather than the
formula: four hand-checked numbers beat a curve fitted to four points.

```css
.card {
  transform: translate(var(--x), var(--y)) rotate(var(--rot)) scale(var(--scale));
  opacity: var(--op);
  transition: transform .72s cubic-bezier(.22,.61,.36,1), opacity .5s ease;
}
```

## A flick, not a scrub

Move one card per gesture regardless of distance. A deck has no continuum to
scrub along — only an order — so take the step once the gesture passes a
threshold and ignore the rest of the drag until the pointer lifts.

```js
const onMove = (e) => {
  if (!grab.current || moved.current) return
  const dx = e.clientX - grab.current.x
  if (Math.abs(dx) < 40) return
  moved.current = true
  setFront((i) => wrap(i + (dx < 0 ? 1 : -1)))
}
```

## Traps

**A link is natively draggable.** If the card is an `<a>`, pressing and
moving starts the browser's own drag of the href — `pointermove` stops
firing and the deck never moves. It works perfectly under synthetic pointer
events and cannot work under a real hand. Set `draggable={false}` on the
anchor and prevent `dragstart` on the deck.

**Fade from the live position, not the index.** The index does not change
until the gesture ends, so a hard cut on it leaves a card dragged most of
the way into view still hidden, then popping in all at once. Compute
opacity from `offset + dragFraction`.

**Use a ref, not state, for the did-it-move flag.** `click` fires after
`pointerup`, so a state flag is one render behind and a drag that ends on a
card will follow its link.

## Prompt

> Build a horizontal depth stack. Each card is positioned purely by
> transform from a depth table: scale 1/0.9/0.75/0.6, rotation 0/5/10/15
> degrees, x as a fraction of card width 0/0.25/0.45/0.55, and the fourth
> depth at zero opacity. Mirror the sign for cards on the left and give
> nearer cards a higher z-index. Advance one card per flick once a drag
> passes 40px, ignoring the remainder of the gesture. The card is a link, so
> disable native dragging on it and guard the click with a ref rather than
> state.
