{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"breathing-timer","type":"registry:component","title":"Breathing Timer","description":"A gentle visual pace for box, 4 7 8, and equal breathing.","author":"Notion Arena","categories":["time"],"tags":["widget","free","breathing","wellbeing","mindfulness","timer","focus"],"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/breathing-timer","fileUrl":"https://notionarena.vercel.app/widgets/breathing-timer.html","downloadUrl":"https://notionarena.vercel.app/api/widget/breathing-timer","height":280,"minHeight":210,"maxHeight":420,"params":[{"key":"label","label":"Session label","type":"text","default":"Breathing break"},{"key":"pattern","label":"Breathing pattern","type":"select","default":"box","options":[{"value":"box","label":"Box: 4 4 4 4"},{"value":"478","label":"4 7 8"},{"value":"equal","label":"Equal: 4 4"}]},{"key":"rounds","label":"Rounds","type":"number","default":4,"min":1,"max":12},{"key":"accent","label":"Accent","type":"color","default":"#6940a5"},{"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/breathing-timer.html","type":"registry:file","target":"notion/widgets/breathing-timer.html","content":"<!doctype html>\n<html lang=\"en\" data-theme=\"auto\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n<title>Breathing Break</title>\n<style>\n:root{--bg:#ffffff;--fg:#37352f;--muted:#787774;--surface:#f7f7f5;--border:#e9e9e7;--accent:#6940a5;--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%}\nbody{background:var(--bg);color:var(--fg);font-family:var(--font);-webkit-font-smoothing:antialiased;overflow:hidden}\n.wrap{height:100%;min-height:210px;display:flex;flex-direction:column;gap:10px;padding:14px;overflow:auto}\n.head{display:flex;align-items:center;justify-content:space-between;gap:12px}\n.kicker{font-size:9.5px;text-transform:uppercase;letter-spacing:.09em;color:var(--muted);font-weight:650}\n.title{margin-top:2px;font-size:14px;font-weight:650;letter-spacing:-.02em}\n.pattern{font-size:11px;color:var(--muted);text-align:right}\n.main{display:flex;align-items:center;justify-content:center;gap:22px;flex:1;min-height:0;flex-wrap:wrap}\n.orb-wrap{position:relative;width:132px;height:132px;flex:0 0 auto;display:grid;place-items:center}\n.orb-ring{position:absolute;inset:0;width:100%;height:100%;transform:rotate(-90deg)}\n.orb-ring circle{fill:none;stroke-width:2}\n.orb-ring .track{stroke:var(--track)}\n.orb-ring .progress{stroke:var(--accent);stroke-linecap:round;stroke-dasharray:301.59;stroke-dashoffset:301.59;transition:stroke-dashoffset .25s linear}\n.orb{position:relative;width:82px;height:82px;border-radius:50%;background:color-mix(in oklab,var(--accent) 17%,var(--surface));box-shadow:inset 0 0 0 1px color-mix(in oklab,var(--accent) 24%,transparent);transform:scale(.78);transition-property:transform;transition-timing-function:ease-in-out}\n.center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;pointer-events:none}\n.phase{font-size:13px;font-weight:650;letter-spacing:-.01em}\n.seconds{font-size:28px;font-weight:650;letter-spacing:-.04em;line-height:1;font-variant-numeric:tabular-nums}\n.details{display:flex;flex:1 1 170px;min-width:155px;max-width:300px;flex-direction:column;gap:8px}\n.instruction{min-height:34px;font-size:12px;line-height:1.4;color:var(--muted)}\n.round{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:11px;color:var(--muted);font-variant-numeric:tabular-nums}\n.overall{height:5px;width:100%;overflow:hidden;border-radius:99px;background:var(--track)}\n.overall i{display:block;height:100%;width:0;border-radius:inherit;background:var(--accent);transition:width .2s linear}\n.status{min-height:15px;font-size:11px;color:var(--muted)}\n.controls{display:flex;align-items:center;gap:6px;flex-wrap:wrap}\nbutton{min-height:30px;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:var(--fg);font:inherit;font-size:11.5px;font-weight:550;padding:5px 10px;cursor:pointer;transition:border-color .15s,background .15s,opacity .15s}\nbutton:hover{border-color:color-mix(in oklab,var(--accent) 45%,var(--border))}\nbutton:focus-visible{outline:2px solid var(--accent);outline-offset:2px}\nbutton.primary{border-color:var(--accent);background:var(--accent);color:#fff}\nbutton.primary:hover{filter:brightness(.94)}\n@media(max-width:390px){.wrap{padding:11px}.main{gap:12px}.orb-wrap{width:108px;height:108px}.orb{width:70px;height:70px}.details{min-width:140px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}}\n</style>\n</head>\n<body>\n<div class=\"wrap\">\n  <header class=\"head\">\n    <div><div class=\"kicker\">A small pause</div><div class=\"title\" id=\"title\">Breathing break</div></div>\n    <div class=\"pattern\" id=\"pattern\">Box breathing</div>\n  </header>\n  <main class=\"main\">\n    <div class=\"orb-wrap\" role=\"img\" aria-label=\"Breathing pace animation\">\n      <svg class=\"orb-ring\" viewBox=\"0 0 100 100\" aria-hidden=\"true\">\n        <circle class=\"track\" cx=\"50\" cy=\"50\" r=\"48\"></circle>\n        <circle class=\"progress\" id=\"phaseProgress\" cx=\"50\" cy=\"50\" r=\"48\"></circle>\n      </svg>\n      <div class=\"orb\" id=\"orb\"></div>\n      <div class=\"center\"><span class=\"phase\" id=\"phase\">Ready</span><span class=\"seconds\" id=\"seconds\">4</span></div>\n    </div>\n    <section class=\"details\" aria-label=\"Breathing session controls\">\n      <div class=\"instruction\" id=\"instruction\" aria-live=\"polite\">Press start when you are ready.</div>\n      <div class=\"round\"><span id=\"round\">Round 1 of 4</span><span id=\"elapsed\">0% complete</span></div>\n      <div class=\"overall\" id=\"overall\" role=\"progressbar\" aria-label=\"Session progress\" aria-valuemin=\"0\" aria-valuemax=\"100\" aria-valuenow=\"0\"><i id=\"fill\"></i></div>\n      <div class=\"status\" id=\"status\">Follow the circle at a comfortable pace.</div>\n      <div class=\"controls\">\n        <button class=\"primary\" id=\"start\" type=\"button\">Start</button>\n        <button id=\"reset\" type=\"button\">Reset</button>\n      </div>\n    </section>\n  </main>\n</div>\n<script>\n(function(){\n  var cfgEl=document.getElementById('bk-config'),base={};\n  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];\n  q.forEach(function(v,k){c[k]=v;});\n  function num(k,d){var v=parseFloat(c[k]);return isFinite(v)?v: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';\n  var ACCENT=safeColor(c.accent,'#6940a5');\n  var LABEL=String(c.label||'Breathing break').slice(0,50);\n  var ROUNDS=Math.max(1,Math.min(12,Math.round(num('rounds',4))));\n  var PATTERNS={\n    box:{name:'Box breathing',steps:[{name:'Inhale',seconds:4},{name:'Hold',seconds:4},{name:'Exhale',seconds:4},{name:'Hold',seconds:4}]},\n    '478':{name:'4 7 8 breathing',steps:[{name:'Inhale',seconds:4},{name:'Hold',seconds:7},{name:'Exhale',seconds:8}]},\n    equal:{name:'Equal breathing',steps:[{name:'Inhale',seconds:4},{name:'Exhale',seconds:4}]}\n  };\n  var selected=PATTERNS[String(c.pattern||'box')]||PATTERNS.box;\n  var steps=selected.steps;\n  if(theme)document.documentElement.setAttribute('data-theme',theme);\n  document.documentElement.style.setProperty('--accent',ACCENT);\n  document.getElementById('title').textContent=LABEL;\n  document.getElementById('pattern').textContent=selected.name;\n  document.title=LABEL;\n\n  var phaseEl=document.getElementById('phase'),secondsEl=document.getElementById('seconds');\n  var instructionEl=document.getElementById('instruction'),roundEl=document.getElementById('round');\n  var elapsedEl=document.getElementById('elapsed'),statusEl=document.getElementById('status');\n  var orb=document.getElementById('orb'),phaseProgress=document.getElementById('phaseProgress');\n  var fill=document.getElementById('fill'),overall=document.getElementById('overall'),startButton=document.getElementById('start');\n  var roundNumber=1,stepIndex=0,running=false,paused=false,complete=false,phaseEnd=0,remainingMs=steps[0].seconds*1000;\n  var timer=null,phaseStart=0,CIRC=2*Math.PI*48;\n  var reduceMotion=!!(window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches);\n  var instruction={Inhale:'Breathe in gently.',Hold:'Rest with the breath held softly.',Exhale:'Let the breath out slowly.'};\n\n  function step(){return steps[stepIndex];}\n  function freezeOrb(){\n    if(reduceMotion)return;\n    var current=getComputedStyle(orb).transform;\n    orb.style.transitionDuration='0ms';\n    if(current&&current!=='none')orb.style.transform=current;\n  }\n  function applyBreathTarget(seconds,animate){\n    if(reduceMotion){orb.style.transitionDuration='0ms';orb.style.transform='scale(1)';return;}\n    var target=step().name==='Inhale'?'scale(1.14)':(step().name==='Exhale'?'scale(.78)':null);\n    if(!target)return;\n    if(animate){\n      var current=getComputedStyle(orb).transform;\n      orb.style.transitionDuration='0ms';orb.style.transform=current==='none'?'scale(.78)':current;\n      requestAnimationFrame(function(){orb.style.transitionDuration=Math.max(0,seconds)+'ms';orb.style.transform=target;});\n    }else{\n      orb.style.transitionDuration='0ms';orb.style.transform=target;\n    }\n  }\n  function setPhase(){\n    var s=step();phaseStart=phaseEnd-s.seconds*1000;\n    phaseEl.textContent=s.name;secondsEl.textContent=String(s.seconds);\n    instructionEl.textContent=instruction[s.name]||'Breathe at your own pace.';\n    roundEl.textContent='Round '+roundNumber+' of '+ROUNDS;\n    applyBreathTarget(s.seconds*1000,true);\n  }\n  function percentDone(now){\n    var total=steps.length*ROUNDS;\n    var phaseNumber=(roundNumber-1)*steps.length+stepIndex;\n    var duration=step().seconds*1000;\n    var fraction=duration?Math.max(0,Math.min(1,(now-phaseStart)/duration)):0;\n    return complete?100:Math.max(0,Math.min(100,((phaseNumber+fraction)/total)*100));\n  }\n  function render(now){\n    if(now===undefined)now=Date.now();\n    var ms=running?Math.max(0,phaseEnd-now):remainingMs;\n    var whole=Math.ceil(ms/1000);\n    secondsEl.textContent=String(Math.max(0,whole));\n    var pct=percentDone(now);\n    var ringOffset=CIRC*(1-Math.max(0,Math.min(1,(step().seconds*1000-ms)/(step().seconds*1000))));\n    phaseProgress.setAttribute('stroke-dashoffset',String(complete?0:ringOffset));\n    fill.style.width=pct.toFixed(1)+'%';\n    overall.setAttribute('aria-valuenow',String(Math.round(pct)));\n    elapsedEl.textContent=Math.round(pct)+'% complete';\n    roundEl.textContent=complete?'Session complete':'Round '+roundNumber+' of '+ROUNDS;\n    startButton.textContent=complete?'Start again':(running?'Pause':(paused?'Resume':'Start'));\n    statusEl.textContent=complete?'You finished '+ROUNDS+' calm rounds. Take a moment.':(running?'Stay with this breath.':(paused?'Paused. Resume when ready.':'Follow the circle at a comfortable pace.'));\n  }\n  function stopTimer(){if(timer){clearInterval(timer);timer=null;}}\n  function finish(){\n    running=false;paused=false;complete=true;remainingMs=0;stopTimer();\n    phaseEl.textContent='Complete';secondsEl.textContent='✓';\n    instructionEl.textContent='Session complete.';roundEl.textContent='Session complete';\n    statusEl.textContent='You finished '+ROUNDS+' calm rounds. Take a moment.';\n    fill.style.width='100%';overall.setAttribute('aria-valuenow','100');elapsedEl.textContent='100% complete';\n    phaseProgress.setAttribute('stroke-dashoffset','0');startButton.textContent='Start again';\n    orb.style.transitionDuration='0ms';orb.style.transform='scale(.9)';\n  }\n  function advance(){\n    stepIndex++;\n    if(stepIndex>=steps.length){stepIndex=0;roundNumber++;}\n    if(roundNumber>ROUNDS){finish();return;}\n    phaseEnd+=step().seconds*1000;\n    setPhase();\n  }\n  function tick(){\n    if(!running)return;\n    var now=Date.now(),guard=0;\n    while(running&&now>=phaseEnd&&guard<100){advance();guard++;}\n    if(running){remainingMs=Math.max(0,phaseEnd-now);render(now);}\n  }\n  function start(){\n    if(complete){reset();}\n    if(running){remainingMs=Math.max(0,phaseEnd-Date.now());running=false;paused=true;stopTimer();freezeOrb();render();return;}\n    running=true;paused=false;phaseEnd=Date.now()+Math.max(0,remainingMs);phaseStart=phaseEnd-step().seconds*1000;\n    applyBreathTarget(remainingMs,true);startButton.textContent='Pause';statusEl.textContent='Stay with this breath.';\n    timer=setInterval(tick,100);tick();\n  }\n  function reset(){\n    running=false;paused=false;complete=false;stopTimer();roundNumber=1;stepIndex=0;\n    remainingMs=steps[0].seconds*1000;phaseEnd=0;phaseStart=0;\n    phaseEl.textContent='Ready';secondsEl.textContent=String(steps[0].seconds);\n    instructionEl.textContent='Press start when you are ready.';\n    statusEl.textContent='Follow the circle at a comfortable pace.';\n    roundEl.textContent='Round 1 of '+ROUNDS;elapsedEl.textContent='0% complete';\n    fill.style.width='0%';overall.setAttribute('aria-valuenow','0');\n    phaseProgress.setAttribute('stroke-dashoffset',String(CIRC));\n    orb.style.transitionDuration='0ms';orb.style.transform='scale(.78)';\n    startButton.textContent='Start';\n  }\n  startButton.addEventListener('click',start);\n  document.getElementById('reset').addEventListener('click',reset);\n  reset();\n})();\n</script>\n</body>\n</html>\n"}]}