// BigInt keeps entered integers exact, even for very large countdowns. export function integer(value) { return /^\d+$/.test(String(value)) ? BigInt(value) : 0n; } export function splitSeconds(seconds) { return { minutes: String(seconds / 60n), seconds: String(seconds % 60n).padStart(2, '0'), }; } export function adjustTime(minutes, seconds, field, delta) { const total = integer(minutes) * 60n + integer(seconds); if (field === 'minutes') { const adjusted = total / 60n + delta; return splitSeconds((adjusted < 0n ? 0n : adjusted) * 60n + total % 60n); } const adjusted = total + delta; return splitSeconds(adjusted < 0n ? 0n : adjusted); } export class Countdown { constructor(now = Date.now) { this.now = now; this.state = 'idle'; this.finished = false; this.duration = 0n; this.remaining = 0n; this.deadline = 0n; } edit(minutes, seconds) { if (this.state === 'running') return; this.duration = (integer(minutes) * 60n + integer(seconds)) * 1000n; this.remaining = this.duration; this.state = 'idle'; this.finished = false; } update() { if (this.state !== 'running') return; const delta = this.deadline - BigInt(this.now()); this.remaining = delta > 0n ? delta : 0n; if (this.remaining === 0n) { this.state = 'idle'; this.finished = true; } } toggle() { if (this.state === 'running') { this.update(); if (!this.finished) this.state = 'paused'; return; } if (this.finished) this.remaining = this.duration; this.finished = false; this.deadline = BigInt(this.now()) + this.remaining; this.state = 'running'; this.update(); } reset() { this.remaining = this.duration; this.state = 'idle'; this.finished = false; } display() { // Round up so the timer only displays zero once its deadline is reached. return splitSeconds((this.remaining + 999n) / 1000n); } }