# Countdown

> A drift-free countdown hook, a timer ring with phase colours, the mm:ss readout, cycle dots, and an idle-time prompt for time that passed while you were away.

Source: https://docs.nasaqui.com/components/countdown

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/countdown.json
```

The reusable parts under a pomodoro or any timed phase. `useCountdownTimer` counts down without drift: it stores the
moment the countdown ends and works out the time left from the clock, so a throttled background tab or a sleeping laptop
is still right when it wakes. `TimerRing` draws the ring, `TimerReadout` the mm:ss figure, `CycleDots` the sessions in a
set. `useIdleTime` and `IdleTimePrompt` ask what to do with time that passed while the person was away.

## When to use

- A visible countdown: a focus session, a break, a code that expires, a warm-up.
- A ring or dots around any phase-based progress.
- Tracking work time where you want to ask about idle time on return.

## When not to use

- A whole pomodoro cycle (focus, break, long break): use [`usePomodoro`](https://docs.nasaqui.com/components/pomodoro), built on these parts.
- A running stopwatch that counts up: see [`TimeTracker`](https://docs.nasaqui.com/components/time-tracker).
- Locking the app after inactivity: use [`IdleLock`](https://docs.nasaqui.com/components/idle-lock).
- A straight bar: use [`Progress`](https://docs.nasaqui.com/components/progress).

## Import

```tsx
import { CycleDots, IdleTimePrompt, TimerReadout, TimerRing, useCountdownTimer, useIdleTime } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { Button, TimerReadout, TimerRing, useCountdownTimer } from "@fadymondy/nasaq/web";

export function Warmup() {
  const timer = useCountdownTimer({ durationMs: 90_000, onComplete: () => console.log("go") });
  return (
    <div className="flex flex-col items-center gap-3">
      <TimerRing fraction={1 - timer.elapsed} paused={timer.status === "paused"}>
        <TimerReadout seconds={timer.seconds} label="Warm-up" />
      </TimerRing>
      <Button onClick={() => (timer.status === "running" ? timer.pause() : timer.status === "paused" ? timer.resume() : timer.start())}>
        {timer.status === "running" ? "Pause" : "Start"}
      </Button>
    </div>
  );
}
```

## Anatomy

```
TimerRing                       data-slot="timer-ring", data-tone, data-paused
├─ svg track and arc            data-slot="timer-ring-arc"
└─ children                     TimerReadout, a phase icon and word
TimerReadout                    data-slot="timer-readout", role="timer"
CycleDots                       data-slot="cycle-dots", role="img"
IdleTimePrompt                  data-slot="idle-time-prompt" (an AlertDialog)
```

## API

### `useCountdownTimer(options): CountdownTimer`

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `durationMs` | `number` | required | Length in milliseconds. |
| `autoStart?` | `boolean` | `false` | Start when it mounts. |
| `speed?` | `number` | `1` | Runs this many times faster than real time. For demos and tests only. |
| `onComplete?` | `() => void` | none | Called once when the time reaches zero. |

Returns `status` (`"idle" \| "running" \| "paused" \| "done"`), `remainingMs`, `seconds` (whole seconds, rounded up, so
`0` means finished), `elapsed` (0 to 1), `durationMs`, `start(durationMs?)`, `pause()`, `resume()` and `reset(durationMs?)`.

### `TimerRing`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `fraction` | `number` | required | How much of the ring is filled, 0 to 1. Pass `1 - elapsed` to make it drain. |
| `tone?` | `"primary" \| "success" \| "info" \| "warning" \| "neutral"` | `"primary"` | Phase colour. Also give the phase a word or icon. |
| `size?` | `number` | `224` | Diameter in pixels. |
| `thickness?` | `number` | `12` | Stroke width in pixels. |
| `paused?` | `boolean` | `false` | Dashes the arc so the paused state does not rely on colour. |
| `children?` | `ReactNode` | none | Centre content. |

`timerToneText` maps each tone to a matching text colour class for the word inside the ring.

### `TimerReadout`

`seconds` (required), `label?` (accessible name, default "Timer"), `size?` (`"md"` or `"lg"`). It is always `dir="ltr"` with
tabular digits, formats `mm:ss` or `h:mm:ss`, and has `role="timer"` with `aria-live="off"`.

### `CycleDots`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `total` | `number` | required | Sessions in a set. |
| `done` | `number` | required | Finished sessions. |
| `active?` | `boolean` | `false` | Rings the next dot while a session runs. |
| `labels?` | `CountdownLabels` | built-in | String overrides. |

### `useIdleTime(options)` and `IdleTimePrompt`

`useIdleTime({ thresholdMs = 300000, disabled })` returns `{ idle, dismiss }`. `idle` is `{ idleMs, since }` once the person
gives any input after `thresholdMs` without one, otherwise `null`. It compares wall-clock time, so a sleeping machine is caught.

| `IdleTimePrompt` prop | Type | Default | Description |
| --- | --- | --- | --- |
| `idle` | `IdleTime \| null` | required | Open while not null. |
| `onKeep` | `() => void` | required | Keep the idle time. |
| `onDiscard` | `(idle: IdleTime) => void` | required | Remove it and keep the timer running. |
| `onDiscardAndStop?` | `(idle: IdleTime) => void` | none | Remove it and stop. Omit to hide the button. |
| `labels?` | `CountdownLabels` | built-in | String overrides. |

Pure helpers for your own logic and tests are exported too: `startCountdown`, `idleCountdown`, `pauseCountdown`,
`resumeCountdown`, `tickCountdown`, `remainingAt`, `displaySeconds`, `elapsedFraction`, `nextTickDelay`, `formatTimer`,
`scaledClock`, `idleMinutes`. Each takes `now` as an argument.

## Examples

### A ten minute demo in ten seconds

```tsx
const timer = useCountdownTimer({ durationMs: 10 * 60_000, speed: 60, autoStart: true });
```

### Ask about idle time while a timer runs

```tsx
const { idle, dismiss } = useIdleTime({ thresholdMs: 10 * 60_000, disabled: !running });
<IdleTimePrompt
  idle={idle}
  onKeep={dismiss}
  onDiscard={(i) => { trimTimer(i.idleMs); dismiss(); }}
  onDiscardAndStop={(i) => { trimTimer(i.idleMs); stopTimer(); dismiss(); }}
