Micro-interactions
Hold
A confirm that has to be earned — the ring fills while you hold, snaps shut when it commits, and empties on a different curve entirely if you let go early.
Install
bun add motion clsx tailwind-mergeThen copy 2 files into your project:
- components/registry/hold/hold.tsx
- lib/utils.ts
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| duration | number | 1200 | How long a full press has to be held before it commits. Under about 600ms people let go by reflex; past 2s they assume it is broken. |
| indicator | "ring" | "bar" | "fill" | "ring" | Where the progress lives — a ring traced around the glyph, a bar under the control, or the control's own face flooding left to right. |
| threshold | number | 0.92 | Fraction of the track that counts as committed. Below 1 the last sliver closes itself, so the press lands a beat before the eye expects it — that early snap is what makes it feel decisive rather than punctual. |
| rewindRate | number | 2.4 | How much faster a cancelled hold empties than it filled. At 1 it rewinds the same tape; above 2 the release reads as a spring letting go, which is the entire point of an asymmetric curve. |
| strokeWidth | number | 3 | Weight of the progress track. Thin reads as instrumentation, thick reads as a toy. |
| holdScale | number | 0.96 | How far the control sinks under the finger. The press feedback is doing more work here than the ring is — take it to 1 and the hold stops feeling physical. |
| commitScale | number | 1.06 | Overshoot the control pops to at the instant of commit, before settling. This is the receipt. |
| successHold | number | 1.4 | How long the confirmed state stays on screen before the control re-arms itself. |
| accentColor | string (hex) | "#a855f7" | Amethyst of the filling track and the confirmed state. |
| trackColor | string (hex) | "#2a2136" | The unfilled remainder. Keep it close to the surface underneath — a high-contrast track turns the ring into a bullseye. |
What It Demonstrates
- Hold to confirm
- A progress effect that fills up while the user holds a button.
- Asymmetric easing
- A curve that accelerates and decelerates at different rates. Feels more alive than a symmetric one.
- Press / Tap feedback
- A subtle scale-down when an element is clicked, so it feels physical.
- Interruptible animation
- An animation that can be smoothly redirected mid-flight instead of finishing first.
- Ease-out
- Starts fast, ends slow. The default for most UI and anything responding to the user.
- Purposeful animation
- Motion should serve a function — orient, give feedback, show relationships — not just decorate.
Related Components
Magnetic Button
A button that leans into your cursor — caught in a magnetic field it slides toward the pointer while an amethyst aura swells with proximity, then snaps home on a spring and answers every click with a dark ripple bursting outward from the point of contact.
Tilt
A machined card that leans toward the pointer in 3D while a specular sheet slides across its face.
Reorder
A sortable list that answers to a finger and to a keyboard equally — grab a row, and the rest step aside to make the space.
Dial
A machined rotary knob: angular drag with detents you can feel on the way past, inertia on a flick, and a spring that settles into the nearest stop. The angle lives in a motion value rather than in React state — a drag writes it sixty times a second and a spring more often than that — so the only thing React holds is the detent index, and only when that integer actually changes. Arrow keys step through the same spring the hand does, so the keyboard is not a second, worse path to the same control.
Pixel Transition
Reveal a second layer through a random pixelated dissolve on hover.
Border Glow
A cursor-proximity gradient border with an outer glow.