{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"billable-time-tracker","type":"registry:component","title":"Billable Time Tracker","description":"Log project sessions and calculate a manual hourly estimate locally.","author":"Notion Arena","categories":["business"],"tags":["widget","free","time tracking","billable hours","freelancer","project","estimate"],"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/billable-time-tracker","fileUrl":"https://notionarena.vercel.app/widgets/billable-time-tracker.html","downloadUrl":"https://notionarena.vercel.app/api/widget/billable-time-tracker","height":370,"minHeight":270,"maxHeight":520,"params":[{"key":"label","label":"Widget title","type":"text","default":"Billable time"},{"key":"currency","label":"Currency label","type":"text","default":"$"},{"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/billable-time-tracker.html","type":"registry:file","target":"notion/widgets/billable-time-tracker.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>Billable Time Tracker</title>\n<style>\n:root{--bg:#fff;--fg:#37352f;--muted:#787774;--surface:#f7f7f5;--border:#e9e9e7;--accent:#d9730d;--accent-ink:#fff;--accent-text:var(--accent)--danger:#c24136;--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;--danger:#ff7369}\n@media(prefers-color-scheme:dark){html[data-theme=\"auto\"]{--bg:#191919;--fg:#e6e6e4;--muted:#9b9a97;--surface:#252525;--border:#343434;--danger:#ff7369}}\n*{box-sizing:border-box}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}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:270px;display:flex;flex-direction:column;gap:8px;padding:13px;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:13.5px;font-weight:650;margin-top:2px}.clock{font-size:clamp(24px,7vw,34px);font-weight:700;letter-spacing:-.05em;font-variant-numeric:tabular-nums;color:var(--accent-text);text-align:center}.fields{display:grid;grid-template-columns:minmax(0,1fr) 105px;gap:5px}.field{display:flex;min-width:0;flex-direction:column;gap:3px}.field label{font-size:8.5px;color:var(--muted);font-weight:600}.fields input{height:29px;min-width:0;border:1px solid var(--border);border-radius:7px;background:var(--surface);color:var(--fg);padding:4px 6px;font:inherit;font-size:10px}.fields input:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.controls{display:flex;justify-content:center;gap:5px}.controls button{min-height:29px;border:1px solid var(--border);border-radius:7px;background:var(--surface);color:var(--fg);font:inherit;font-size:9.5px;padding:4px 8px;cursor:pointer}.controls .primary{background:var(--accent);border-color:var(--accent);color:var(--accent-ink)}.controls button:disabled{opacity:.45;cursor:not-allowed}.controls button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.summary{display:flex;justify-content:space-between;gap:8px;font-size:9px;color:var(--muted)}.list{display:flex;min-height:50px;flex:1;flex-direction:column;gap:4px;overflow:auto}.entry{display:grid;grid-template-columns:minmax(0,1fr) auto 25px;gap:3px 6px;padding:5px 6px;border:1px solid var(--border);border-radius:7px;background:var(--surface)}.project{font-size:9.5px;font-weight:600;overflow-wrap:anywhere}.detail{grid-column:1/3;font-size:8.5px;color:var(--muted)}.amount{font-size:9px;font-variant-numeric:tabular-nums;color:var(--accent-text)}.remove{grid-column:3;grid-row:1/3;width:24px;height:24px;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--muted);cursor:pointer}.status{min-height:12px;text-align:center;font-size:8.5px;color:var(--muted)}.empty{text-align:center;padding:8px;color:var(--muted);font-size:9px}\n@media(max-width:330px){.wrap{padding:10px}.fields{grid-template-columns:minmax(0,1fr) 90px}.controls button{padding:4px 6px;font-size:9px}}\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\">Browser-local time log</div><div class=\"title\" id=\"title\">Billable time</div></div></header>\n  <div class=\"clock\" id=\"clock\" role=\"timer\" aria-live=\"off\">00:00:00</div>\n  <section class=\"fields\"><div class=\"field\"><label for=\"project\">Project or task</label><input id=\"project\" maxlength=\"60\" placeholder=\"Add a label\" /></div><div class=\"field\"><label id=\"rateLabel\" for=\"rate\">Rate per hour</label><input id=\"rate\" type=\"number\" min=\"0\" max=\"100000\" step=\"0.01\" placeholder=\"0.00\" /></div></section>\n  <div class=\"controls\"><button class=\"primary\" id=\"start\" type=\"button\">Start timer</button><button id=\"pause\" type=\"button\" disabled>Pause</button><button id=\"stop\" type=\"button\" disabled>Stop &amp; save</button></div>\n  <div class=\"status\" id=\"status\" aria-live=\"polite\">Time and estimates save only in this browser.</div>\n  <div class=\"summary\"><span id=\"today\">Today: 0m</span><span id=\"total\">0 sessions</span></div>\n  <section class=\"list\" id=\"list\" aria-label=\"Logged sessions\"></section>\n  <div class=\"status\">Estimates only, not an invoice or audited timesheet. Nothing is synced.</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 safeColor(v,d){return /^#[0-9a-f]{6}$/i.test(String(v||''))?String(v):d;}\n  function rgb(hex){var s=String(hex).slice(1);return [0,2,4].map(function(i){return parseInt(s.slice(i,i+2),16);});}\n  function luminance(values){return values.map(function(v){v/=255;return v<=.04045?v/12.92:Math.pow((v+.055)/1.055,2.4);}).reduce(function(a,v,i){return a+v*[.2126,.7152,.0722][i];},0);}\n  function contrast(a,b){var x=luminance(a),y=luminance(b);return (Math.max(x,y)+.05)/(Math.min(x,y)+.05);}\n  function accentInk(hex){var c=rgb(hex);return contrast(c,[255,255,255])>=contrast(c,[0,0,0])?'#fff':'#000';}\n  function accentText(hex){var dark=theme==='dark'||(theme==='auto'&&window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches),bg=rgb(dark?'#252525':'#f7f7f5'),base=rgb(hex),target=dark?[255,255,255]:[0,0,0];if(contrast(base,bg)>=4.5)return hex;for(var t=.02;t<=1;t+=.02){var mix=base.map(function(v,i){return Math.round(v+(target[i]-v)*t);});if(contrast(mix,bg)>=4.5)return '#'+mix.map(function(v){return v.toString(16).padStart(2,'0');}).join('');}return dark?'#fff':'#191919';}\n  function setAccentText(){document.documentElement.style.setProperty('--accent-text',accentText(ACCENT));}\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||'Billable time').slice(0,48),CURRENCY=String(c.currency||'$').slice(0,6);\n  document.documentElement.setAttribute('data-theme',theme);document.documentElement.style.setProperty('--accent',ACCENT);document.documentElement.style.setProperty('--accent-ink',accentInk(ACCENT));setAccentText();if(theme==='auto'&&window.matchMedia){var scheme=window.matchMedia('(prefers-color-scheme: dark)');if(scheme.addEventListener)scheme.addEventListener('change',setAccentText);else if(scheme.addListener)scheme.addListener(setAccentText);}document.getElementById('title').textContent=LABEL;document.getElementById('rateLabel').textContent='Rate per hour ('+CURRENCY+')';document.title=LABEL;\n  function dateKey(d){return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0');}\n  function hash(s){var h=5381;for(var i=0;i<s.length;i++)h=((h<<5)-h+s.charCodeAt(i))|0;return 'b'+(h>>>0).toString(36);}\n  var id=hash(LABEL),KEY='notion-arena.billable-time-tracker.v1',store={logs:{}},state={sessions:[],active:null};\n  try{var saved=JSON.parse(localStorage.getItem(KEY)||'{}');if(saved&&saved.logs)store=saved;var rec=store.logs&&store.logs[id];if(rec){state.sessions=Array.isArray(rec.sessions)?rec.sessions.filter(function(x){return x&&typeof x.project==='string';}).slice(0,30):[];state.active=rec.active&&typeof rec.active.project==='string'?rec.active:null;}}catch(e){}\n  function save(){if(!store.logs)store.logs={};store.logs[id]=state;var keys=Object.keys(store.logs);while(keys.length>8){var old=keys.shift();if(old===id){keys.push(old);continue;}delete store.logs[old];}try{localStorage.setItem(KEY,JSON.stringify(store));}catch(e){}}\n  var project=document.getElementById('project'),rateInput=document.getElementById('rate'),startBtn=document.getElementById('start'),pauseBtn=document.getElementById('pause'),stopBtn=document.getElementById('stop'),clock=document.getElementById('clock'),timer=null;\n  function elapsed(){var a=state.active;if(!a)return 0;return Math.max(0,Number(a.elapsed)||0)+(a.running&&a.startedAt?Math.max(0,Date.now()-a.startedAt):0);}\n  function format(ms){var s=Math.floor(ms/1000),h=Math.floor(s/3600),m=Math.floor(s%3600/60),sec=s%60;return String(h).padStart(2,'0')+':'+String(m).padStart(2,'0')+':'+String(sec).padStart(2,'0');}\n  function money(n){return CURRENCY+' '+Number(n||0).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2});}\n  function render(){var active=state.active,ms=elapsed();clock.textContent=format(ms);project.disabled=!!active;rateInput.disabled=!!active;startBtn.textContent=active?'Resume':'Start timer';startBtn.disabled=!!(active&&active.running);pauseBtn.disabled=!active||!active.running;stopBtn.disabled=!active;document.getElementById('status').textContent=active?(active.running?'Timer running for '+active.project:(ms?'Paused. Resume or save the session.':'Paused.')):'Time and estimates save only in this browser.';var today=dateKey(new Date()),todayMs=state.sessions.filter(function(x){return x.date===today;}).reduce(function(sum,x){return sum+(Number(x.duration)||0);},0)+(active&&active.date===today?ms:0);document.getElementById('today').textContent='Today: '+(todayMs/3600000).toFixed(2)+'h';document.getElementById('total').textContent=state.sessions.length+' sessions';var list=document.getElementById('list');while(list.firstChild)list.removeChild(list.firstChild);state.sessions.forEach(function(entry,index){var row=document.createElement('div'),name=document.createElement('div'),amount=document.createElement('div'),detail=document.createElement('div'),remove=document.createElement('button');row.className='entry';name.className='project';name.textContent=entry.project;amount.className='amount';amount.textContent=money((Number(entry.duration)||0)/3600000*(Number(entry.rate)||0));detail.className='detail';detail.textContent=entry.date+' · '+format(entry.duration);remove.className='remove';remove.type='button';remove.textContent='×';remove.setAttribute('aria-label','Remove '+entry.project+' session');remove.addEventListener('click',function(){state.sessions.splice(index,1);save();render();});row.appendChild(name);row.appendChild(amount);row.appendChild(detail);row.appendChild(remove);list.appendChild(row);});if(!state.sessions.length){var empty=document.createElement('div');empty.className='empty';empty.textContent='No saved sessions yet.';list.appendChild(empty);}}\n  function start(){if(!state.active){var name=project.value.trim();if(!name){document.getElementById('status').textContent='Enter a project or task first.';return;}state.active={project:name.slice(0,60),elapsed:0,startedAt:Date.now(),running:true,rate:Math.max(0,Math.min(100000,parseFloat(rateInput.value)||0)),date:dateKey(new Date())};}else{state.active.startedAt=Date.now();state.active.running=true;}save();if(!timer)timer=setInterval(function(){render();save();},1000);render();}\n  startBtn.addEventListener('click',start);\n  pauseBtn.addEventListener('click',function(){if(!state.active||!state.active.running)return;state.active.elapsed=elapsed();state.active.running=false;state.active.startedAt=0;if(timer){clearInterval(timer);timer=null;}save();render();});\n  stopBtn.addEventListener('click',function(){if(!state.active)return;var a=state.active,duration=elapsed();if(duration>0)state.sessions.unshift({project:a.project,duration:duration,rate:Number(a.rate)||0,date:a.date||dateKey(new Date())});state.sessions=state.sessions.slice(0,30);state.active=null;if(timer){clearInterval(timer);timer=null;}save();render();});\n  if(state.active&&state.active.running)timer=setInterval(function(){render();save();},1000);render();\n})();\n</script>\n</body>\n</html>\n"}]}