Productivity
POMODORO TIMER
Focus in 25-minute sprints with short and long breaks. Audio alert when each session ends.
25:00
FOCUS
0
Pomodoros
0m
Focus Time
0
Breaks Taken
Durations (minutes)
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
const CIRCUMFERENCE = 2 * Math.PI * 108; // 678.58
let durations = { work: 25 * 60, short: 5 * 60, long: 15 * 60 };
let mode = 'work';
let timeLeft = durations.work;
let totalTime = durations.work;
let running = false;
let interval = null;
let stats = { pomos: 0, focusMin: 0, breaks: 0 };
const modeColors = { work: 'var(--accent)', short: 'var(--success)', long: '#64b5f6' };
const modeLabels = { work: 'FOCUS', short: 'SHORT BREAK', long: 'LONG BREAK' };
const modeBtnClass = { work: 'work', short: 'short', long: 'long' };
function fmt(s) {
return String(Math.floor(s / 60)).padStart(2,'0') + ':' + String(s % 60).padStart(2,'0');
}
function updateDisplay() {
document.getElementById('timerTime').textContent = fmt(timeLeft);
document.getElementById('timerLabel').textContent = modeLabels[mode];
const progress = timeLeft / totalTime;
const offset = CIRCUMFERENCE * (1 - progress);
const ring = document.getElementById('ringProgress');
ring.style.strokeDashoffset = offset;
ring.style.stroke = modeColors[mode];
document.title = fmt(timeLeft) + ' — Pomodoro';
}
function setMode(m) {
if (running) stopTimer();
mode = m;
timeLeft = durations[m];
totalTime = durations[m];
document.querySelectorAll('.mode-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.mode-tab')[['work','short','long'].indexOf(m)].classList.add('active');
const btn = document.getElementById('btnStart');
btn.className = 'btn-start ' + modeBtnClass[m];
btn.textContent = 'START';
updateDisplay();
}
function toggleTimer() {
if (running) { stopTimer(); } else { startTimer(); }
}
function startTimer() {
running = true;
document.getElementById('btnStart').textContent = 'PAUSE';
interval = setInterval(() => {
if (timeLeft <= 0) {
clearInterval(interval);
running = false;
onSessionEnd();
return;
}
timeLeft--;
updateDisplay();
}, 1000);
}
function stopTimer() {
running = false;
clearInterval(interval);
document.getElementById('btnStart').textContent = 'RESUME';
}
function resetTimer() {
stopTimer();
timeLeft = durations[mode];
totalTime = durations[mode];
document.getElementById('btnStart').textContent = 'START';
document.title = 'Pomodoro Timer — toolpad.cc';
updateDisplay();
}
function onSessionEnd() {
playBeep();
document.getElementById('btnStart').textContent = 'START';
if (mode === 'work') {
stats.pomos++;
stats.focusMin += durations.work / 60;
} else {
stats.breaks++;
}
document.getElementById('statPomos').textContent = stats.pomos;
document.getElementById('statFocus').textContent = stats.focusMin + 'm';
document.getElementById('statBreaks').textContent = stats.breaks;
// auto-advance suggestion
if (Notification && Notification.permission === 'granted') {
new Notification('Pomodoro', { body: mode === 'work' ? 'Focus session complete! Take a break.' : 'Break over — back to work!' });
}
}
function playBeep() {
try {
const ctx = new (window.AudioContext || window.webkitAudioContext)();
[0, 0.3, 0.6].forEach(delay => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain); gain.connect(ctx.destination);
osc.frequency.value = 880;
osc.type = 'sine';
gain.gain.setValueAtTime(0.4, ctx.currentTime + delay);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + delay + 0.25);
osc.start(ctx.currentTime + delay);
osc.stop(ctx.currentTime + delay + 0.3);
});
} catch(e) {}
}
function updateDurations() {
const w = parseInt(document.getElementById('setWork').value) || 25;
const s = parseInt(document.getElementById('setShort').value) || 5;
const l = parseInt(document.getElementById('setLong').value) || 15;
durations = { work: w * 60, short: s * 60, long: l * 60 };
if (!running) { timeLeft = durations[mode]; totalTime = durations[mode]; updateDisplay(); }
}
// Request notification permission
if (Notification && Notification.permission === 'default') {
Notification.requestPermission();
}
updateDisplay();