# Wheel Picker

> An animated wheel picker for React and shadcn, an iOS-style drum with keyboard and type-ahead support.

An iOS-style drum picker: rows sit on a real cylinder and a flick coasts with scroll friction onto a row. Every row crossing the centre ticks the band (onTick, optional vibration). A spinbutton with arrows, Page keys, Home/End and type-ahead; group several behind one band.

- Tier: Eased Pro
- Interaction: Drag
- Page: https://eased.dev/components/wheel-picker
- Installs: motion

## Install

Wheel Picker ships with Eased Pro (https://eased.dev/#pricing). Members add the `@eased-pro` registry with their token (https://eased.dev/account), then run:

```bash
npx shadcn add @eased-pro/wheel-picker
```

An iOS-style drum: drag or flick it and it coasts onto a row, ticking the selection band as rows pass. Built on the gesture kit and inertia core.

## When to use

- Times, durations, dates, quantities and short lists picked by thumb on touch screens.
- Several linked values (hour, minute, AM/PM) behind one band with `WheelPickerGroup`.

## When not to use

- Long lists that need search: use a Combobox.
- Desktop forms where typing is faster: use a Select or a Number field.

## Usage

```tsx example.tsx
import { WheelPicker, WheelPickerGroup } from "@/components/eased/wheel-picker/wheel-picker";

export function Example() {
  const [hour, setHour] = useState(7);
  const [minute, setMinute] = useState(30);
  return (
    <WheelPickerGroup aria-label="Time" className="w-56">
      <WheelPicker aria-label="Hour" options={[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]} value={hour} onValueChange={setHour} align="end" className="flex-1" />
      <WheelPicker
        aria-label="Minute"
        options={Array.from({ length: 12 }, (_, i) => ({ value: i * 5, label: String(i * 5).padStart(2, "0") }))}
        value={minute}
        onValueChange={setMinute}
        className="flex-1"
      />
    </WheelPickerGroup>
  );
}
```

## API reference

### WheelPicker

| Prop | Type | Default | Description |
|---|---|---|---|
| `options` | `(WheelOption \| string \| number)[]` | | `{ value, label?, textValue?, disabled? }`. Disabled rows are skipped. |
| `value` / `defaultValue` | `V` | first enabled | Controlled or uncontrolled. |
| `onValueChange` | `(value: V) => void` | | When the wheel rests on a new row, or a key selects one. |
| `onTick` | `(value: V) => void` | | Each row crossing the centre: wire a sound here. |
| `haptics` | `boolean` | `false` | `navigator.vibrate` per tick where supported. |
| `itemHeight` | `number` | `36` | Row height, px. |
| `visibleCount` | `number` | `5` | Rows in view (odd). |
| `align` | `"start" \| "center" \| "end"` | `"center"` | |
| `aria-label` | `string` | | Required. |
| `disabled` | `boolean` | `false` | |

### WheelPickerGroup

`itemHeight`, `visibleCount`, `aria-label`, `className`. Draws one band behind its wheels and sets their geometry.

## Keyboard

| Key | Action |
|---|---|
| `Tab` | Moves between wheels. |
| `↑` / `↓` | Previous or next row. |
| `Page Up` / `Page Down` | Five rows. |
| `Home` / `End` | First or last row. |
| Letters | Type-ahead to the next matching row. |

## Accessibility

- Each wheel is a `spinbutton` with `aria-valuetext` set to the row's text; the group is a named `group`.
- The rows are decorative (`aria-hidden`); the value text is what is read.
- Mouse wheel and trackpad scroll the drum and settle on a row.

## Motion

Rows sit on a cylinder (`translateZ` and `rotateX`, one MotionValue, no re-renders), fading with the cosine of their angle. A flick coasts with UIScrollView friction (decay at 0.998) and eases onto a row; a slow release (under 100px/s) or a pause in wheel scrolling settles on the nearest row with the snappy spring; a drag past the ends rubber-bands. Each row crossing the centre bumps the band (scaleY 0.9 → 1 over 180ms). Keys and taps glide on a 300ms spring. Reduced motion: a flat list with no perspective and no band bump, and every move jumps.

## Notes for AI

Choose it for touch-first pickers of short ordered lists. Compose time or date pickers from several wheels in a `WheelPickerGroup`; give each wheel a clear `aria-label`.

Do not wrap it in a scroll container or add scroll snapping; it handles drag, wheel, keys and snapping itself. It is not a listbox: do not add `role="option"` children.

## Related

- [Segmented](/components/segmented): two to five choices, all visible.
- [Scrub Label](/components/scrub-label): drag to change a number without a list.
