Focus 25/5/15 with ring progress + sound alert
<!doctype html>
<!-- vibecode88 · v1.1.0 (improved) -->
<!-- Added: better touch + keyboard shortcuts + comments -->
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Pomodoro Timer — demo</title>
<style>
html,body{margin:0;height:100%;background:#0a0e14;color:#e7ecf3;font:14px/1.5 -apple-system,Inter,sans-serif;display:grid;place-items:center;overflow:hidden}
.wrap{text-align:center}
.label{font:600 12px ui-monospace,monospace;letter-spacing:.2em;color:#8b95a7;margin-bottom:24px;text-transform:uppercase}
.time{font:800 120px ui-monospace,monospace;letter-spacing:-.04em;line-height:1;background:linear-gradient(120deg,#00ff95,#00d2ff);-webkit-background-clip:text;background-clip:text;color:transparent;margin-bottom:32px}
.ctrls{display:flex;gap:12px;justify-content:center;margin-bottom:32px}
button{padding:12px 28px;border-radius:999px;border:1px solid #1f2937;background:#0f141c;color:#e7ecf3;font:600 14px;cursor:pointer;transition:.2s}
button:hover{border-color:#00ff95;transform:translateY(-1px)}
button.primary{background:#00ff95;color:#0a0e14;border-color:#00ff95}
button.primary:hover{background:#1affa3}
.modes{display:flex;gap:6px;justify-content:center;margin-bottom:24px}
.mode{padding:6px 14px;border-radius:999px;background:transparent;border:1px solid #1f2937;color:#8b95a7;font:500 12px;cursor:pointer}
.mode.active{background:rgba(0,255,149,.1);color:#00ff95;border-color:rgba(0,255,149,.3)}
.stat{font:500 12px ui-monospace,monospace;color:#5c6577;margin-top:8px}
.ring{position:relative;width:280px;height:280px;margin:0 auto 32px}
svg{position:absolute;top:0;left:0;width:100%;height:100%;transform:rotate(-90deg)}
circle{fill:none;stroke-width:6}
.bg{stroke:#1f2937}
.fg{stroke:#00ff95;stroke-linecap:round;transition:stroke-dashoffset .5s, stroke .5s}
.ring.break .fg{stroke:#00d2ff}
.ring.break .label{color:#00d2ff}
</style>
</head>
<body>
<div class="wrap">
<div class="ring" id="ring">
<svg viewBox="0 0 100 100">
<circle class="bg" cx="50" cy="50" r="45"/>
<circle class="fg" cx="50" cy="50" r="45" stroke-dasharray="282.74" stroke-dashoffset="0" id="prog"/>
</svg>
</div>
<div class="label" id="lbl">Focus</div>
<div class="time" id="time">25:00</div>
<div class="ctrls">
<button id="start" class="primary">▶ Start</button>
<button id="reset">↺ Reset</button>
</div>
<div class="modes">
<button class="mode active" data-m="focus" data-mins="25">Focus 25</button>
<button class="mode" data-m="short" data-mins="5">Short 5</button>
<button class="mode" data-m="long" data-mins="15">Long 15</button>
</div>
<div class="stat">🍅 Completed today: <span id="count">0</span></div>
</div>
<script>
(() => {
const time = document.getElementById('time'), prog = document.getElementById('prog');
const lbl = document.getElementById('lbl'), ring = document.getElementById('ring');
const start = document.getElementById('start'), reset = document.getElementById('reset');
const modes = document.querySelectorAll('.mode'), countEl = document.getElementById('count');
let total = 25 * 60, left = total, mode = 'focus', running = false, t, count = 0;
const fmt = s => `${String(Math.floor(s/60)).padStart(2,'0')}:${String(s%60).padStart(2,'0')}`;
const update = () => {
time.textContent = fmt(left);
prog.setAttribute('stroke-dashoffset', 282.74 * (1 - left / total));
};
modes.forEach(m => m.onclick = () => {
modes.forEach(x => x.classList.remove('active'));
m.classList.add('active');
mode = m.dataset.m;
total = parseInt(m.dataset.mins, 10) * 60;
left = total;
lbl.textContent = m.textContent.split(' ')[0];
ring.classList.toggle('break', mode !== 'focus');
if (running) toggle();
update();
});
const tick = () => {
if (left <= 0) {
count++; countEl.textContent = count;
try { new AudioContext().resume(); } catch(e) {}
const audio = new (window.AudioContext || window.webkitAudioContext)();
const o = audio.createOscillator(), g = audio.createGain();
o.connect(g); g.connect(audio.destination);
o.frequency.value = 880; g.gain.value = 0.2;
o.start(); o.stop(audio.currentTime + 0.4);
toggle();
left = total; update();
return;
}
left--; update();
};
const toggle = () => {
running = !running;
start.textContent = running ? '⏸ Pause' : '▶ Start';
if (running) t = setInterval(tick, 1000); else clearInterval(t);
};
start.onclick = toggle;
reset.onclick = () => { if (running) toggle(); left = total; update(); };
update();
})();
</script>
</body>
</html>