/>
```

## Accessibility

| Key | Action |
| --- | --- |
| Tab | Move between the prompt buttons. |
| Enter or Space | Choose an answer. |

- The readout is a `<time role="timer">` with a name and no live announcements, so it does not chatter every second.
  Announce phase changes yourself (`PomodoroCard` does).
- The ring is decorative (`aria-hidden`). A paused ring is dashed and dots differ by fill, so state is not colour only.
- `IdleTimePrompt` is an AlertDialog: focus is trapped and only its buttons close it. Localise the labels for other languages.
- `prefers-reduced-motion` removes the ring and dot transitions.

## RTL & i18n

- The ring is a circle that runs clockwise from the top in both directions; the readout stays left-to-right.
- Built-in English and Arabic strings follow the Nasaq locale. Numbers use Western digits.
- The time in the idle message is isolated left-to-right inside Arabic text.

## Styling & tokens

Tokens only: `stroke-primary`, `stroke-nq-success`, `stroke-nq-info`, `stroke-nq-warning`, `stroke-nq-line`, `bg-primary`,
`border-nq-line-strong`. Target `data-tone`, `data-paused` on the ring and `data-state` (`done`, `current`, `todo`) on dots.
Pass `className` for layout.

## Do / Don't

- Do compute time from a stored end, as the hook does. Do not decrement a counter in an interval.
- Do give every ring phase a word or icon.
- Do not use `speed` in production, and do not persist state made with it.

## Related

- [`Pomodoro`](https://docs.nasaqui.com/components/pomodoro)
- [`FocusStatus`](https://docs.nasaqui.com/components/focus-status)
- [`TimeTracker`](https://docs.nasaqui.com/components/time-tracker)
- [`IdleLock`](https://docs.nasaqui.com/components/idle-lock)

## Lab

`https://docs.nasaqui.com/?path=/docs/components-utilities-countdown--docs`

## Code

### React

```tsx
import { Button, TimerReadout, TimerRing, useCountdownTimer } from "@fadymondy/nasaq/web";

export function Warmup() {
  const timer = useCountdownTimer({ durationMs: 90_000, onComplete: () => console.log("go") });
  return (
    <div className="flex flex-col items-center gap-3">
      <TimerRing fraction={1 - timer.elapsed} paused={timer.status === "paused"}>
        <TimerReadout seconds={timer.seconds} label="Warm-up" />
      </TimerRing>
      <Button onClick={() => (timer.status === "running" ? timer.pause() : timer.status === "paused" ? timer.resume() : timer.start())}>
        {timer.status === "running" ? "Pause" : "Start"}
      </Button>
    </div>
  );
}
```

