{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"mood-tracker","type":"registry:component","title":"Mood & Energy Tracker","description":"Log how you feel in one tap and see the month as a calendar heatmap.","author":"Notion Arena","categories":["trackers"],"tags":["widget","pro","mood","wellbeing","journal","calendar"],"meta":{"kind":"widget","tier":"pro","version":"1.0.0","updatedAt":"2026-09-30","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/mood-tracker","fileUrl":"https://notionarena.vercel.app/widgets/mood-tracker.html","downloadUrl":"https://notionarena.vercel.app/api/widget/mood-tracker","height":300,"minHeight":240,"maxHeight":520,"params":[{"key":"showEnergy","label":"Log energy too","type":"boolean","default":true},{"key":"accent","label":"Accent","type":"color","default":"#ad1a72"},{"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/mood-tracker.html","type":"registry:file","target":"notion/widgets/mood-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>Mood &amp; Energy Tracker</title>\n<style>\n:root{--bg:#ffffff;--fg:#37352f;--muted:#787774;--surface:#f7f7f5;--border:#e9e9e7;--accent:#ad1a72;--cell:#ebedef;\n--font:ui-sans-serif,-apple-system,BlinkMacSystemFont,\"Segoe UI\",Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"}\nhtml[data-theme=\"dark\"]{--bg:#191919;--fg:#e6e6e4;--muted:#9b9a97;--surface:#252525;--border:#2f2f2f;--cell:#2a2a2a}\n@media (prefers-color-scheme:dark){html[data-theme=\"auto\"]{--bg:#191919;--fg:#e6e6e4;--muted:#9b9a97;--surface:#252525;--border:#2f2f2f;--cell:#2a2a2a}}\n*{box-sizing:border-box}html,body{margin:0;height:100%}\nbody{background:var(--bg);color:var(--fg);font-family:var(--font);font-size:13px;-webkit-font-smoothing:antialiased;overflow:hidden}\n.wrap{height:100%;display:flex;flex-direction:column;gap:10px;padding:12px 14px;overflow:auto}\n.head{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}\n.title{font-size:13px;font-weight:600}\n.nav{display:flex;gap:4px;align-items:center}\nbutton{font:inherit;font-size:11.5px;font-weight:500;border:1px solid var(--border);background:var(--surface);color:var(--muted);border-radius:7px;padding:3px 9px;cursor:pointer}\nbutton:hover{color:var(--fg)}\n.month{font-size:12px;font-weight:600;min-width:110px;text-align:center}\n.pick{display:flex;gap:6px;align-items:center;flex-wrap:wrap}\n.pick .lbl{font-size:11px;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.06em}\n.mood{border:1px solid var(--border);background:var(--surface);border-radius:9px;padding:5px 9px;cursor:pointer;font-size:16px;line-height:1;transition:.12s}\n.mood:hover{transform:translateY(-1px);border-color:color-mix(in oklab,var(--accent) 45%,var(--border))}\n.mood.sel{border-color:var(--accent);background:color-mix(in oklab,var(--accent) 12%,var(--surface))}\n.cal{display:grid;grid-template-columns:repeat(7,1fr);gap:4px}\n.dow{font-size:9.5px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);text-align:center;font-weight:600}\n.day{aspect-ratio:1/1;border:1px solid var(--border);border-radius:8px;background:var(--bg);display:flex;flex-direction:column;\nalign-items:center;justify-content:center;gap:1px;cursor:pointer;position:relative;min-height:32px;transition:.12s}\n.day:hover{border-color:color-mix(in oklab,var(--fg) 25%,var(--border))}\n.day.out{opacity:.3;cursor:default}\n.day.today{border-color:var(--accent);box-shadow:0 0 0 1px color-mix(in oklab,var(--accent) 35%,transparent)}\n.day .n{font-size:9.5px;color:var(--muted);font-variant-numeric:tabular-nums;position:absolute;top:2px;left:4px}\n.day .e{font-size:15px;line-height:1}\n.day .en{font-size:8px;color:var(--muted);letter-spacing:.02em}\n.foot{display:flex;align-items:center;gap:12px;flex-wrap:wrap;border-top:1px solid var(--border);padding-top:8px}\n.avg{font-size:11.5px;color:var(--muted)}\n.avg b{color:var(--fg);font-weight:600}\n.spark{flex:1 1 120px;min-width:110px;height:30px}\n.spark path{fill:none;stroke:var(--accent);stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}\n.spark .dots circle{fill:var(--accent)}\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  <div class=\"head\">\n    <div class=\"title\">Mood &amp; energy</div>\n    <div class=\"nav\">\n      <button id=\"prev\">‹</button>\n      <div class=\"month\" id=\"month\"></div>\n      <button id=\"next\">›</button>\n      <button id=\"todayBtn\">Today</button>\n    </div>\n  </div>\n  <div class=\"pick\" id=\"pick\">\n    <span class=\"lbl\">Log today</span>\n  </div>\n  <div class=\"cal\" id=\"cal\"></div>\n  <div class=\"foot\">\n    <div class=\"avg\" id=\"avg\"></div>\n    <svg class=\"spark\" id=\"spark\" viewBox=\"0 0 120 30\" preserveAspectRatio=\"none\"></svg>\n  </div>\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 bool(k,d){var v=c[k];return v===undefined?d:(v===true||v==='true'||v==='1');}\n\n  var ACCENT=c.accent||'#ad1a72';\n  if(c.theme)document.documentElement.setAttribute('data-theme',c.theme);\n  document.documentElement.style.setProperty('--accent',ACCENT);\n  var SHOW_ENERGY=bool('showEnergy',true);\n\n  var MOODS=[{v:1,e:'😞',t:'Awful'},{v:2,e:'🙁',t:'Low'},{v:3,e:'😐',t:'Okay'},{v:4,e:'🙂',t:'Good'},{v:5,e:'😄',t:'Great'}];\n  var ENERGY=['spent','low','steady','high','buzzing'];\n\n  var KEY='notion-arena.mood.v1';\n  var data={};\n  try{data=JSON.parse(localStorage.getItem(KEY)||'{}')||{};}catch(e){data={};}\n  function save(){try{localStorage.setItem(KEY,JSON.stringify(data));}catch(e){}}\n  function iso(d){return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0');}\n  function sod(d){return new Date(d.getFullYear(),d.getMonth(),d.getDate());}\n\n  var view=sod(new Date());view.setDate(1);\n  var today=sod(new Date());\n  var pickSel=0;\n\n  var pickEl=document.getElementById('pick'),calEl=document.getElementById('cal'),\n      monthEl=document.getElementById('month'),avgEl=document.getElementById('avg'),sparkEl=document.getElementById('spark');\n\n  function renderPicker(){\n    pickEl.querySelectorAll('.mood').forEach(function(n){n.remove();});\n    MOODS.forEach(function(m){\n      var b=document.createElement('button');\n      b.className='mood'+(pickSel===m.v?' sel':'');\n      b.type='button';b.textContent=m.e;b.title=m.t+' · '+m.v+'/5';\n      b.addEventListener('click',function(){\n        pickSel=pickSel===m.v?0:m.v;\n        var k=iso(today);\n        if(!data[k])data[k]={};\n        if(pickSel===0)delete data[k].mood;else data[k].mood=pickSel;\n        save();renderPicker();renderCal();renderFoot();\n      });\n      pickEl.appendChild(b);\n    });\n    if(SHOW_ENERGY){\n      var sel=document.getElementById('energy');\n      if(!sel){\n        sel=document.createElement('select');sel.id='energy';\n        sel.style.cssText='font:inherit;font-size:11.5px;border:1px solid var(--border);background:var(--surface);color:var(--muted);border-radius:7px;padding:3px 6px';\n        sel.innerHTML='<option value=\"\">energy…</option>'+ENERGY.map(function(e,i){return '<option value=\"'+(i+1)+'\">'+e+'</option>';}).join('');\n        sel.addEventListener('change',function(){\n          var k=iso(today);if(!data[k])data[k]={};\n          if(!sel.value)delete data[k].energy;else data[k].energy=parseInt(sel.value,10);\n          save();renderCal();renderFoot();\n        });\n        pickEl.appendChild(sel);\n      }\n      var cur=(data[iso(today)]||{}).energy;\n      sel.value=cur?String(cur):'';\n    }\n  }\n  function renderCal(){\n    monthEl.textContent=view.toLocaleDateString(undefined,{month:'long',year:'numeric'});\n    calEl.innerHTML='';\n    ['S','M','T','W','T','F','S'].forEach(function(d){\n      var s=document.createElement('div');s.className='dow';s.textContent=d;calEl.appendChild(s);\n    });\n    var first=new Date(view.getFullYear(),view.getMonth(),1);\n    var pad=first.getDay();\n    var dim=new Date(view.getFullYear(),view.getMonth()+1,0).getDate();\n    for(var i=0;i<pad;i++){var e=document.createElement('div');e.className='day out';calEl.appendChild(e);}\n    for(var dnum=1;dnum<=dim;dnum++){\n      var d=new Date(view.getFullYear(),view.getMonth(),dnum);\n      var k=iso(d),rec=data[k]||{};\n      var el=document.createElement('div');\n      el.className='day'+(d.getTime()===today.getTime()?' today':'')+(d>today?' out':'');\n      var mood=MOODS.filter(function(m){return m.v===rec.mood;})[0];\n      el.innerHTML='<span class=\"n\">'+dnum+'</span>'+(mood?'<span class=\"e\">'+mood.e+'</span>':'')\n        +(SHOW_ENERGY&&rec.energy?'<span class=\"en\">'+ENERGY[rec.energy-1].slice(0,5)+'</span>':'');\n      if(d<=today){\n        (function(kk,el2){\n          el2.addEventListener('click',function(){\n            var cur=(data[kk]||{}).mood||0;\n            var nx=cur>=5?1:cur+1;\n            if(!data[kk])data[kk]={};\n            data[kk].mood=nx;\n            if(kk===iso(today)){pickSel=nx;renderPicker();}\n            save();renderCal();renderFoot();\n          });\n        })(k,el);\n      }\n      calEl.appendChild(el);\n    }\n  }\n  function renderFoot(){\n    var pts=[],vals=[];\n    for(var i=29;i>=0;i--){\n      var d=new Date(today.getTime()-i*864e5);\n      var m=(data[iso(d)]||{}).mood;\n      if(m){pts.push([29-i,m]);vals.push(m);}\n    }\n    var avg=vals.length?(vals.reduce(function(a,b){return a+b;},0)/vals.length):0;\n    avgEl.innerHTML='<b>'+(avg?avg.toFixed(1):'n/a')+'</b> / 5 · last 30 days · '+vals.length+' logged';\n    if(!pts.length){sparkEl.innerHTML='';return;}\n    var W=120,H=30,padY=4;\n    var xs=pts.map(function(p){return padY+(p[0]/29)*(W-padY*2);});\n    var ys=pts.map(function(p){return H-padY-((p[1]-1)/4)*(H-padY*2);});\n    var dpath=xs.map(function(x,i){return (i?'L':'M')+x.toFixed(1)+' '+ys[i].toFixed(1);}).join(' ');\n    var dots=xs.map(function(x,i){return '<circle cx=\"'+x.toFixed(1)+'\" cy=\"'+ys[i].toFixed(1)+'\" r=\"1.4\"></circle>';}).join('');\n    sparkEl.innerHTML='<path d=\"'+dpath+'\"></path><g class=\"dots\">'+dots+'</g>';\n  }\n  document.getElementById('prev').addEventListener('click',function(){view.setMonth(view.getMonth()-1);renderCal();});\n  document.getElementById('next').addEventListener('click',function(){view.setMonth(view.getMonth()+1);renderCal();});\n  document.getElementById('todayBtn').addEventListener('click',function(){view=sod(new Date());view.setDate(1);renderCal();});\n  pickSel=(data[iso(today)]||{}).mood||0;\n  renderPicker();renderCal();renderFoot();\n})();\n</script>\n</body>\n</html>\n"}]}