# Mac focus timer

> An animated macOS menu bar focus timer for React and shadcn, with the countdown in the status item.

A focus timer that lives in the macOS menu bar: the countdown rolls in the status item, the popover holds the session, its length, Do Not Disturb and today's sessions, and the HUD confirms start and pause. At zero the status item's ring closes on a check, the HUD shows it, and the session slides into the list.

- Tier: Eased Pro
- Interaction: Press
- Page: https://eased.dev/components/mac-focus-timer
- Installs: motion, @base-ui/react

## Install

Mac focus timer ships with Eased Pro (https://eased.dev/#pricing). Members add the `@eased-pro` registry with their token (https://eased.dev/account), then run:

```bash
npx shadcn add @eased-pro/mac-focus-timer
```

A focus timer that lives in the macOS menu bar: a status item where the countdown rolls, a popover with the session, its length, Do Not Disturb and today's sessions, and a HUD that confirms start, pause and the finish. Built on Mac Menu Popover, Mac HUD, Number Roll, Text Swap, Button, Segmented and Switch.

## When to use

- A menu bar utility built around one running thing: a focus or break timer, a stopwatch, a time tracker.
- A Mac app whose state should stay visible while people work elsewhere: the countdown sits in the menu bar, and the popover holds everything else.
- A web page for your Mac app, showing it working on a desktop as it does on a Mac.

## When not to use

- A menu bar panel with no timer in it: use [Mac Menu Popover](/components/mac-menu-popover) with your own rows.
- A countdown inside a web page, such as an offer ending: use [Number Roll](/components/number-roll) on its own.

## Usage

Render it where your menu bar's status items go. It brings its own status item, popover and HUD.

```tsx menu-bar.tsx
import { MacFocusTimer, type FocusSession } from "@/components/eased/mac-focus-timer/mac-focus-timer";

export function MenuBar({ today, save }: { today: FocusSession[]; save: (s: FocusSession) => void }) {
  return (
    <MacFocusTimer
      name="Stint"
      defaultSessions={today}
      onSessionEnd={save}
      onDoNotDisturbChange={(on) => setDoNotDisturb(on)}
      onOpenSettings={() => openSettings()}
      onQuit={() => quit()}
    />
  );
}
```

The Do Not Disturb switch is a preference: `onDoNotDisturbChange` tells you when to turn the system's Do Not Disturb on and off, at the start and end of each session. Start and Resume close the popover, so the countdown carries on in the menu bar; Pause leaves it open. End stops the session early and keeps it as a session if it ran for a minute or more. A new length mid-session keeps the time already focused, and lengths shorter than that are disabled.

## Examples

### On a desktop

Keep the popover and HUD on one screen: pass the same element as the popover's collision boundary and the HUD's container. The HUD follows the element's box and shows above the open popover.

```tsx
const [screen, setScreen] = useState<HTMLDivElement | null>(null);

<div ref={setScreen} className="h-[560px]">
  <MenuBar>
    <MacFocusTimer collisionBoundary={screen ?? undefined} hudContainer={screen} />
  </MenuBar>
</div>
```

### Restoring a session

After a relaunch, mount it running with the time that was left.

```tsx
<MacFocusTimer defaultLength={50} defaultRemaining={saved.remaining} />
```

## API reference

### MacFocusTimer

| Prop | Type | Default | Description |
|---|---|---|---|
| `name` | `string` | `"Focus"` | The app's name: the panel's title, the status item's name and the Quit row. |
| `lengths` | `number[]` | `[15, 25, 50]` | Session lengths on offer, in minutes. |
| `defaultLength` | `number` | `25` | The selected length, in minutes. |
| `defaultRemaining` | `number` | | Mount with a session running and this many ms left. |
| `defaultSessions` | `FocusSession[]` | `[]` | Today's sessions, newest first. The panel lists the last three and totals them all. |
| `onSessionEnd` | `(session: FocusSession) => void` | | A session ran out, or was ended after a minute or more. |
| `defaultDoNotDisturb` | `boolean` | `false` | The switch: Do Not Disturb turns on for each session while it is on. |
| `onDoNotDisturbChange` | `(on: boolean) => void` | | When the system's Do Not Disturb should change: `true` as a session starts with the switch on (or it goes on mid-session), `false` when the session ends or the switch goes off. Pausing keeps it on. Toggling the switch between sessions calls nothing. Mounting with `defaultDoNotDisturb` and `defaultRemaining` calls it with `true` on mount; unmounting mid-session does not call it with `false`, so turn it off yourself on quit. |
| `onOpenSettings` / `onQuit` | `() => void` | | Add the Settings… (⌘,) and Quit (⌘Q) rows. Display only: wire the keys yourself. |
| `now` | `() => number` | `Date.now` | The clock, epoch ms. Pass a stable function. |
| `locale` / `timeZone` | `string` | the runtime's | For the sessions' end times. |
| `material` | `"css" \| "native"` | `"css"` | `native` drops the CSS blur on the panel and HUD for a window with real vibrancy. |
| `collisionBoundary` | `Element \| Element[]` | the viewport | The area the popover stays inside. |
| `hudContainer` | `HTMLElement \| null` | the viewport | An element the HUD sits over, centred near its bottom and above the open popover. `null` waits for the element. |

### FocusSession

| Field | Type | Description |
|---|---|---|
| `id` | `string` | |
| `length` | `number` | What it was set to, in minutes. |
| `focused` | `number` | Time focused, ms: the whole length when it ran out, less when it was ended early. |
| `endedAt` | `number` | Epoch ms. |

## Keyboard

| Key | Action |
|---|---|
| `Enter` / `Space` | On the status item, opens the popover with focus on its first control. |
| `Tab` | Moves through End, Start or Pause, Length, Do Not Disturb and the app's rows. |
| `←` / `→` | Change the length. |
| `Escape` | Closes the popover and returns focus to the status item. |

## Accessibility

- The status item is named for what it shows: "Stint, 12 minutes left", "Stint, paused, 12 minutes left", "Stint, session done". It names minutes, not seconds, so it doesn't change every second.
- Start, pause, resume and the finish are announced from the HUD's live region ("Paused, 12 minutes left", "Session done, 25 minutes focused"), so a screen reader hears them wherever focus is.
- The time in the panel is a `timer` named in minutes left. Sessions in the list say whether they were completed or ended early, as text as well as the mark: a filled disc with a check, or a partial ring.

## Motion

The countdown's digits roll down on the snappy spring in the menu bar and in the panel. The status item's icon is a ring that fills as the session goes, on the smooth spring; pause shows two bars inside it and the time dims. At zero the ring closes on a check, "Done" swaps in for the time while the item's width springs, the HUD shows a check and a full bar, and in the panel the session slides in at the top of Recent while the oldest fades and the day's total rolls. Every change after the first reaches the HUD in place: the glyph cross-fades and un-blurs on the text-swap tokens and the HUD's 1.1-second hold restarts. Buttons press by colour only, as on a Mac. Reduced motion keeps 100ms fades: nothing rolls, slides or springs.

## Notes for AI

Pick this for a Mac menu bar timer, or a page that shows one running. Pass today's sessions in `defaultSessions` and save new ones from `onSessionEnd`; wire Do Not Disturb, Settings and Quit through their callbacks. On a desktop mock-up, pass the screen as `collisionBoundary` and `hudContainer`.

Do not run your own interval or render another countdown, HUD or status item beside it: it keeps the clock, the HUD and the hold timer. Do not wrap the status item in a button or scale it on press.

## Related

- [Mac Menu Popover](/components/mac-menu-popover): the status item and panel it is built on.
- [Mac HUD](/components/mac-hud): the overlay that confirms each change.
- [Number Roll](/components/number-roll): the rolling digits in the countdown.
