Skip to content
peetzweg/ui

TimerSurface

PresetAn opinionated preset of MorphSurface.

Composed with TickTape; bakes in the sleep-timer domain — minute range, duration formatting, palette, and start semantics.

An iOS-style sleep timer. A compact pill showing the set duration morphs into a dark panel: a minute tape (a tick per minute, labels every five) scrubbed under a fixed pointer, a live h:mm:ss readout, and a start button. Scrub the tape and the readout follows; press start and the surface reports the duration and collapses back into the pill.

The mechanics all live in the primitives — the morph, dismissal, and click-outside come from MorphSurface; the scrubbing, snapping, glow trail, and per-minute haptic tick on mobile from TickTape. This layer is the timer domain: the orange-on-black palette, the formatting, and start/collapse semantics.

Click the pill, drag the tape, start the timer:

 

Installation

pnpm dlx shadcn@latest add peetzweg/ui/timer-surface

Pulls morph-surface and tick-tape (and their dependencies) as registryDependencies, plus motion and lucide-react.

Usage

import { TimerSurface } from "@/components/ui/timer-surface"
 
<TimerSurface
  defaultMinutes={15}
  maxMinutes={120}
  onStart={(minutes) => startSleepTimer(minutes)}
/>

Uncontrolled by default. Both axes can be controlled independently: the duration via minutes + onMinutesChange, the surface via open + onOpenChange (both forwarded to MorphSurface). The start button is disabled at 0:00.

Swap the text with startLabel and the readout format with formatDuration (the default formatTimerDuration renders 15:00 / 1:40:00 and is exported). className lands on the morphing surface — override the palette there. For a different layout entirely, drop down to the primitives.

Props

PropTypeDefaultDescription
minutesnumberControlled duration, in whole minutes.
defaultMinutesnumber15Initial duration when uncontrolled.
onMinutesChange(minutes: number) => voidFires live while scrubbing.
maxMinutesnumber120Last tick on the tape.
onStart(minutes: number) => voidFired with the duration when start is pressed.
startLabelReactNode"Start Timer"Start button content.
formatDuration(minutes: number) => stringformatTimerDurationReadout + trigger text.
openbooleanControlled open state (forwarded to MorphSurface).
defaultOpenbooleanfalseInitial open state when uncontrolled.
onOpenChange(open: boolean) => voidNotified on expand / collapse.
collapseOnStartbooleantrueCollapse the surface after starting.
expandedWidthnumber480Panel width; also the reserved footprint (px).
expandedHeightnumber210Panel height; also the reserved footprint (px).
classNamestringLands on the morphing surface — override palette here.