{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"water-tracker","type":"registry:component","title":"Water Intake Tracker","description":"A one-tap daily drink counter with a goal ring and undo.","author":"Notion Arena","categories":["trackers"],"tags":["widget","free","water","hydration","daily","counter","wellbeing"],"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":true,"writesToNotion":false,"needsNetwork":false},"embedUrl":"https://notionarena.vercel.app/embed/water-tracker","fileUrl":"https://notionarena.vercel.app/widgets/water-tracker.html","downloadUrl":"https://notionarena.vercel.app/api/widget/water-tracker","height":240,"minHeight":190,"maxHeight":380,"params":[{"key":"label","label":"Widget title","type":"text","default":"Water today"},{"key":"goal","label":"Daily goal","type":"number","default":8,"min":1,"max":50},{"key":"unit","label":"Drink unit","type":"text","default":"glasses"},{"key":"accent","label":"Accent","type":"color","default":"#0f7b6c"},{"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/water-tracker.html","type":"registry:file","target":"notion/widgets/water-tracker.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>Water Tracker</title>\n<style>\n:root{--bg:#ffffff;--fg:#37352f;--muted:#787774;--surface:#f7f7f5;--border:#e9e9e7;--accent:#0f7b6c;--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:#2f2f2f;--track:#343434}\n@media (prefers-color-scheme:dark){html[data-theme=\"auto\"]{--bg:#191919;--fg:#e6e6e4;--muted:#9b9a97;--surface:#252525;--border:#2f2f2f;--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:190px;display:flex;flex-direction:column;gap:12px;padding:14px;overflow:auto}\n.head{display:flex;align-items:center;justify-content:space-between;gap:10px}\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:15px;font-weight:650;letter-spacing:-.02em}\n.date{font-size:11px;color:var(--muted);white-space:nowrap}\n.main{display:flex;align-items:center;justify-content:center;gap:18px;flex:1;min-height:0;flex-wrap:wrap}\n.ring{position:relative;width:116px;height:116px;flex:0 0 auto}\n.ring svg{width:100%;height:100%;transform:rotate(-90deg)}\n.ring circle{fill:none;stroke-width:9}\n.ring .track{stroke:var(--track)}\n.ring .value{stroke:var(--accent);stroke-linecap:round;transition:stroke-dashoffset .45s cubic-bezier(.16,1,.3,1)}\n.center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px}\n.count{font-size:31px;font-weight:650;letter-spacing:-.04em;line-height:1;font-variant-numeric:tabular-nums}\n.unit{font-size:10.5px;color:var(--muted);max-width:82px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.details{display:flex;flex:1 1 170px;min-width:155px;max-width:310px;flex-direction:column;gap:8px}\n.goalrow{display:flex;align-items:baseline;justify-content:space-between;gap:8px}\n.goal{font-size:12.5px;font-weight:600;font-variant-numeric:tabular-nums}\n.percent{font-family:var(--font);font-size:11px;color:var(--muted);font-variant-numeric:tabular-nums}\n.bar{height:6px;width:100%;overflow:hidden;border-radius:99px;background:var(--track)}\n.bar i{display:block;height:100%;width:0;border-radius:inherit;background:var(--accent);transition:width .35s ease}\n.status{min-height:16px;font-size:11.5px;color:var(--muted)}\n.status.done{color:var(--accent);font-weight:600}\n.actions{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 9px;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:disabled{opacity:.45;cursor:not-allowed}\nbutton.primary{border-color:var(--accent);background:var(--accent);color:#fff}\nbutton.primary:hover{filter:brightness(.94)}\n.reset{margin-left:auto;color:var(--muted)}\n@media(max-width:380px){.wrap{padding:11px}.main{gap:12px}.ring{width:94px;height:94px}.count{font-size:26px}.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\">Daily check-in</div><div class=\"title\" id=\"title\">Water today</div></div>\n    <div class=\"date\" id=\"date\"></div>\n  </header>\n  <main class=\"main\">\n    <div class=\"ring\" role=\"img\" aria-label=\"Water goal progress\">\n      <svg viewBox=\"0 0 120 120\" aria-hidden=\"true\">\n        <circle class=\"track\" cx=\"60\" cy=\"60\" r=\"51\"></circle>\n        <circle class=\"value\" id=\"ring\" cx=\"60\" cy=\"60\" r=\"51\" stroke-dasharray=\"320.44\" stroke-dashoffset=\"320.44\"></circle>\n      </svg>\n      <div class=\"center\"><span class=\"count\" id=\"count\">0</span><span class=\"unit\" id=\"unit\">glasses</span></div>\n    </div>\n    <section class=\"details\" aria-label=\"Daily water goal\">\n      <div class=\"goalrow\"><span class=\"goal\" id=\"goal\">0 of 8 glasses</span><span class=\"percent\" id=\"percent\">0%</span></div>\n      <div class=\"bar\" id=\"bar\" role=\"progressbar\" aria-label=\"Water intake goal\" aria-valuemin=\"0\" aria-valuemax=\"8\" aria-valuenow=\"0\"><i id=\"fill\"></i></div>\n      <div class=\"status\" id=\"status\" aria-live=\"polite\">Log a drink to get started.</div>\n      <div class=\"actions\">\n        <button class=\"primary\" id=\"add\" type=\"button\" aria-label=\"Log one glass\">+ Log one</button>\n        <button id=\"undo\" type=\"button\" aria-label=\"Undo one drink\">Undo</button>\n        <button class=\"reset\" 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,'#0f7b6c');\n  var LABEL=String(c.label||'Water today').slice(0,60);\n  var UNIT=String(c.unit||'glasses').slice(0,18);\n  var GOAL=Math.max(1,Math.min(50,Math.round(num('goal',8))));\n  if(theme)document.documentElement.setAttribute('data-theme',theme);\n  document.documentElement.style.setProperty('--accent',ACCENT);\n\n  var KEY='notion-arena.water.v1',count=0,todayKey='';\n  function dateKey(){var d=new Date();return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0');}\n  function load(){\n    var saved={};\n    try{saved=JSON.parse(localStorage.getItem(KEY)||'{}')||{};}catch(e){saved={};}\n    todayKey=dateKey();\n    count=saved.date===todayKey&&isFinite(Number(saved.count))?Math.max(0,Math.min(999,Math.floor(Number(saved.count)))):0;\n    save();\n  }\n  function save(){try{localStorage.setItem(KEY,JSON.stringify({date:todayKey||dateKey(),count:count}));}catch(e){}}\n\n  var title=document.getElementById('title'),date=document.getElementById('date'),unit=document.getElementById('unit');\n  var goal=document.getElementById('goal'),percent=document.getElementById('percent'),bar=document.getElementById('bar');\n  var fill=document.getElementById('fill'),ring=document.getElementById('ring'),status=document.getElementById('status');\n  var countEl=document.getElementById('count'),undo=document.getElementById('undo');\n  var resetButton=document.getElementById('reset'),resetArmed=false,resetTimer=null;\n  var CIRC=2*Math.PI*51;\n  title.textContent=LABEL;unit.textContent=UNIT;\n  document.getElementById('add').setAttribute('aria-label','Log one '+UNIT);\n  date.textContent=new Date().toLocaleDateString(undefined,{weekday:'short',month:'short',day:'numeric'});\n\n  function disarmReset(){\n    resetArmed=false;\n    if(resetTimer){clearTimeout(resetTimer);resetTimer=null;}\n    resetButton.textContent='Reset';\n    resetButton.setAttribute('aria-label',\"Reset today's water count\");\n  }\n  function syncToday(){\n    var current=dateKey();\n    if(current!==todayKey){todayKey=current;count=0;save();disarmReset();date.textContent=new Date().toLocaleDateString(undefined,{weekday:'short',month:'short',day:'numeric'});}\n  }\n  function render(){\n    syncToday();\n    var ratio=count/GOAL,pct=Math.round(ratio*100),visual=Math.min(1,ratio);\n    countEl.textContent=String(count);\n    goal.textContent=count+' of '+GOAL+' '+UNIT;\n    percent.textContent=pct+'%';\n    bar.setAttribute('aria-valuemax',String(GOAL));bar.setAttribute('aria-valuenow',String(Math.min(count,GOAL)));\n    fill.style.width=(visual*100).toFixed(1)+'%';\n    ring.setAttribute('stroke-dashoffset',String(CIRC*(1-visual)));\n    status.className='status'+(count>=GOAL?' done':'');\n    status.textContent=count>=GOAL?'Daily goal reached. Nice work.':(GOAL-count)+' '+UNIT+' to go.';\n    undo.disabled=count===0;resetButton.disabled=count===0;\n  }\n  document.getElementById('add').addEventListener('click',function(){syncToday();disarmReset();count=Math.min(999,count+1);save();render();});\n  undo.addEventListener('click',function(){syncToday();disarmReset();count=Math.max(0,count-1);save();render();});\n  resetButton.addEventListener('click',function(){\n    syncToday();\n    if(count===0)return;\n    if(!resetArmed){\n      resetArmed=true;resetButton.textContent='Confirm reset';\n      resetButton.setAttribute('aria-label',\"Confirm reset of today's water count\");\n      status.className='status';status.textContent=\"Tap Confirm reset again to clear today's count.\";\n      resetTimer=setTimeout(function(){disarmReset();render();},4000);return;\n    }\n    disarmReset();count=0;save();render();\n  });\n  load();render();setInterval(render,60000);\n})();\n</script>\n</body>\n</html>\n"}]}