function playCompletionChime() { try { const AudioContext = window.AudioContext || window.webkitAudioContext; if (!AudioContext) return; const context = new AudioContext(); const oscillator = context.createOscillator(); const gain = context.createGain(); oscillator.type = "sine"; oscillator.frequency.setValueAtTime(523.25, context.currentTime); oscillator.frequency.setValueAtTime(659.25, context.currentTime + 0.12); gain.gain.setValueAtTime(0.0001, context.currentTime); gain.gain.exponentialRampToValueAtTime(0.12, context.currentTime + 0.02); gain.gain.exponentialRampToValueAtTime(0.0001, context.currentTime + 0.35); oscillator.connect(gain); gain.connect(context.destination); oscillator.start(); oscillator.stop(context.currentTime + 0.36); oscillator.addEventListener("ended", () => context.close()); } catch (_error) { // Audio feedback is optional; browser policy must never block completion. } } function initializeTimer(timer) { const duration = Number(timer.dataset.duration); const taskId = timer.dataset.taskId; const storageKey = `steady.timer.${taskId}`; const display = timer.querySelector(".timer__display"); const progress = timer.querySelector(".timer__progress span"); const announcement = timer.querySelector(".timer__announcement"); let intervalId; function readState() { try { const stored = JSON.parse(localStorage.getItem(storageKey)); if (stored && Number.isFinite(stored.remaining)) return stored; } catch (_error) { // A corrupt or unavailable local store simply starts a fresh timer. } return { remaining: duration, running: false, updatedAt: Date.now() }; } let state = readState(); function currentRemaining() { if (!state.running) return state.remaining; const elapsed = Math.floor((Date.now() - state.updatedAt) / 1000); return Math.max(0, state.remaining - elapsed); } function saveState() { try { localStorage.setItem(storageKey, JSON.stringify(state)); } catch (_error) { // The countdown still works for this page when storage is unavailable. } } function render() { const remaining = currentRemaining(); const minutes = Math.floor(remaining / 60); const seconds = remaining % 60; display.textContent = `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`; progress.style.width = `${Math.max(0, Math.min(100, (remaining / duration) * 100))}%`; if (remaining === 0 && state.running) { state = { remaining: 0, running: false, updatedAt: Date.now() }; saveState(); clearInterval(intervalId); timer.classList.add("timer--complete"); announcement.textContent = "Focus block complete. Take a breath."; if (timer.dataset.chime === "true") playCompletionChime(); } } timer.querySelector("[data-timer-start]").addEventListener("click", () => { const remaining = currentRemaining() || duration; state = { remaining, running: true, updatedAt: Date.now() }; timer.classList.remove("timer--complete"); saveState(); clearInterval(intervalId); intervalId = window.setInterval(render, 250); render(); }); timer.querySelector("[data-timer-pause]").addEventListener("click", () => { state = { remaining: currentRemaining(), running: false, updatedAt: Date.now(), }; saveState(); clearInterval(intervalId); render(); }); timer.querySelector("[data-timer-reset]").addEventListener("click", () => { state = { remaining: duration, running: false, updatedAt: Date.now() }; saveState(); clearInterval(intervalId); timer.classList.remove("timer--complete"); announcement.textContent = "Timer reset."; render(); }); if (state.running) intervalId = window.setInterval(render, 250); render(); } document.querySelectorAll("[data-focus-timer]").forEach(initializeTimer); const celebration = document.querySelector('[data-celebrate="true"]'); if (celebration && celebration.dataset.chime === "true") { playCompletionChime(); }