{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"interval-timer","type":"registry:component","title":"Interval Timer","description":"Alternate timed work and rest rounds with pause and skip controls.","author":"Notion Arena","categories":["time"],"tags":["widget","free","interval","timer","workout","focus","rounds"],"meta":{"kind":"widget","tier":"free","version":"1.0.0","updatedAt":"2026-10-11","notionPlan":"Free","emoji":"⏲️","delivery":["embed","html-block","download"],"capabilities":{"interactive":true,"offlineSafe":true,"persistsLocally":false,"writesToNotion":false,"needsNetwork":false},"embedUrl":"https://notionarena.vercel.app/embed/interval-timer","fileUrl":"https://notionarena.vercel.app/widgets/interval-timer.html","downloadUrl":"https://notionarena.vercel.app/api/widget/interval-timer","height":280,"minHeight":220,"maxHeight":420,"params":[{"key":"label","label":"Session label","type":"text","default":"Interval timer"},{"key":"work","label":"Work seconds","type":"number","default":45,"min":1,"max":3600},{"key":"rest","label":"Rest seconds","type":"number","default":15,"min":0,"max":3600},{"key":"rounds","label":"Rounds","type":"number","default":6,"min":1,"max":50},{"key":"accent","label":"Accent","type":"color","default":"#d9730d"},{"key":"theme","label":"Theme","type":"select","default":"auto","options":[{"value":"auto","label":"Match Notion"},{"value":"light","label":"Light"},{"value":"dark","label":"Dark"}]}]},"files":[{"path":"widgets/interval-timer.html","type":"registry:file","target":"notion/widgets/interval-timer.html","content":"<!doctype html>\n<html lang=\"en\" data-theme=\"auto\">\n<head>\n<meta charset=\"utf-8\" /><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n<title>Interval Timer</title>\n<style>\n:root{--bg:#fff;--fg:#37352f;--muted:#787774;--surface:#f7f7f5;--border:#e9e9e7;--accent:#d9730d;--track:#e9e9e7;--font:ui-sans-serif,-apple-system,BlinkMacSystemFont,\"Segoe UI\",Helvetica,Arial,sans-serif}\nhtml[data-theme=\"dark\"]{--bg:#191919;--fg:#e6e6e4;--muted:#9b9a97;--surface:#252525;--border:#343434;--track:#343434}\n@media(prefers-color-scheme:dark){html[data-theme=\"auto\"]{--bg:#191919;--fg:#e6e6e4;--muted:#9b9a97;--surface:#252525;--border:#343434;--track:#343434}}\n*{box-sizing:border-box}html,body{margin:0;height:100%}body{background:var(--bg);color:var(--fg);font-family:ui-sans-serif,-apple-system,BlinkMacSystemFont,\"Segoe UI\",Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;overflow:hidden}\n.wrap{height:100%;min-height:220px;display:flex;flex-direction:column;gap:10px;padding:14px;overflow:auto}.head{display:flex;align-items:center;justify-content:space-between;gap:8px}.kicker{font-size:9px;text-transform:uppercase;letter-spacing:.1em;color:var(--muted);font-weight:650}.title{font-size:14px;font-weight:650;margin-top:2px}.round{font-size:10px;color:var(--muted);font-variant-numeric:tabular-nums}\n.main{display:flex;flex:1;flex-direction:column;align-items:center;justify-content:center;gap:5px}.phase{font-size:12px;text-transform:uppercase;letter-spacing:.1em;color:var(--accent);font-weight:700}.time{font-size:clamp(42px,12vw,64px);font-weight:650;letter-spacing:-.06em;font-variant-numeric:tabular-nums;line-height:1}.progress{height:6px;border-radius:99px;background:var(--track);overflow:hidden}.progress i{display:block;width:0;height:100%;background:var(--accent);border-radius:inherit;transition:width .15s linear}.status{min-height:14px;font-size:10px;color:var(--muted);text-align:center}.controls{display:flex;justify-content:center;gap:6px;flex-wrap:wrap}\nbutton{min-height:30px;border:1px solid var(--border);border-radius:7px;background:var(--surface);color:var(--fg);font:inherit;font-size:11px;font-weight:550;padding:5px 10px;cursor:pointer}button:hover{border-color:color-mix(in oklab,var(--accent) 45%,var(--border))}button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}button.primary{border-color:var(--accent);background:var(--accent);color:#fff}button:disabled{opacity:.45;cursor:not-allowed}\n@media(max-width:330px){.wrap{padding:10px}.time{font-size:42px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.01ms!important;transition-duration:.01ms!important;scroll-behavior:auto!important}}\n</style>\n</head>\n<body><main class=\"wrap\">\n  <header class=\"head\"><div><div class=\"kicker\">Work and rest</div><div class=\"title\" id=\"title\">Interval timer</div></div><div class=\"round\" id=\"round\">Round 1 of 6</div></header>\n  <section class=\"main\"><div class=\"phase\" id=\"phase\">Work</div><div class=\"time\" id=\"time\" role=\"timer\" aria-live=\"off\">00:45</div></section>\n  <div class=\"progress\" id=\"progress\" role=\"progressbar\" aria-label=\"Interval session progress\" aria-valuemin=\"0\" aria-valuemax=\"100\" aria-valuenow=\"0\"><i id=\"fill\"></i></div>\n  <div class=\"status\" id=\"status\" aria-live=\"polite\">No sound. Follow the on-screen phases.</div>\n  <div class=\"controls\"><button class=\"primary\" id=\"start\" type=\"button\">Start</button><button id=\"skip\" type=\"button\" disabled>Skip phase</button><button id=\"reset\" type=\"button\">Reset</button></div>\n</main>\n<script>\n(function(){\n  var cfgEl=document.getElementById('bk-config'),base={};try{base=cfgEl?JSON.parse(cfgEl.textContent||'{}'):{};}catch(e){}\n  var q=new URLSearchParams(location.search),c={};for(var k in base)c[k]=base[k];q.forEach(function(v,k){c[k]=v;});\n  function num(k,d){var n=parseInt(c[k],10);return isFinite(n)?n:d;}\n  function safeColor(v,d){return /^#[0-9a-f]{6}$/i.test(String(v||''))?String(v):d;}\n  var theme=['auto','light','dark'].indexOf(String(c.theme||'auto'))>-1?String(c.theme||'auto'):'auto',ACCENT=safeColor(c.accent,'#d9730d'),LABEL=String(c.label||'Interval timer').slice(0,48),WORK=Math.max(1,Math.min(3600,num('work',45))),REST=Math.max(0,Math.min(3600,num('rest',15))),ROUNDS=Math.max(1,Math.min(50,num('rounds',6)));\n  document.documentElement.setAttribute('data-theme',theme);document.documentElement.style.setProperty('--accent',ACCENT);document.getElementById('title').textContent=LABEL;document.title=LABEL;\n  var round=1,phase='work',running=false,paused=false,complete=false,remaining=WORK*1000,phaseEnd=0,timer=null;\n  var timeEl=document.getElementById('time'),phaseEl=document.getElementById('phase'),roundEl=document.getElementById('round'),startBtn=document.getElementById('start'),skipBtn=document.getElementById('skip'),fill=document.getElementById('fill'),progress=document.getElementById('progress'),status=document.getElementById('status'),totalMs=ROUNDS*(WORK+REST)*1000;\n  function pad(n){return String(n).padStart(2,'0');}\n  function currentDuration(){return(phase==='work'?WORK:REST)*1000;}\n  function stop(){if(timer){clearInterval(timer);timer=null;}}\n  function finish(){running=false;paused=false;complete=true;remaining=0;stop();phaseEl.textContent='Complete';timeEl.textContent='00:00';roundEl.textContent='Session complete';fill.style.width='100%';progress.setAttribute('aria-valuenow','100');startBtn.textContent='Start again';skipBtn.disabled=true;status.textContent='Interval session complete.';}\n  function advance(at){\n    if(phase==='work'&&REST>0)phase='rest';\n    else{round++;phase='work';if(round>ROUNDS){finish();return;}}\n    remaining=currentDuration();phaseEnd=at+remaining;\n  }\n  function elapsed(now){var done=(round-1)*(WORK+REST)*1000+(phase==='work'?WORK*1000-remaining:WORK*1000+REST*1000-remaining);if(running)done=(round-1)*(WORK+REST)*1000+(phase==='work'?WORK*1000-(phaseEnd-now):WORK*1000+REST*1000-(phaseEnd-now));return Math.max(0,Math.min(totalMs,done));}\n  function render(now){if(now===undefined)now=Date.now();var ms=running?Math.max(0,phaseEnd-now):remaining,sec=Math.ceil(ms/1000);timeEl.textContent=pad(Math.floor(sec/60))+':'+pad(sec%60);if(!complete){phaseEl.textContent=phase==='work'?'Work':'Rest';roundEl.textContent='Round '+round+' of '+ROUNDS;var pct=totalMs?Math.round(elapsed(now)/totalMs*100):0;fill.style.width=pct+'%';progress.setAttribute('aria-valuenow',String(pct));}startBtn.textContent=complete?'Start again':(running?'Pause':(paused?'Resume':'Start'));skipBtn.disabled=complete||!running;status.textContent=complete?'Interval session complete.':(running?(phase==='work'?'Work interval.':'Rest interval.'):(paused?'Paused. Resume when ready.':'No sound. Follow the on-screen phases.'));}\n  function tick(){if(!running)return;var now=Date.now(),guard=0;while(running&&now>=phaseEnd&&guard<100){var end=phaseEnd;advance(end);guard++;}if(running){remaining=Math.max(0,phaseEnd-now);render(now);}}\n  startBtn.addEventListener('click',function(){if(complete){reset();}if(running){remaining=Math.max(0,phaseEnd-Date.now());running=false;paused=true;stop();render();return;}running=true;paused=false;phaseEnd=Date.now()+remaining;timer=setInterval(tick,100);tick();});\n  skipBtn.addEventListener('click',function(){if(!running)return;advance(Date.now());render();});\n  function reset(){running=false;paused=false;complete=false;round=1;phase='work';remaining=WORK*1000;phaseEnd=0;stop();render();}\n  document.getElementById('reset').addEventListener('click',reset);render();\n})();\n</script>\n</body>\n</html>\n"}]}