Timer
A countdown with the time left on the trailing side and pause and cancel buttons when expanded.

A countdown that ticks on its own. You pass when it ends (or how much is left while paused); the time in the island updates every second without re-rendering your component.
Install
npx shadcn@latest add https://islandcn.dev/r/timer-activity.jsonpnpm dlx shadcn@latest add https://islandcn.dev/r/timer-activity.jsonyarn dlx shadcn@latest add https://islandcn.dev/r/timer-activity.jsonbunx shadcn@latest add https://islandcn.dev/r/timer-activity.jsonThis writes components/islandcn/timer-activity.tsx, plus the content kit and the engine if the project lacks them. Mount the island once first, see Installation.
What it shows
- Compact: a timer glyph and the time left.
- Split: its compact pair, as the reference phone shows it, in the pill or in the 62.18 wide capsule. When the pair is too wide for the split, the fallback is a ring that empties as time runs out, with the time left beside it.
- Expanded: pause and cancel buttons when you pass handlers, the label, and the time left in large type.
- Updates: the time turns over its digits each second with the engine’s
IslandNumber. In the expanded view the time sits in a frame as tall as the buttons, so the two stay level on the top-aligned row.
Usage
"use client";
import { useState } from "react";
import { TimerActivity, type TimerState } from "@/components/islandcn/timer-activity";
export const TeaTimer = () => {
const [state, setState] = useState<TimerState | null>(() => ({ kind: "running", endsAt: Date.now() + 4 * 60_000 }));
if (!state) return null;
return (
<TimerActivity
label="Tea"
state={state}
duration={4 * 60_000}
onToggle={() =>
setState((s) =>
s?.kind === "running" ? { kind: "paused", remaining: s.endsAt - Date.now() } : s && { kind: "running", endsAt: Date.now() + s.remaining },
)
}
onCancel={() => setState(null)}
onEnd={() => setState(null)}
/>
);
};
Props
state{ kind: "running"; endsAt: number } | { kind: "paused"; remaining: number }
Running with an end time (epoch ms), or paused with the time left (ms).
{ kind: "running"; endsAt: number } | { kind: "paused"; remaining: number }durationnumber
Full length in ms, for the ring in the minimal fallback.
numberlabel?string
Name in the expanded view.
string"Timer"accent?string
Accent colour.
stringorangeonToggle?() => void
Pause or resume button in the expanded view.
() => voidonCancel?() => void
Cancel button in the expanded view.
() => voidonEnd?() => void
Called when a running timer reaches zero.
() => voidid?string
Activity id.
string"timer"priority?number
Higher priority takes the main pill when two activities run.
number0Make it yours
Install this preset when it is exactly what you need. Otherwise open it in the Playground, change what each region shows and copy the code, or read its source for technique and build your own island on the engine.