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();