KQ-1269 · REWIND/UNDO
Rewind Undo
Undo and redo as a tape deck: the Undo button carries a reel holding the applied history, the Redo button a reel holding what has been rewound, and the tape between them reads out each step. A press rewinds one step as both reels turn the same way on a spring as heavy as the spool, each by the tape's length over its own radius so a nearly empty reel visibly spins faster, while the undone action's name is pulled out onto the tape on the snap spring and struck through on flick; holding scrubs back with steps that come faster and faster, the reels spinning up and the names flicking past, and Redo slides in on glide once there is something to redo. Both are native buttons in a named group: Enter or Space steps and, held, scrubs exactly as a held pointer does, Escape cancels a step the host is still applying, and optional shortcuts bind Cmd or Ctrl+Z and Shift+Cmd+Z or Ctrl+Y outside text fields.
Q3 forecast
Fernworks Sheets · 9 of 9 edits
9 of 9 steps · last: Froze header
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/rewind-undo
Props
- history
- { id: string; label: string }[]
- default defaultHistory
- Everything done so far, oldest first: the tape. Each label names its step on the tape and in the buttons' names.
- index / defaultIndex
- number
- default history.length
- How many steps of history are applied, controlled or initial.
- onIndexChange
- (index: number) => void
- default —
- Each new position, from the press, the hold or the shortcut that moved it.
- onUndo
- (step: { id: string; label: string }, signal: AbortSignal) => void | Promise<unknown>
- default —
- Undoes one step. Return a promise and the step waits on it (the reels tug, the name is faint) and commits once it settles; the signal aborts on Escape.
- onRedo
- (step: { id: string; label: string }, signal: AbortSignal) => void | Promise<unknown>
- default —
- Redoes one step, the same way.
- state
- "idle" | "pending" | "success" | "error"
- default —
- Controlled action state. Every move goes through onStateChange and waits for this.
- onStateChange
- (state: "idle" | "pending" | "success" | "error") => void
- default —
- Each action state, from the press, the answer or the timer that caused it.
- steps
- number
- default 6
- Steps one turn of a half-full reel holds, 3 to 12; also the reel's spokes. Fewer means bigger turns per press.
- spool
- number
- default 0.5
- Reel weight, 0 to 1: a light reel snaps to each step, a heavy one carries past and settles back.
- hold
- number
- default 0.5
- How hard a held press speeds up the scrub, 0 to 1: 0 keeps a steady pace, 1 races to the end.
- holdDelay
- number
- default 380
- How long a press is held before it starts to scrub, in ms.
- shortcuts
- boolean
- default false
- Bind Cmd or Ctrl+Z to undo and Shift+Cmd+Z or Ctrl+Y to redo, from anywhere on the page outside inputs, text areas and editable content.
- undoLabel
- string
- default "Undo"
- The Undo button's word; its accessible name adds the step it would undo.
- redoLabel
- string
- default "Redo"
- The Redo button's word; its accessible name adds the step it would redo.
- readoutLabels
- { undid?: string; redid?: string; last?: string; empty?: string; failed?: string }
- default { undid: "Undid", redid: "Redid", last: "Last", empty: "Nothing to undo", failed: "Failed" }
- The words on the tape between the reels.
- accent
- string
- default "var(--accent-bright)"
- The tape on the reels. Any CSS colour.
- successHold
- number
- default 2200
- How long a step's name stays on the tape before it rests on the last step, in ms.
- errorHold
- number
- default 2600
- How long a failure holds before it rests, in ms.
- size
- "sm" | "md" | "lg"
- default "md"
- 32, 40 or 48 px tall; the reels scale with it.
- sound
- boolean
- default false
- Play a click per step pitched by its place in history, a dull click at either end, and the reels' whir while a hold scrubs. No audio context exists unless this is on.
- disabled
- boolean
- default false
- Dims and disables both buttons and the shortcuts.
- className
- string
- default —
- Merged onto the group's classes.
| Prop | Type | Default | Description |
|---|---|---|---|
| history | { id: string; label: string }[] | defaultHistory | Everything done so far, oldest first: the tape. Each label names its step on the tape and in the buttons' names. |
| index / defaultIndex | number | history.length | How many steps of history are applied, controlled or initial. |
| onIndexChange | (index: number) => void | — | Each new position, from the press, the hold or the shortcut that moved it. |
| onUndo | (step: { id: string; label: string }, signal: AbortSignal) => void | Promise<unknown> | — | Undoes one step. Return a promise and the step waits on it (the reels tug, the name is faint) and commits once it settles; the signal aborts on Escape. |
| onRedo | (step: { id: string; label: string }, signal: AbortSignal) => void | Promise<unknown> | — | Redoes one step, the same way. |
| state | "idle" | "pending" | "success" | "error" | — | Controlled action state. Every move goes through onStateChange and waits for this. |
| onStateChange | (state: "idle" | "pending" | "success" | "error") => void | — | Each action state, from the press, the answer or the timer that caused it. |
| steps | number | 6 | Steps one turn of a half-full reel holds, 3 to 12; also the reel's spokes. Fewer means bigger turns per press. |
| spool | number | 0.5 | Reel weight, 0 to 1: a light reel snaps to each step, a heavy one carries past and settles back. |
| hold | number | 0.5 | How hard a held press speeds up the scrub, 0 to 1: 0 keeps a steady pace, 1 races to the end. |
| holdDelay | number | 380 | How long a press is held before it starts to scrub, in ms. |
| shortcuts | boolean | false | Bind Cmd or Ctrl+Z to undo and Shift+Cmd+Z or Ctrl+Y to redo, from anywhere on the page outside inputs, text areas and editable content. |
| undoLabel | string | "Undo" | The Undo button's word; its accessible name adds the step it would undo. |
| redoLabel | string | "Redo" | The Redo button's word; its accessible name adds the step it would redo. |
| readoutLabels | { undid?: string; redid?: string; last?: string; empty?: string; failed?: string } | { undid: "Undid", redid: "Redid", last: "Last", empty: "Nothing to undo", failed: "Failed" } | The words on the tape between the reels. |
| accent | string | "var(--accent-bright)" | The tape on the reels. Any CSS colour. |
| successHold | number | 2200 | How long a step's name stays on the tape before it rests on the last step, in ms. |
| errorHold | number | 2600 | How long a failure holds before it rests, in ms. |
| size | "sm" | "md" | "lg" | "md" | 32, 40 or 48 px tall; the reels scale with it. |
| sound | boolean | false | Play a click per step pitched by its place in history, a dull click at either end, and the reels' whir while a hold scrubs. No audio context exists unless this is on. |
| disabled | boolean | false | Dims and disables both buttons and the shortcuts. |
| className | string | — | Merged onto the group's classes. |