More Dashboards Snippets
Pomodoro Timer — Free HTML CSS JS Snippet
Pomodoro Timer · Dashboards · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Pomodoro Timer — Circular SVG Ring, Auto-Cycling Focus & Break Modes

The Pomodoro Technique breaks work into focused 25-minute intervals separated by short breaks, with a longer break after every fourth session. This snippet implements a complete, polished Pomodoro timer with a circular SVG progress ring, three modes (Focus, Short Break, Long Break), automatic cycling between focus and breaks, a session counter, a round tracker, accumulated focus minutes, and an ambient background colour that shifts with the active mode.
The circular SVG progress ring
The ring is two stacked SVG circles. The background circle is a faint track; the foreground circle is the animated progress indicator. The technique relies on stroke-dasharray and stroke-dashoffset: the dasharray equals the circle's circumference (2πr ≈ 678.58 for r=108), and the dashoffset is animated from 0 to the full circumference to "drain" the ring as time elapses. The SVG is rotated −90° so the ring starts depleting from the top, and stroke-linecap: round gives the progress end a soft cap.
The countdown engine
A setInterval fires every second, decrementing the remaining seconds and calling render(), which formats MM:SS with padStart and updates the ring offset proportionally (remaining ÷ total duration). When remaining hits zero, complete() runs. Storing remaining as a plain seconds integer — rather than wall-clock timestamps — keeps the logic simple, though for background-tab accuracy you would reconcile against Date.now() on each tick.
Auto-cycling and the Pomodoro rules
complete() encodes the classic Pomodoro flow: after a focus session it increments the session count and accumulated focus minutes, then switches to a short break — unless it was the 4th session, in which case it switches to a long break and advances the round. After any break it returns to focus mode. This automatic mode-switching is what distinguishes a true Pomodoro timer from a plain countdown.
Mode theming
Each mode has its own ring colour and a body background class. switchMode() updates the active mode button, the phase label, the ring stroke colour, and the body class, which drives a 0.6-second background transition. The colour shift gives an ambient, glanceable signal of whether you should be working or resting without reading any text.
Controls
The Start button toggles to Pause while running and visually de-emphasises. Reset returns the current mode to its full duration. Skip jumps straight to completion, advancing to the next phase — useful when you finish early or want to move on.
Step by step
How to Use
- 1Start a focus sessionClick Start to begin the 25-minute focus countdown. The ring drains as time passes and the button switches to Pause.
- 2Take breaks automaticallyWhen a focus session ends, the timer switches to a short break on its own. After four focus sessions it switches to a long break and advances the round counter.
- 3Switch modes manuallyUse the Focus, Short Break, and Long Break tabs to jump to any mode. The background colour shifts to match, giving an ambient cue.
- 4Pause, reset, or skipPause stops the countdown without losing your place. Reset restores the current mode to full time. Skip jumps to the end and advances to the next phase.
- 5Track your progressThe stats row shows completed sessions, the current round out of four, and total focus minutes accumulated for the day.
- 6Export for your frameworkClick "JSX" for a React component using useEffect for the interval and useState for the timer state. Click "Vue" for a Vue 3 SFC with onMounted/onUnmounted cleanup.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
The Pomodoro Technique, created by Francesco Cirillo, breaks work into 25-minute focused intervals (Pomodoros) separated by 5-minute short breaks, with a longer 15-30 minute break after every four Pomodoros. This timer encodes that flow exactly: completing a focus session automatically starts a short break, and every fourth completion triggers a long break and advances the round counter. You can also switch modes manually at any time.
Browsers throttle setInterval in inactive tabs, so a pure per-second counter can drift behind in the background. For accuracy, store the target end time as Date.now() + remaining * 1000 when starting, and on each tick compute remaining = Math.round((endTime - Date.now()) / 1000) instead of simply decrementing. This reconciles against the real clock so the timer stays correct even after the tab was backgrounded.
In the complete() function, play a sound with the Web Audio API or an Audio element: new Audio("/chime.mp3").play(). For desktop notifications, first request permission with Notification.requestPermission() on a user gesture, then fire new Notification("Break time!", { body: "Focus session complete" }) when a phase ends. Combine both so users get an audible and visual cue even when the tab is not focused. Browsers block autoplay audio until the user has interacted with the page, so trigger the first sound from the Start button click to unlock the audio context for later automatic plays.
Each mode adds a class to the document body (focus, short, or long), and the body has a 0.6-second background transition. Focus mode is a deep purple, short break a calm green, and long break a restful blue. This ambient colour shift gives a glanceable, peripheral signal of whether you should be working or resting — you can tell your current state from across the room without reading the timer. Colour-coding states this way reduces the cognitive load of context-switching between work and rest.
Store mode, remaining, running, sessions, and round in useState. Run the countdown in a useEffect that sets an interval when running is true and clears it on cleanup: useEffect(() => { if (!running) return; const id = setInterval(() => setRemaining(r => r - 1), 1000); return () => clearInterval(id); }, [running]). Watch remaining for zero in another effect to trigger the auto-cycle. Compute the ring dashoffset from remaining / duration.
Pomodoro Timer — Export as HTML, React, Vue, Angular & Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Pomodoro Timer</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #1a1625; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; transition: background 0.6s; }
body.focus { background: #2d1b3d; }
body.short { background: #163d2e; }
body.long { background: #15324d; }
.wrap { width: 100%; max-width: 380px; }
.card { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 28px; padding: 26px; backdrop-filter: blur(20px); }
.modes { display: flex; gap: 4px; background: rgba(0,0,0,0.2); border-radius: 14px; padding: 4px; margin-bottom: 28px; }
.mode-btn { flex: 1; padding: 10px 4px; border: none; background: none; border-radius: 10px; font-size: 12px; font-weight: 700; color: rgba(255,255,255,0.5); cursor: pointer; transition: all 0.2s; white-space: nowrap; }
.mode-btn.active { background: rgba(255,255,255,0.12); color: #fff; }
.dial { position: relative; width: 240px; height: 240px; margin: 0 auto 28px; }
.ring { width: 100%; height: 100%; transform: rotate(-90deg); }
.ring-bg { fill: none; stroke: rgba(255,255,255,0.07); stroke-width: 10; }
.ring-fg { fill: none; stroke: #c084fc; stroke-width: 10; stroke-linecap: round; stroke-dasharray: 678.58; stroke-dashoffset: 0; transition: stroke-dashoffset 1s linear, stroke 0.5s; }
.dial-center { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.time { font-size: 56px; font-weight: 800; color: #fff; font-variant-numeric: tabular-nums; letter-spacing: -1px; }
.phase { font-size: 13px; color: rgba(255,255,255,0.5); margin-top: 4px; }
.controls { display: flex; align-items: center; justify-content: center; gap: 16px; margin-bottom: 26px; }
.ctrl-btn { cursor: pointer; transition: all 0.15s; }
.ctrl-btn.main { background: #fff; color: #2d1b3d; border: none; padding: 14px 44px; border-radius: 16px; font-size: 16px; font-weight: 800; letter-spacing: 0.5px; }
.ctrl-btn.main:hover { transform: scale(1.04); }
.ctrl-btn.main.running { background: rgba(255,255,255,0.15); color: #fff; }
.ctrl-btn.reset, .ctrl-btn.skip { width: 44px; height: 44px; border-radius: 12px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); color: rgba(255,255,255,0.6); display: flex; align-items: center; justify-content: center; }
.ctrl-btn.reset:hover, .ctrl-btn.skip:hover { background: rgba(255,255,255,0.12); color: #fff; }
.stats { display: grid; grid-template-columns: repeat(3,1fr); gap: 10px; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 20px; }
.stat { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.stat-num { font-size: 22px; font-weight: 800; color: #fff; font-variant-numeric: tabular-nums; }
.of { font-size: 13px; color: rgba(255,255,255,0.4); }
.stat-label { font-size: 11px; color: rgba(255,255,255,0.4); }
</style>
</head>
<body>
<div class="wrap">
<div class="card">
<div class="modes">
<button class="mode-btn active" onclick="setMode(this,'focus')">Focus</button>
<button class="mode-btn" onclick="setMode(this,'short')">Short Break</button>
<button class="mode-btn" onclick="setMode(this,'long')">Long Break</button>
</div>
<div class="dial">
<svg class="ring" viewBox="0 0 240 240">
<circle class="ring-bg" cx="120" cy="120" r="108"/>
<circle class="ring-fg" id="ring" cx="120" cy="120" r="108"/>
</svg>
<div class="dial-center">
<div class="time" id="time">25:00</div>
<div class="phase" id="phase">Time to focus</div>
</div>
</div>
<div class="controls">
<button class="ctrl-btn reset" onclick="reset()" title="Reset">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
</button>
<button class="ctrl-btn main" id="startBtn" onclick="toggle()">Start</button>
<button class="ctrl-btn skip" onclick="skip()" title="Skip">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><polygon points="5 4 15 12 5 20 5 4"/><line x1="19" y1="5" x2="19" y2="19"/></svg>
</button>
</div>
<div class="stats">
<div class="stat"><span class="stat-num" id="doneCount">0</span><span class="stat-label">Sessions</span></div>
<div class="stat"><span class="stat-num" id="roundCount">1<span class="of">/4</span></span><span class="stat-label">Round</span></div>
<div class="stat"><span class="stat-num" id="focusMin">0</span><span class="stat-label">Focus min</span></div>
</div>
</div>
</div>
<script>
var DURATIONS = { focus: 25 * 60, short: 5 * 60, long: 15 * 60 };
var COLORS = { focus: '#c084fc', short: '#4ade80', long: '#60a5fa' };
var PHASES = { focus: 'Time to focus', short: 'Take a short break', long: 'Take a long break' };
var CIRC = 678.58;
var mode = 'focus';
var remaining = DURATIONS.focus;
var running = false;
var timer = null;
var sessions = 0;
var round = 1;
var focusMinutes = 0;
document.body.classList.add('focus');
function render() {
var m = Math.floor(remaining / 60);
var s = remaining % 60;
document.getElementById('time').textContent = String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
var pct = remaining / DURATIONS[mode];
document.getElementById('ring').style.strokeDashoffset = CIRC * (1 - pct);
}
function tick() {
if (remaining > 0) {
remaining--;
if (mode === 'focus' && remaining % 60 === 0) { /* whole minute elapsed handled on complete */ }
render();
} else {
complete();
}
}
function complete() {
clearInterval(timer);
running = false;
if (mode === 'focus') {
sessions++;
focusMinutes += DURATIONS.focus / 60;
document.getElementById('doneCount').textContent = sessions;
document.getElementById('focusMin').textContent = focusMinutes;
var next = (sessions % 4 === 0) ? 'long' : 'short';
if (sessions % 4 === 0) { round++; }
switchMode(next);
} else {
switchMode('focus');
}
document.getElementById('roundCount').innerHTML = round + '<span class="of">/4</span>';
updateStartBtn();
}
function toggle() {
running = !running;
if (running) { timer = setInterval(tick, 1000); }
else { clearInterval(timer); }
updateStartBtn();
}
function updateStartBtn() {
var btn = document.getElementById('startBtn');
btn.textContent = running ? 'Pause' : 'Start';
btn.classList.toggle('running', running);
}
function reset() {
clearInterval(timer);
running = false;
remaining = DURATIONS[mode];
render();
updateStartBtn();
}
function skip() {
remaining = 0;
complete();
}
function switchMode(m) {
mode = m;
remaining = DURATIONS[m];
document.querySelectorAll('.mode-btn').forEach(function(b, i) {
b.classList.toggle('active', ['focus', 'short', 'long'][i] === m);
});
document.getElementById('phase').textContent = PHASES[m];
document.getElementById('ring').style.stroke = COLORS[m];
document.body.className = m;
render();
}
function setMode(btn, m) {
clearInterval(timer);
running = false;
switchMode(m);
updateStartBtn();
}
render();
</script>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Pomodoro Timer</title>
<!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
* {
box-sizing: border-box; margin: 0; padding: 0;
}
body {
font-family: system-ui, sans-serif; background: #1a1625; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; transition: background 0.6s;
}
body.focus {
background: #2d1b3d;
}
body.short {
background: #163d2e;
}
body.long {
background: #15324d;
}
.mode-btn.active {
background: rgba(255,255,255,0.12); color: #fff;
}
.ring {
width: 100%; height: 100%; transform: rotate(-90deg);
}
.time {
font-size: 56px; font-weight: 800; color: #fff; font-variant-numeric: tabular-nums; letter-spacing: -1px;
}
.phase {
font-size: 13px; color: rgba(255,255,255,0.5); margin-top: 4px;
}
.ctrl-btn.main {
background: #fff; color: #2d1b3d; border: none; padding: 14px 44px; border-radius: 16px; font-size: 16px; font-weight: 800; letter-spacing: 0.5px;
}
.ctrl-btn.main:hover {
transform: scale(1.04);
}
.ctrl-btn.main.running {
background: rgba(255,255,255,0.15); color: #fff;
}
.ctrl-btn.reset, .ctrl-btn.skip {
width: 44px; height: 44px; border-radius: 12px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); color: rgba(255,255,255,0.6); display: flex; align-items: center; justify-content: center;
}
.ctrl-btn.reset:hover, .ctrl-btn.skip:hover {
background: rgba(255,255,255,0.12); color: #fff;
}
.of {
font-size: 13px; color: rgba(255,255,255,0.4);
}
</style>
</head>
<body>
<div class="w-full max-w-[380px]">
<div class="bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.08)] rounded-[28px] p-[26px] [backdrop-filter:blur(20px)]">
<div class="flex gap-1 bg-[rgba(0,0,0,0.2)] rounded-[14px] p-1 mb-7">
<button class="mode-btn flex-1 py-2.5 px-1 border-0 bg-transparent rounded-[10px] text-xs font-bold text-[rgba(255,255,255,0.5)] cursor-pointer [transition:all_0.2s] whitespace-nowrap active" onclick="setMode(this,'focus')">Focus</button>
<button class="mode-btn flex-1 py-2.5 px-1 border-0 bg-transparent rounded-[10px] text-xs font-bold text-[rgba(255,255,255,0.5)] cursor-pointer [transition:all_0.2s] whitespace-nowrap" onclick="setMode(this,'short')">Short Break</button>
<button class="mode-btn flex-1 py-2.5 px-1 border-0 bg-transparent rounded-[10px] text-xs font-bold text-[rgba(255,255,255,0.5)] cursor-pointer [transition:all_0.2s] whitespace-nowrap" onclick="setMode(this,'long')">Long Break</button>
</div>
<div class="relative w-60 h-60 mt-0 mx-auto mb-7">
<svg class="ring" viewBox="0 0 240 240">
<circle class="fill-none stroke-[rgba(255,255,255,0.07)] [stroke-width:10]" cx="120" cy="120" r="108"/>
<circle class="fill-none stroke-[#c084fc] [stroke-width:10] [stroke-linecap:round] [stroke-dasharray:678.58] [stroke-dashoffset:0] [transition:stroke-dashoffset_1s_linear,_stroke_0.5s]" id="ring" cx="120" cy="120" r="108"/>
</svg>
<div class="absolute inset-0 flex flex-col items-center justify-center">
<div class="time" id="time">25:00</div>
<div class="phase" id="phase">Time to focus</div>
</div>
</div>
<div class="flex items-center justify-center gap-4 mb-[26px]">
<button class="ctrl-btn cursor-pointer [transition:all_0.15s] reset" onclick="reset()" title="Reset">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
</button>
<button class="ctrl-btn cursor-pointer [transition:all_0.15s] main" id="startBtn" onclick="toggle()">Start</button>
<button class="ctrl-btn cursor-pointer [transition:all_0.15s] skip" onclick="skip()" title="Skip">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><polygon points="5 4 15 12 5 20 5 4"/><line x1="19" y1="5" x2="19" y2="19"/></svg>
</button>
</div>
<div class="grid grid-cols-3 gap-2.5 border-t border-t-[rgba(255,255,255,0.08)] pt-5">
<div class="flex flex-col items-center gap-[3px]"><span class="text-[22px] font-extrabold text-[#fff] [font-variant-numeric:tabular-nums]" id="doneCount">0</span><span class="text-[11px] text-[rgba(255,255,255,0.4)]">Sessions</span></div>
<div class="flex flex-col items-center gap-[3px]"><span class="text-[22px] font-extrabold text-[#fff] [font-variant-numeric:tabular-nums]" id="roundCount">1<span class="of">/4</span></span><span class="text-[11px] text-[rgba(255,255,255,0.4)]">Round</span></div>
<div class="flex flex-col items-center gap-[3px]"><span class="text-[22px] font-extrabold text-[#fff] [font-variant-numeric:tabular-nums]" id="focusMin">0</span><span class="text-[11px] text-[rgba(255,255,255,0.4)]">Focus min</span></div>
</div>
</div>
</div>
<script>
var DURATIONS = { focus: 25 * 60, short: 5 * 60, long: 15 * 60 };
var COLORS = { focus: '#c084fc', short: '#4ade80', long: '#60a5fa' };
var PHASES = { focus: 'Time to focus', short: 'Take a short break', long: 'Take a long break' };
var CIRC = 678.58;
var mode = 'focus';
var remaining = DURATIONS.focus;
var running = false;
var timer = null;
var sessions = 0;
var round = 1;
var focusMinutes = 0;
document.body.classList.add('focus');
function render() {
var m = Math.floor(remaining / 60);
var s = remaining % 60;
document.getElementById('time').textContent = String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
var pct = remaining / DURATIONS[mode];
document.getElementById('ring').style.strokeDashoffset = CIRC * (1 - pct);
}
function tick() {
if (remaining > 0) {
remaining--;
if (mode === 'focus' && remaining % 60 === 0) { /* whole minute elapsed handled on complete */ }
render();
} else {
complete();
}
}
function complete() {
clearInterval(timer);
running = false;
if (mode === 'focus') {
sessions++;
focusMinutes += DURATIONS.focus / 60;
document.getElementById('doneCount').textContent = sessions;
document.getElementById('focusMin').textContent = focusMinutes;
var next = (sessions % 4 === 0) ? 'long' : 'short';
if (sessions % 4 === 0) { round++; }
switchMode(next);
} else {
switchMode('focus');
}
document.getElementById('roundCount').innerHTML = round + '<span class="of">/4</span>';
updateStartBtn();
}
function toggle() {
running = !running;
if (running) { timer = setInterval(tick, 1000); }
else { clearInterval(timer); }
updateStartBtn();
}
function updateStartBtn() {
var btn = document.getElementById('startBtn');
btn.textContent = running ? 'Pause' : 'Start';
btn.classList.toggle('running', running);
}
function reset() {
clearInterval(timer);
running = false;
remaining = DURATIONS[mode];
render();
updateStartBtn();
}
function skip() {
remaining = 0;
complete();
}
function switchMode(m) {
mode = m;
remaining = DURATIONS[m];
document.querySelectorAll('.mode-btn').forEach(function(b, i) {
b.classList.toggle('active', ['focus', 'short', 'long'][i] === m);
});
document.getElementById('phase').textContent = PHASES[m];
document.getElementById('ring').style.stroke = COLORS[m];
document.body.className = m;
render();
}
function setMode(btn, m) {
clearInterval(timer);
running = false;
switchMode(m);
updateStartBtn();
}
render();
</script>
</body>
</html>import React, { useEffect } from 'react';
// CSS — optionally move to PomodoroTimer.module.css
const css = `
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #1a1625; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; transition: background 0.6s; }
body.focus { background: #2d1b3d; }
body.short { background: #163d2e; }
body.long { background: #15324d; }
.wrap { width: 100%; max-width: 380px; }
.card { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 28px; padding: 26px; backdrop-filter: blur(20px); }
.modes { display: flex; gap: 4px; background: rgba(0,0,0,0.2); border-radius: 14px; padding: 4px; margin-bottom: 28px; }
.mode-btn { flex: 1; padding: 10px 4px; border: none; background: none; border-radius: 10px; font-size: 12px; font-weight: 700; color: rgba(255,255,255,0.5); cursor: pointer; transition: all 0.2s; white-space: nowrap; }
.mode-btn.active { background: rgba(255,255,255,0.12); color: #fff; }
.dial { position: relative; width: 240px; height: 240px; margin: 0 auto 28px; }
.ring { width: 100%; height: 100%; transform: rotate(-90deg); }
.ring-bg { fill: none; stroke: rgba(255,255,255,0.07); stroke-width: 10; }
.ring-fg { fill: none; stroke: #c084fc; stroke-width: 10; stroke-linecap: round; stroke-dasharray: 678.58; stroke-dashoffset: 0; transition: stroke-dashoffset 1s linear, stroke 0.5s; }
.dial-center { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.time { font-size: 56px; font-weight: 800; color: #fff; font-variant-numeric: tabular-nums; letter-spacing: -1px; }
.phase { font-size: 13px; color: rgba(255,255,255,0.5); margin-top: 4px; }
.controls { display: flex; align-items: center; justify-content: center; gap: 16px; margin-bottom: 26px; }
.ctrl-btn { cursor: pointer; transition: all 0.15s; }
.ctrl-btn.main { background: #fff; color: #2d1b3d; border: none; padding: 14px 44px; border-radius: 16px; font-size: 16px; font-weight: 800; letter-spacing: 0.5px; }
.ctrl-btn.main:hover { transform: scale(1.04); }
.ctrl-btn.main.running { background: rgba(255,255,255,0.15); color: #fff; }
.ctrl-btn.reset, .ctrl-btn.skip { width: 44px; height: 44px; border-radius: 12px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); color: rgba(255,255,255,0.6); display: flex; align-items: center; justify-content: center; }
.ctrl-btn.reset:hover, .ctrl-btn.skip:hover { background: rgba(255,255,255,0.12); color: #fff; }
.stats { display: grid; grid-template-columns: repeat(3,1fr); gap: 10px; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 20px; }
.stat { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.stat-num { font-size: 22px; font-weight: 800; color: #fff; font-variant-numeric: tabular-nums; }
.of { font-size: 13px; color: rgba(255,255,255,0.4); }
.stat-label { font-size: 11px; color: rgba(255,255,255,0.4); }
`;
export default function PomodoroTimer() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
var DURATIONS = { focus: 25 * 60, short: 5 * 60, long: 15 * 60 };
var COLORS = { focus: '#c084fc', short: '#4ade80', long: '#60a5fa' };
var PHASES = { focus: 'Time to focus', short: 'Take a short break', long: 'Take a long break' };
var CIRC = 678.58;
var mode = 'focus';
var remaining = DURATIONS.focus;
var running = false;
var timer = null;
var sessions = 0;
var round = 1;
var focusMinutes = 0;
document.body.classList.add('focus');
function render() {
var m = Math.floor(remaining / 60);
var s = remaining % 60;
document.getElementById('time').textContent = String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
var pct = remaining / DURATIONS[mode];
document.getElementById('ring').style.strokeDashoffset = CIRC * (1 - pct);
}
function tick() {
if (remaining > 0) {
remaining--;
if (mode === 'focus' && remaining % 60 === 0) { /* whole minute elapsed handled on complete */ }
render();
} else {
complete();
}
}
function complete() {
clearInterval(timer);
running = false;
if (mode === 'focus') {
sessions++;
focusMinutes += DURATIONS.focus / 60;
document.getElementById('doneCount').textContent = sessions;
document.getElementById('focusMin').textContent = focusMinutes;
var next = (sessions % 4 === 0) ? 'long' : 'short';
if (sessions % 4 === 0) { round++; }
switchMode(next);
} else {
switchMode('focus');
}
document.getElementById('roundCount').innerHTML = round + '<span class="of">/4</span>';
updateStartBtn();
}
function toggle() {
running = !running;
if (running) { timer = setInterval(tick, 1000); }
else { clearInterval(timer); }
updateStartBtn();
}
function updateStartBtn() {
var btn = document.getElementById('startBtn');
btn.textContent = running ? 'Pause' : 'Start';
btn.classList.toggle('running', running);
}
function reset() {
clearInterval(timer);
running = false;
remaining = DURATIONS[mode];
render();
updateStartBtn();
}
function skip() {
remaining = 0;
complete();
}
function switchMode(m) {
mode = m;
remaining = DURATIONS[m];
document.querySelectorAll('.mode-btn').forEach(function(b, i) {
b.classList.toggle('active', ['focus', 'short', 'long'][i] === m);
});
document.getElementById('phase').textContent = PHASES[m];
document.getElementById('ring').style.stroke = COLORS[m];
document.body.className = m;
render();
}
function setMode(btn, m) {
clearInterval(timer);
running = false;
switchMode(m);
updateStartBtn();
}
render();
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
// Expose handlers used by inline JSX events to the global scope
if (typeof setMode === 'function') window.setMode = setMode;
if (typeof reset === 'function') window.reset = reset;
if (typeof toggle === 'function') window.toggle = toggle;
if (typeof skip === 'function') window.skip = skip;
}, []);
return (
<>
<style>{css}</style>
<div className="wrap">
<div className="card">
<div className="modes">
<button className="mode-btn active" onClick={(event) => { setMode(event.currentTarget,'focus') }}>Focus</button>
<button className="mode-btn" onClick={(event) => { setMode(event.currentTarget,'short') }}>Short Break</button>
<button className="mode-btn" onClick={(event) => { setMode(event.currentTarget,'long') }}>Long Break</button>
</div>
<div className="dial">
<svg className="ring" viewBox="0 0 240 240">
<circle className="ring-bg" cx="120" cy="120" r="108"/>
<circle className="ring-fg" id="ring" cx="120" cy="120" r="108"/>
</svg>
<div className="dial-center">
<div className="time" id="time">25:00</div>
<div className="phase" id="phase">Time to focus</div>
</div>
</div>
<div className="controls">
<button className="ctrl-btn reset" onClick={(event) => { reset() }} title="Reset">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2"><path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
</button>
<button className="ctrl-btn main" id="startBtn" onClick={(event) => { toggle() }}>Start</button>
<button className="ctrl-btn skip" onClick={(event) => { skip() }} title="Skip">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2"><polygon points="5 4 15 12 5 20 5 4"/><line x1="19" y1="5" x2="19" y2="19"/></svg>
</button>
</div>
<div className="stats">
<div className="stat"><span className="stat-num" id="doneCount">0</span><span className="stat-label">Sessions</span></div>
<div className="stat"><span className="stat-num" id="roundCount">1<span className="of">/4</span></span><span className="stat-label">Round</span></div>
<div className="stat"><span className="stat-num" id="focusMin">0</span><span className="stat-label">Focus min</span></div>
</div>
</div>
</div>
</>
);
}import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+
export default function PomodoroTimer() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
var DURATIONS = { focus: 25 * 60, short: 5 * 60, long: 15 * 60 };
var COLORS = { focus: '#c084fc', short: '#4ade80', long: '#60a5fa' };
var PHASES = { focus: 'Time to focus', short: 'Take a short break', long: 'Take a long break' };
var CIRC = 678.58;
var mode = 'focus';
var remaining = DURATIONS.focus;
var running = false;
var timer = null;
var sessions = 0;
var round = 1;
var focusMinutes = 0;
document.body.classList.add('focus');
function render() {
var m = Math.floor(remaining / 60);
var s = remaining % 60;
document.getElementById('time').textContent = String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
var pct = remaining / DURATIONS[mode];
document.getElementById('ring').style.strokeDashoffset = CIRC * (1 - pct);
}
function tick() {
if (remaining > 0) {
remaining--;
if (mode === 'focus' && remaining % 60 === 0) { /* whole minute elapsed handled on complete */ }
render();
} else {
complete();
}
}
function complete() {
clearInterval(timer);
running = false;
if (mode === 'focus') {
sessions++;
focusMinutes += DURATIONS.focus / 60;
document.getElementById('doneCount').textContent = sessions;
document.getElementById('focusMin').textContent = focusMinutes;
var next = (sessions % 4 === 0) ? 'long' : 'short';
if (sessions % 4 === 0) { round++; }
switchMode(next);
} else {
switchMode('focus');
}
document.getElementById('roundCount').innerHTML = round + '<span class="of">/4</span>';
updateStartBtn();
}
function toggle() {
running = !running;
if (running) { timer = setInterval(tick, 1000); }
else { clearInterval(timer); }
updateStartBtn();
}
function updateStartBtn() {
var btn = document.getElementById('startBtn');
btn.textContent = running ? 'Pause' : 'Start';
btn.classList.toggle('running', running);
}
function reset() {
clearInterval(timer);
running = false;
remaining = DURATIONS[mode];
render();
updateStartBtn();
}
function skip() {
remaining = 0;
complete();
}
function switchMode(m) {
mode = m;
remaining = DURATIONS[m];
document.querySelectorAll('.mode-btn').forEach(function(b, i) {
b.classList.toggle('active', ['focus', 'short', 'long'][i] === m);
});
document.getElementById('phase').textContent = PHASES[m];
document.getElementById('ring').style.stroke = COLORS[m];
document.body.className = m;
render();
}
function setMode(btn, m) {
clearInterval(timer);
running = false;
switchMode(m);
updateStartBtn();
}
render();
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
// Expose handlers used by inline JSX events to the global scope
if (typeof setMode === 'function') window.setMode = setMode;
if (typeof reset === 'function') window.reset = reset;
if (typeof toggle === 'function') window.toggle = toggle;
if (typeof skip === 'function') window.skip = skip;
}, []);
return (
<>
<style>{`
* {
box-sizing: border-box; margin: 0; padding: 0;
}
body {
font-family: system-ui, sans-serif; background: #1a1625; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; transition: background 0.6s;
}
body.focus {
background: #2d1b3d;
}
body.short {
background: #163d2e;
}
body.long {
background: #15324d;
}
.mode-btn.active {
background: rgba(255,255,255,0.12); color: #fff;
}
.ring {
width: 100%; height: 100%; transform: rotate(-90deg);
}
.time {
font-size: 56px; font-weight: 800; color: #fff; font-variant-numeric: tabular-nums; letter-spacing: -1px;
}
.phase {
font-size: 13px; color: rgba(255,255,255,0.5); margin-top: 4px;
}
.ctrl-btn.main {
background: #fff; color: #2d1b3d; border: none; padding: 14px 44px; border-radius: 16px; font-size: 16px; font-weight: 800; letter-spacing: 0.5px;
}
.ctrl-btn.main:hover {
transform: scale(1.04);
}
.ctrl-btn.main.running {
background: rgba(255,255,255,0.15); color: #fff;
}
.ctrl-btn.reset, .ctrl-btn.skip {
width: 44px; height: 44px; border-radius: 12px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); color: rgba(255,255,255,0.6); display: flex; align-items: center; justify-content: center;
}
.ctrl-btn.reset:hover, .ctrl-btn.skip:hover {
background: rgba(255,255,255,0.12); color: #fff;
}
.of {
font-size: 13px; color: rgba(255,255,255,0.4);
}
`}</style>
<div className="w-full max-w-[380px]">
<div className="bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.08)] rounded-[28px] p-[26px] [backdrop-filter:blur(20px)]">
<div className="flex gap-1 bg-[rgba(0,0,0,0.2)] rounded-[14px] p-1 mb-7">
<button className="mode-btn flex-1 py-2.5 px-1 border-0 bg-transparent rounded-[10px] text-xs font-bold text-[rgba(255,255,255,0.5)] cursor-pointer [transition:all_0.2s] whitespace-nowrap active" onClick={(event) => { setMode(event.currentTarget,'focus') }}>Focus</button>
<button className="mode-btn flex-1 py-2.5 px-1 border-0 bg-transparent rounded-[10px] text-xs font-bold text-[rgba(255,255,255,0.5)] cursor-pointer [transition:all_0.2s] whitespace-nowrap" onClick={(event) => { setMode(event.currentTarget,'short') }}>Short Break</button>
<button className="mode-btn flex-1 py-2.5 px-1 border-0 bg-transparent rounded-[10px] text-xs font-bold text-[rgba(255,255,255,0.5)] cursor-pointer [transition:all_0.2s] whitespace-nowrap" onClick={(event) => { setMode(event.currentTarget,'long') }}>Long Break</button>
</div>
<div className="relative w-60 h-60 mt-0 mx-auto mb-7">
<svg className="ring" viewBox="0 0 240 240">
<circle className="fill-none stroke-[rgba(255,255,255,0.07)] [stroke-width:10]" cx="120" cy="120" r="108"/>
<circle className="fill-none stroke-[#c084fc] [stroke-width:10] [stroke-linecap:round] [stroke-dasharray:678.58] [stroke-dashoffset:0] [transition:stroke-dashoffset_1s_linear,_stroke_0.5s]" id="ring" cx="120" cy="120" r="108"/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<div className="time" id="time">25:00</div>
<div className="phase" id="phase">Time to focus</div>
</div>
</div>
<div className="flex items-center justify-center gap-4 mb-[26px]">
<button className="ctrl-btn cursor-pointer [transition:all_0.15s] reset" onClick={(event) => { reset() }} title="Reset">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2"><path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
</button>
<button className="ctrl-btn cursor-pointer [transition:all_0.15s] main" id="startBtn" onClick={(event) => { toggle() }}>Start</button>
<button className="ctrl-btn cursor-pointer [transition:all_0.15s] skip" onClick={(event) => { skip() }} title="Skip">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2"><polygon points="5 4 15 12 5 20 5 4"/><line x1="19" y1="5" x2="19" y2="19"/></svg>
</button>
</div>
<div className="grid grid-cols-3 gap-2.5 border-t border-t-[rgba(255,255,255,0.08)] pt-5">
<div className="flex flex-col items-center gap-[3px]"><span className="text-[22px] font-extrabold text-[#fff] [font-variant-numeric:tabular-nums]" id="doneCount">0</span><span className="text-[11px] text-[rgba(255,255,255,0.4)]">Sessions</span></div>
<div className="flex flex-col items-center gap-[3px]"><span className="text-[22px] font-extrabold text-[#fff] [font-variant-numeric:tabular-nums]" id="roundCount">1<span className="of">/4</span></span><span className="text-[11px] text-[rgba(255,255,255,0.4)]">Round</span></div>
<div className="flex flex-col items-center gap-[3px]"><span className="text-[22px] font-extrabold text-[#fff] [font-variant-numeric:tabular-nums]" id="focusMin">0</span><span className="text-[11px] text-[rgba(255,255,255,0.4)]">Focus min</span></div>
</div>
</div>
</div>
</>
);
}<template>
<div class="wrap">
<div class="card">
<div class="modes">
<button class="mode-btn active" @click="setMode($event.currentTarget,'focus')">Focus</button>
<button class="mode-btn" @click="setMode($event.currentTarget,'short')">Short Break</button>
<button class="mode-btn" @click="setMode($event.currentTarget,'long')">Long Break</button>
</div>
<div class="dial">
<svg class="ring" viewBox="0 0 240 240">
<circle class="ring-bg" cx="120" cy="120" r="108"/>
<circle class="ring-fg" id="ring" cx="120" cy="120" r="108"/>
</svg>
<div class="dial-center">
<div class="time" id="time">25:00</div>
<div class="phase" id="phase">Time to focus</div>
</div>
</div>
<div class="controls">
<button class="ctrl-btn reset" @click="reset()" title="Reset">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
</button>
<button class="ctrl-btn main" id="startBtn" @click="toggle()">Start</button>
<button class="ctrl-btn skip" @click="skip()" title="Skip">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><polygon points="5 4 15 12 5 20 5 4"/><line x1="19" y1="5" x2="19" y2="19"/></svg>
</button>
</div>
<div class="stats">
<div class="stat"><span class="stat-num" id="doneCount">0</span><span class="stat-label">Sessions</span></div>
<div class="stat"><span class="stat-num" id="roundCount">1<span class="of">/4</span></span><span class="stat-label">Round</span></div>
<div class="stat"><span class="stat-num" id="focusMin">0</span><span class="stat-label">Focus min</span></div>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
var DURATIONS = { focus: 25 * 60, short: 5 * 60, long: 15 * 60 };
var COLORS = { focus: '#c084fc', short: '#4ade80', long: '#60a5fa' };
var PHASES = { focus: 'Time to focus', short: 'Take a short break', long: 'Take a long break' };
var CIRC = 678.58;
var mode = 'focus';
var remaining = DURATIONS.focus;
var running = false;
var timer = null;
var sessions = 0;
var round = 1;
var focusMinutes = 0;
function render() {
var m = Math.floor(remaining / 60);
var s = remaining % 60;
document.getElementById('time').textContent = String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
var pct = remaining / DURATIONS[mode];
document.getElementById('ring').style.strokeDashoffset = CIRC * (1 - pct);
}
function tick() {
if (remaining > 0) {
remaining--;
if (mode === 'focus' && remaining % 60 === 0) { /* whole minute elapsed handled on complete */ }
render();
} else {
complete();
}
}
function complete() {
clearInterval(timer);
running = false;
if (mode === 'focus') {
sessions++;
focusMinutes += DURATIONS.focus / 60;
document.getElementById('doneCount').textContent = sessions;
document.getElementById('focusMin').textContent = focusMinutes;
var next = (sessions % 4 === 0) ? 'long' : 'short';
if (sessions % 4 === 0) { round++; }
switchMode(next);
} else {
switchMode('focus');
}
document.getElementById('roundCount').innerHTML = round + '<span class="of">/4</span>';
updateStartBtn();
}
function toggle() {
running = !running;
if (running) { timer = setInterval(tick, 1000); }
else { clearInterval(timer); }
updateStartBtn();
}
function updateStartBtn() {
var btn = document.getElementById('startBtn');
btn.textContent = running ? 'Pause' : 'Start';
btn.classList.toggle('running', running);
}
function reset() {
clearInterval(timer);
running = false;
remaining = DURATIONS[mode];
render();
updateStartBtn();
}
function skip() {
remaining = 0;
complete();
}
function switchMode(m) {
mode = m;
remaining = DURATIONS[m];
document.querySelectorAll('.mode-btn').forEach(function(b, i) {
b.classList.toggle('active', ['focus', 'short', 'long'][i] === m);
});
document.getElementById('phase').textContent = PHASES[m];
document.getElementById('ring').style.stroke = COLORS[m];
document.body.className = m;
render();
}
function setMode(btn, m) {
clearInterval(timer);
running = false;
switchMode(m);
updateStartBtn();
}
onMounted(() => {
document.body.classList.add('focus');
render();
});
</script>
<style scoped>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #1a1625; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; transition: background 0.6s; }
body.focus { background: #2d1b3d; }
body.short { background: #163d2e; }
body.long { background: #15324d; }
.wrap { width: 100%; max-width: 380px; }
.card { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 28px; padding: 26px; backdrop-filter: blur(20px); }
.modes { display: flex; gap: 4px; background: rgba(0,0,0,0.2); border-radius: 14px; padding: 4px; margin-bottom: 28px; }
.mode-btn { flex: 1; padding: 10px 4px; border: none; background: none; border-radius: 10px; font-size: 12px; font-weight: 700; color: rgba(255,255,255,0.5); cursor: pointer; transition: all 0.2s; white-space: nowrap; }
.mode-btn.active { background: rgba(255,255,255,0.12); color: #fff; }
.dial { position: relative; width: 240px; height: 240px; margin: 0 auto 28px; }
.ring { width: 100%; height: 100%; transform: rotate(-90deg); }
.ring-bg { fill: none; stroke: rgba(255,255,255,0.07); stroke-width: 10; }
.ring-fg { fill: none; stroke: #c084fc; stroke-width: 10; stroke-linecap: round; stroke-dasharray: 678.58; stroke-dashoffset: 0; transition: stroke-dashoffset 1s linear, stroke 0.5s; }
.dial-center { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.time { font-size: 56px; font-weight: 800; color: #fff; font-variant-numeric: tabular-nums; letter-spacing: -1px; }
.phase { font-size: 13px; color: rgba(255,255,255,0.5); margin-top: 4px; }
.controls { display: flex; align-items: center; justify-content: center; gap: 16px; margin-bottom: 26px; }
.ctrl-btn { cursor: pointer; transition: all 0.15s; }
.ctrl-btn.main { background: #fff; color: #2d1b3d; border: none; padding: 14px 44px; border-radius: 16px; font-size: 16px; font-weight: 800; letter-spacing: 0.5px; }
.ctrl-btn.main:hover { transform: scale(1.04); }
.ctrl-btn.main.running { background: rgba(255,255,255,0.15); color: #fff; }
.ctrl-btn.reset, .ctrl-btn.skip { width: 44px; height: 44px; border-radius: 12px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); color: rgba(255,255,255,0.6); display: flex; align-items: center; justify-content: center; }
.ctrl-btn.reset:hover, .ctrl-btn.skip:hover { background: rgba(255,255,255,0.12); color: #fff; }
.stats { display: grid; grid-template-columns: repeat(3,1fr); gap: 10px; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 20px; }
.stat { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.stat-num { font-size: 22px; font-weight: 800; color: #fff; font-variant-numeric: tabular-nums; }
.of { font-size: 13px; color: rgba(255,255,255,0.4); }
.stat-label { font-size: 11px; color: rgba(255,255,255,0.4); }
</style>// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-pomodoro-timer',
standalone: true,
imports: [CommonModule],
encapsulation: ViewEncapsulation.None,
template: `
<div class="wrap">
<div class="card">
<div class="modes">
<button class="mode-btn active" (click)="setMode($event.currentTarget,'focus')">Focus</button>
<button class="mode-btn" (click)="setMode($event.currentTarget,'short')">Short Break</button>
<button class="mode-btn" (click)="setMode($event.currentTarget,'long')">Long Break</button>
</div>
<div class="dial">
<svg class="ring" viewBox="0 0 240 240">
<circle class="ring-bg" cx="120" cy="120" r="108"/>
<circle class="ring-fg" id="ring" cx="120" cy="120" r="108"/>
</svg>
<div class="dial-center">
<div class="time" id="time">25:00</div>
<div class="phase" id="phase">Time to focus</div>
</div>
</div>
<div class="controls">
<button class="ctrl-btn reset" (click)="reset()" title="Reset">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
</button>
<button class="ctrl-btn main" id="startBtn" (click)="toggle()">Start</button>
<button class="ctrl-btn skip" (click)="skip()" title="Skip">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><polygon points="5 4 15 12 5 20 5 4"/><line x1="19" y1="5" x2="19" y2="19"/></svg>
</button>
</div>
<div class="stats">
<div class="stat"><span class="stat-num" id="doneCount">0</span><span class="stat-label">Sessions</span></div>
<div class="stat"><span class="stat-num" id="roundCount">1<span class="of">/4</span></span><span class="stat-label">Round</span></div>
<div class="stat"><span class="stat-num" id="focusMin">0</span><span class="stat-label">Focus min</span></div>
</div>
</div>
</div>
`,
styles: [`
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #1a1625; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; transition: background 0.6s; }
body.focus { background: #2d1b3d; }
body.short { background: #163d2e; }
body.long { background: #15324d; }
.wrap { width: 100%; max-width: 380px; }
.card { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 28px; padding: 26px; backdrop-filter: blur(20px); }
.modes { display: flex; gap: 4px; background: rgba(0,0,0,0.2); border-radius: 14px; padding: 4px; margin-bottom: 28px; }
.mode-btn { flex: 1; padding: 10px 4px; border: none; background: none; border-radius: 10px; font-size: 12px; font-weight: 700; color: rgba(255,255,255,0.5); cursor: pointer; transition: all 0.2s; white-space: nowrap; }
.mode-btn.active { background: rgba(255,255,255,0.12); color: #fff; }
.dial { position: relative; width: 240px; height: 240px; margin: 0 auto 28px; }
.ring { width: 100%; height: 100%; transform: rotate(-90deg); }
.ring-bg { fill: none; stroke: rgba(255,255,255,0.07); stroke-width: 10; }
.ring-fg { fill: none; stroke: #c084fc; stroke-width: 10; stroke-linecap: round; stroke-dasharray: 678.58; stroke-dashoffset: 0; transition: stroke-dashoffset 1s linear, stroke 0.5s; }
.dial-center { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.time { font-size: 56px; font-weight: 800; color: #fff; font-variant-numeric: tabular-nums; letter-spacing: -1px; }
.phase { font-size: 13px; color: rgba(255,255,255,0.5); margin-top: 4px; }
.controls { display: flex; align-items: center; justify-content: center; gap: 16px; margin-bottom: 26px; }
.ctrl-btn { cursor: pointer; transition: all 0.15s; }
.ctrl-btn.main { background: #fff; color: #2d1b3d; border: none; padding: 14px 44px; border-radius: 16px; font-size: 16px; font-weight: 800; letter-spacing: 0.5px; }
.ctrl-btn.main:hover { transform: scale(1.04); }
.ctrl-btn.main.running { background: rgba(255,255,255,0.15); color: #fff; }
.ctrl-btn.reset, .ctrl-btn.skip { width: 44px; height: 44px; border-radius: 12px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); color: rgba(255,255,255,0.6); display: flex; align-items: center; justify-content: center; }
.ctrl-btn.reset:hover, .ctrl-btn.skip:hover { background: rgba(255,255,255,0.12); color: #fff; }
.stats { display: grid; grid-template-columns: repeat(3,1fr); gap: 10px; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 20px; }
.stat { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.stat-num { font-size: 22px; font-weight: 800; color: #fff; font-variant-numeric: tabular-nums; }
.of { font-size: 13px; color: rgba(255,255,255,0.4); }
.stat-label { font-size: 11px; color: rgba(255,255,255,0.4); }
`]
})
export class PomodoroTimerComponent implements AfterViewInit {
ngAfterViewInit(): void {
var DURATIONS = { focus: 25 * 60, short: 5 * 60, long: 15 * 60 };
var COLORS = { focus: '#c084fc', short: '#4ade80', long: '#60a5fa' };
var PHASES = { focus: 'Time to focus', short: 'Take a short break', long: 'Take a long break' };
var CIRC = 678.58;
var mode = 'focus';
var remaining = DURATIONS.focus;
var running = false;
var timer = null;
var sessions = 0;
var round = 1;
var focusMinutes = 0;
document.body.classList.add('focus');
function render() {
var m = Math.floor(remaining / 60);
var s = remaining % 60;
document.getElementById('time').textContent = String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
var pct = remaining / DURATIONS[mode];
document.getElementById('ring').style.strokeDashoffset = CIRC * (1 - pct);
}
function tick() {
if (remaining > 0) {
remaining--;
if (mode === 'focus' && remaining % 60 === 0) { /* whole minute elapsed handled on complete */ }
render();
} else {
complete();
}
}
function complete() {
clearInterval(timer);
running = false;
if (mode === 'focus') {
sessions++;
focusMinutes += DURATIONS.focus / 60;
document.getElementById('doneCount').textContent = sessions;
document.getElementById('focusMin').textContent = focusMinutes;
var next = (sessions % 4 === 0) ? 'long' : 'short';
if (sessions % 4 === 0) { round++; }
switchMode(next);
} else {
switchMode('focus');
}
document.getElementById('roundCount').innerHTML = round + '<span class="of">/4</span>';
updateStartBtn();
}
function toggle() {
running = !running;
if (running) { timer = setInterval(tick, 1000); }
else { clearInterval(timer); }
updateStartBtn();
}
function updateStartBtn() {
var btn = document.getElementById('startBtn');
btn.textContent = running ? 'Pause' : 'Start';
btn.classList.toggle('running', running);
}
function reset() {
clearInterval(timer);
running = false;
remaining = DURATIONS[mode];
render();
updateStartBtn();
}
function skip() {
remaining = 0;
complete();
}
function switchMode(m) {
mode = m;
remaining = DURATIONS[m];
document.querySelectorAll('.mode-btn').forEach(function(b, i) {
b.classList.toggle('active', ['focus', 'short', 'long'][i] === m);
});
document.getElementById('phase').textContent = PHASES[m];
document.getElementById('ring').style.stroke = COLORS[m];
document.body.className = m;
render();
}
function setMode(btn, m) {
clearInterval(timer);
running = false;
switchMode(m);
updateStartBtn();
}
render();
// Bind inline-handler functions to the component so the template can call them
Object.assign(this, { render, tick, complete, toggle, updateStartBtn, reset, skip, switchMode, setMode });
}
}