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.

KQ-1269 · REWIND/UNDO

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.