### shadcn

```tsx
import { Button } from "@/components/ui/button";
import { TimerReadout, TimerRing, useCountdownTimer } from "@/components/ui/countdown";

export function Warmup() {
  const timer = useCountdownTimer({ durationMs: 90_000, onComplete: () => console.log("go") });
  return (
    <div className="flex flex-col items-center gap-3">
      <TimerRing fraction={1 - timer.elapsed} paused={timer.status === "paused"}>
        <TimerReadout seconds={timer.seconds} label="Warm-up" />
      </TimerRing>
      <Button onClick={() => (timer.status === "running" ? timer.pause() : timer.status === "paused" ? timer.resume() : timer.start())}>
        {timer.status === "running" ? "Pause" : "Start"}
      </Button>
    </div>
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NqButton, NqCycleDots, NqTimerReadout, NqTimerRing, useCountdownTimer } from "@fadymondy/nasaq/vue";

const timer = useCountdownTimer({ durationMs: 25 * 60_000 });
</script>

<template>
  <div class="flex flex-col items-center gap-4">
    <NqTimerRing :fraction="1 - timer.elapsed.value" :paused="timer.status.value === 'paused'">
      <NqTimerReadout :seconds="timer.seconds.value" label="Focus" />
    </NqTimerRing>
    <NqCycleDots :total="4" :done="1" :active="timer.status.value === 'running'" />
    <div class="flex gap-2">
      <NqButton v-if="timer.status.value === 'idle' || timer.status.value === 'done'" @click="timer.start()">Start</NqButton>
      <NqButton v-else-if="timer.status.value === 'running'" variant="secondary" @click="timer.pause()">Pause</NqButton>
      <NqButton v-else @click="timer.resume()">Resume</NqButton>
      <NqButton variant="ghost" @click="timer.reset()">Reset</NqButton>
    </div>
  </div>
</template>
```

### Blade

```blade
<x-nq::countdown :duration-ms="1500000" :total="4" :done="1" class="flex flex-col items-center gap-4">
    <x-nq::countdown.timer-ring live>
        <x-nq::countdown.timer-readout :seconds="1500" live label="Focus" />
    </x-nq::countdown.timer-ring>
    <x-nq::countdown.cycle-dots :total="4" :done="1" live />
    <div class="flex gap-2">
        <x-nq::button variant="primary" x-show="status === 'idle' || status === 'done'" x-on:click="start()">{{ \Nasaq\Nasaq::t('Start', 'ابدأ') }}</x-nq::button>
        <x-nq::button variant="secondary" x-show="status === 'running'" x-cloak style="display: none" x-on:click="pause()">{{ \Nasaq\Nasaq::t('Pause', 'إيقاف مؤقت') }}</x-nq::button>
        <x-nq::button variant="primary" x-show="status === 'paused'" x-cloak style="display: none" x-on:click="resume()">{{ \Nasaq\Nasaq::t('Resume', 'متابعة') }}</x-nq::button>
        <x-nq::button variant="ghost" x-on:click="reset()">{{ \Nasaq\Nasaq::t('Reset', 'إعادة') }}</x-nq::button>
    </div>
</x-nq::countdown>
<x-nq::countdown.idle-time-prompt discard-and-stop />
```

### HTML + Alpine

