import { CSSProperties, ReactNode, Ref } from 'react'; import { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types.js'; export type TimerSlots = 'root' | 'ringWrapper' | 'ringSvg' | 'ringTrack' | 'ringProgress' | 'ripples' | 'ripple' | 'display' | 'time' | 'label' | 'controls' | 'addButton' | 'removeButton' | 'pauseButton' | 'cancelButton'; export type TimerStatus = 'idle' | 'running' | 'paused' | 'completed' | 'cancelled'; export interface TimerHandle { /** Starts (or restarts, if idle/completed/cancelled) the countdown from its current duration. */ start: () => void; /** Pauses a running timer. No-op if not running. */ pause: () => void; /** Resumes a paused timer from where it left off. No-op if not paused. */ resume: () => void; /** Adds seconds to the remaining time. Clamped to `maxSeconds` if set. */ addTime: (seconds: number) => void; /** Removes seconds from the remaining time. Clamped to 0, which triggers completion. */ removeTime: (seconds: number) => void; /** Stops the timer and resets it back to idle at its original starting duration. Does not fire onComplete. */ cancel: () => void; /** Resets to idle at `durationSeconds` without starting. */ reset: () => void; /** Current status snapshot. */ getStatus: () => TimerStatus; /** Current remaining time in ms, read synchronously. */ getRemainingMs: () => number; } /** Text Timer renders of its own, for translating it. */ export interface TimerLabels { /** Tooltip on the cancel button. */ cancel?: string; } export interface TimerProps extends HtmlProps { /** Starting duration in seconds. Changing this prop while mounted resets the timer to the new duration. */ durationSeconds: number; /** Start ticking automatically on mount (and whenever `durationSeconds` changes). @default true */ autoStart?: boolean; /** When `durationSeconds` changes after mount, restart from the new duration immediately (`true`) or only update the idle baseline (`false`). @default true */ resetOnDurationChange?: boolean; /** Fires roughly every 200ms with the live remaining ms. */ onTick?: (remainingMs: number) => void; /** Fires exactly once when the countdown reaches zero. */ onComplete?: () => void; /** Fires when the timer transitions to running (initial start or resume). */ onStart?: () => void; /** Fires when paused. */ onPause?: () => void; /** Fires when resumed from pause. */ onResume?: () => void; /** Fires when cancelled (via button or ref). */ onCancel?: () => void; /** Fires whenever time is added, with the seconds added and the new total remaining seconds. */ onAddTime?: (secondsAdded: number, newRemainingSeconds: number) => void; /** Fires whenever time is removed. */ onRemoveTime?: (secondsRemoved: number, newRemainingSeconds: number) => void; /** Show the built-in control buttons (add/remove/pause-resume/cancel). @default true */ showControls?: boolean; /** Individually hide the add-time button while keeping the others. */ hideAddButton?: boolean; /** Individually hide the remove-time button while keeping the others. */ hideRemoveButton?: boolean; /** Individually hide the pause/resume button while keeping the others. */ hidePauseButton?: boolean; /** Individually hide the cancel button while keeping the others. */ hideCancelButton?: boolean; /** Seconds added/removed per click of the add/remove buttons. @default 30 */ adjustSeconds?: number; /** Optional cap on remaining time when adding. Unset = uncapped. */ maxSeconds?: number; /** Label rendered under the time readout, e.g. "Boil pasta". */ label?: ReactNode; /** Format the MM:SS (or H:MM:SS) readout. Defaults to a built-in formatter. */ formatTime?: (remainingMs: number) => ReactNode; /** Diameter of the ring, any CSS length. Sets `--timer-size`, which defaults to `10em`, 160px at a 16px base. */ size?: string; /** Ring stroke width in px. Sets `--timer-stroke-width`. @default 10 */ strokeWidth?: number; /** Sweeps the depleting boundary counter-clockwise instead of the default clockwise (matching a clock face). @default false */ counterClockwise?: boolean; /** Fraction of total duration (0-1) at which the ring switches to its urgent colour and the pulse (if enabled) kicks in. @default 0.1 */ urgentThreshold?: number; /** * Floor, in seconds, for how much time the urgent state gets — whichever of this or * `urgentThreshold` (as a fraction of the total duration) produces the larger window wins. * Without this, a short timer's `urgentThreshold` fraction can resolve to under a second, * making the urgent/critical state barely perceptible. @default 3 */ urgentMinSeconds?: number; /** Enables the heartbeat/pulse effect once inside `urgentThreshold`. @default true */ pulseOnUrgent?: boolean; /** Shows a thin ring pulsing outward from the dial's own edge and fading out while running (a beacon/live-indicator-style halo). @default false */ showRunningRipples?: boolean; /** Play a short built-in chime on completion. @default false */ playSoundOnComplete?: boolean; /** Overrides the built-in sound with a consumer-supplied audio URL. */ soundUrl?: string; /** Volume 0-1 for the completion sound. @default 0.5 */ soundVolume?: number; /** Show a browser Notification on completion (permission requested lazily on first `start()`, fails silently if denied/unsupported). @default false */ notifyOnComplete?: boolean; /** Notification title when `notifyOnComplete` is set. @default 'Timer done' */ notificationTitle?: string; /** Notification body when `notifyOnComplete` is set. Falls back to `label` when it's a string. */ notificationBody?: string; /** Imperative handle for starting, pausing and resetting the timer. */ ref?: Ref; className?: string; style?: CSSProperties; classNames?: SlotClassNames; styles?: SlotStyles; /** Overrides the text Timer renders of its own. Merged per key. */ labels?: TimerLabels; }