```html
<div data-slot="countdown" x-data="nqCountdown(1500000, JSON.parse('{\u0022total\u0022:4,\u0022done\u0022:1}'))" class="flex flex-col items-center gap-4"><div data-slot="timer-ring" data-tone="primary"  x-bind="ringRoot(224)"     class="relative inline-flex shrink-0 items-center justify-center">
    <svg aria-hidden="true" viewBox="0 0 224 224" class="absolute inset-0 size-full -rotate-90">
        <circle cx="112" cy="112" r="106" fill="none" stroke-width="12" class="stroke-nq-line" />
        <circle data-slot="timer-ring-arc" cx="112" cy="112" r="106" fill="none" stroke-width="12" stroke-linecap="round"
             x-bind="arc(224, 12)"             class="stroke-primary transition-[stroke-dashoffset,stroke] duration-500 ease-linear motion-reduce:transition-none" />
    </svg>
    <div class="relative flex flex-col items-center justify-center gap-1 text-center"><time data-slot="timer-readout" role="timer" aria-label="Focus" aria-live="off" dir="ltr"
     :datetime="iso()" x-text="clock()"     class="font-medium leading-none tabular-nums text-foreground">25:00</time></div>
</div>
    <div data-slot="cycle-dots" role="img"  :aria-label="cyclesLabel()"  class="inline-flex items-center gap-2">
                                <span :data-state="dotState(0)" :class="dotClass(0)"></span>
                                        <span :data-state="dotState(1)" :class="dotClass(1)"></span>
                                        <span :data-state="dotState(2)" :class="dotClass(2)"></span>
                                        <span :data-state="dotState(3)" :class="dotClass(3)"></span>
            </div>
    <div class="flex gap-2">
        <button data-slot="button"
     type="button"                         x-show="status === &#039;idle&#039; || status === &#039;done&#039;" x-on:click="start()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--nq-action)_88%,var(--nq-fg))] h-control px-[var(--nq-control-pad)]">
        Start</button>
        <button data-slot="button"
     type="button"                         x-show="status === &#039;running&#039;" x-cloak="x-cloak" style="display: none" x-on:click="pause()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control px-[var(--nq-control-pad)]">
        Pause</button>
        <button data-slot="button"
     type="button"                         x-show="status === &#039;paused&#039;" x-cloak="x-cloak" style="display: none" x-on:click="resume()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--nq-action)_88%,var(--nq-fg))] h-control px-[var(--nq-control-pad)]">
        Resume</button>
        <button data-slot="button"
     type="button"                         x-on:click="reset()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover h-control px-[var(--nq-control-pad)]">
        Reset</button>
    </div></div>
<div data-slot="idle-time-prompt-root" x-data="nqIdleTime(300000, JSON.parse('{\u0022disabled\u0022:false}'))" class="contents">
    <div data-slot="alert-dialog" x-data="nqAlertDialog(false)" x-modelable="open" x-id="['nq-alert-dialog']" x-model="away" class="contents">
    <template x-teleport="body">
    <div data-slot="alert-dialog-portal">
        <div data-slot="alert-dialog-backdrop" x-nq-presence="open" class="fixed inset-0 z-50 bg-nq-fg/15 dark:bg-nq-bg/60 transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0"></div>
        <div data-slot="alert-dialog-content" x-bind="popup" x-nq-presence="open" x-trap.noscroll="open"
            class="fixed inset-0 z-50 m-auto grid h-fit w-[calc(100%-2rem)] max-w-md gap-4 rounded-floating border border-border bg-popover p-6 text-popover-foreground outline-none max-h-[calc(100dvh-2rem)] overflow-y-auto transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0">
            <div data-slot="alert-dialog-header" class="flex flex-col gap-1.5 text-start"><h2 data-slot="alert-dialog-title" :id="$id('nq-alert-dialog', 'title')" class="text-h3 text-foreground inline-flex items-center gap-2"><svg aria-hidden="true" class="size-5 text-nq-warning-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M10 2h4"/>
  <path d="M4.6 11a8 8 0 0 0 1.7 8.7 8 8 0 0 0 8.7 1.7"/>
  <path d="M7.4 7.4a8 8 0 0 1 10.3 1 8 8 0 0 1 .9 10.2"/>
  <path d="m2 2 20 20"/>
  <path d="M12 12v-2"/>
</svg>                    You were away</h2>
                <p data-slot="alert-dialog-description" :id="$id('nq-alert-dialog', 'description')" class="text-body-sm text-muted-foreground"><span x-text="description()"></span></p></div>
            <div data-slot="alert-dialog-footer" class="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"><button data-slot="button"
     type="button"                         x-on:click="discardAndStop()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover h-control px-[var(--nq-control-pad)]">
        Discard and stop</button>
                                <button data-slot="button"
     type="button"                         x-on:click="discard()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control px-[var(--nq-control-pad)]">
        <span x-text="discardLabel()"></span></button>
                <button data-slot="button"
     type="button"                         x-on:click="keep()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--nq-action)_88%,var(--nq-fg))] h-control px-[var(--nq-control-pad)]">
        <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M10 2v2"/>
  <path d="M14 2v2"/>
  <path d="M16 8a1 1 0 0 1 1 1v8a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1h14a4 4 0 1 1 0 8h-1"/>
  <path d="M6 2v2"/>
</svg>                    Keep the time</button></div>
        </div>
    </div>
</template>
</div>
</div>
```
