{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"roi-calculator","type":"registry:component","title":"ROI Calculator","description":"Sliders in, a payback chart out, for proposals and sales pages.","author":"Notion Arena","categories":["business"],"tags":["widget","free","calculator","roi","sales","proposal"],"meta":{"kind":"widget","tier":"free","version":"1.0.1","updatedAt":"2026-09-08","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/roi-calculator","fileUrl":"https://notionarena.vercel.app/widgets/roi-calculator.html","downloadUrl":"https://notionarena.vercel.app/api/widget/roi-calculator","height":360,"minHeight":300,"maxHeight":640,"params":[{"key":"title","label":"Title","type":"text","default":"What is this worth to you?"},{"key":"price","label":"Monthly price","type":"number","default":99},{"key":"hours","label":"Hours saved / month","type":"number","default":12},{"key":"rate","label":"Hourly rate","type":"number","default":65},{"key":"currency","label":"Currency","type":"text","default":"$"},{"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/roi-calculator.html","type":"registry:file","target":"notion/widgets/roi-calculator.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>ROI Calculator</title>\n<style>\n:root{--bg:#ffffff;--fg:#37352f;--muted:#787774;--surface:#f7f7f5;--border:#e9e9e7;--accent:#0f7b6c;--good:#0f7b6c;--bad:#e03e3e;\n--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;--good:#4dab9a;--bad:#ff7369}\n@media (prefers-color-scheme:dark){html[data-theme=\"auto\"]{--bg:#191919;--fg:#e6e6e4;--muted:#9b9a97;--surface:#252525;--border:#2f2f2f;--good:#4dab9a;--bad:#ff7369}}\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:11px;padding:13px 15px;overflow:auto}\n.title{font-size:14px;font-weight:600;letter-spacing:-.01em}\n.cols{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:14px}\n@media (max-width:560px){.cols{grid-template-columns:1fr}}\n.field{display:flex;flex-direction:column;gap:4px}\n.field label{font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;display:flex;justify-content:space-between;gap:6px}\n.field .val{font-variant-numeric:tabular-nums;color:var(--fg);font-weight:600;text-transform:none;letter-spacing:0;font-size:11.5px}\n.srow{display:flex;align-items:center;gap:8px}\ninput[type=range]{-webkit-appearance:none;appearance:none;flex:1;height:4px;border-radius:99px;background:var(--border);outline:none}\ninput[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:15px;height:15px;border-radius:50%;background:var(--accent);\nborder:2px solid var(--bg);box-shadow:0 0 0 1px var(--accent);cursor:pointer}\ninput[type=range]::-moz-range-thumb{width:13px;height:13px;border-radius:50%;background:var(--accent);border:2px solid var(--bg);cursor:pointer}\ninput[type=number],input[type=text]{font:inherit;font-size:12.5px;width:74px;padding:4px 7px;border:1px solid var(--border);\nborder-radius:7px;background:var(--bg);color:var(--fg);font-variant-numeric:tabular-nums}\ninput:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px color-mix(in oklab,var(--accent) 20%,transparent)}\n.out{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}\n.o{border:1px solid var(--border);border-radius:10px;background:var(--surface);padding:9px 10px}\n.o span{display:block;font-size:10.5px;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.05em;margin-bottom:2px}\n.o b{font-size:19px;font-weight:640;letter-spacing:-.03em;font-variant-numeric:tabular-nums;line-height:1.15}\n.o.hi b{color:var(--accent)}\n.chart{border:1px solid var(--border);border-radius:10px;background:var(--surface);padding:10px 11px 6px;position:relative}\n.chart .cap{font-size:10.5px;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px}\n.chart svg{width:100%;height:88px;display:block}\n.bar{fill:color-mix(in oklab,var(--accent) 22%,transparent)}\n.bar.pos{fill:var(--accent)}\n.be{stroke:var(--muted);stroke-width:1;stroke-dasharray:3 3}\n.xlab{display:flex;justify-content:space-between;font-size:9.5px;color:var(--muted);margin-top:3px;font-variant-numeric:tabular-nums}\n.actions{display:flex;gap:6px;justify-content:flex-end}\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:4px 10px;cursor:pointer}\nbutton:hover{color:var(--fg);border-color:color-mix(in oklab,var(--fg) 20%,var(--border))}\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=\"title\" id=\"title\">What is this worth to you?</div>\n  <div class=\"cols\">\n    <div style=\"display:flex;flex-direction:column;gap:10px\" id=\"inputs\"></div>\n    <div style=\"display:flex;flex-direction:column;gap:9px\">\n      <div class=\"out\">\n        <div class=\"o\"><span>Cost / mo</span><b id=\"oCost\">n/a</b></div>\n        <div class=\"o hi\"><span>Saving / mo</span><b id=\"oSave\">n/a</b></div>\n        <div class=\"o\"><span>ROI</span><b id=\"oRoi\">n/a</b></div>\n      </div>\n      <div class=\"chart\">\n        <div class=\"cap\">Cumulative value, 12 months</div>\n        <svg id=\"svg\" viewBox=\"0 0 300 88\" preserveAspectRatio=\"none\"></svg>\n        <div class=\"xlab\"><span>M0</span><span>M3</span><span>M6</span><span>M9</span><span>M12</span></div>\n      </div>\n      <div class=\"actions\"><button id=\"copy\">Copy summary</button></div>\n    </div>\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 num(k,d){var v=parseFloat(c[k]);return isFinite(v)?v:d;}\n\n  var ACCENT=c.accent||'#0f7b6c',CUR=c.currency||'$';\n  if(c.theme)document.documentElement.setAttribute('data-theme',c.theme);\n  document.documentElement.style.setProperty('--accent',ACCENT);\n  document.getElementById('title').textContent=c.title||'What is this worth to you?';\n\n  var FIELDS=[\n    {k:'price',label:'Monthly price',def:num('price',99),min:0,max:2000,step:5},\n    {k:'hours',label:'Hours saved / month',def:num('hours',12),min:0,max:80,step:1},\n    {k:'rate',label:'Hourly rate',def:num('rate',65),min:0,max:500,step:5},\n    {k:'seats',label:'People affected',def:num('seats',1),min:1,max:50,step:1}\n  ];\n  var state={};\n  var host=document.getElementById('inputs');\n  FIELDS.forEach(function(f){\n    state[f.k]=f.def;\n    var d=document.createElement('div');d.className='field';\n    d.innerHTML='<label>'+f.label+' <span class=\"val\" id=\"v_'+f.k+'\"></span></label>'+\n      '<div class=\"srow\"><input type=\"range\" id=\"r_'+f.k+'\" min=\"'+f.min+'\" max=\"'+f.max+'\" step=\"'+f.step+'\" value=\"'+f.def+'\">'+\n      '<input type=\"number\" id=\"n_'+f.k+'\" min=\"'+f.min+'\" max=\"'+f.max+'\" step=\"'+f.step+'\" value=\"'+f.def+'\"></div>';\n    host.appendChild(d);\n    var r=d.querySelector('#r_'+f.k),n=d.querySelector('#n_'+f.k);\n    function set(v,from){\n      v=Math.max(f.min,Math.min(f.max,isFinite(v)?v:f.def));\n      state[f.k]=v;r.value=v;n.value=v;paint();\n    }\n    r.addEventListener('input',function(){set(parseFloat(r.value));});\n    n.addEventListener('input',function(){set(parseFloat(n.value));});\n  });\n\n  function money(v){\n    var a=Math.abs(v),s=a>=10000?(v/1000).toFixed(a>=100000?0:1)+'k':a>=1000?Math.round(v).toLocaleString():Math.round(v).toString();\n    return CUR+s;\n  }\n  function compute(){\n    var cost=state.price*state.seats;\n    var value=state.hours*state.rate*state.seats;\n    var net=value-cost;\n    var roi=cost>0?value/cost:0;\n    var payback=net>0?cost/net:null;\n    // cumulative net position, month 0..12: the payback curve\n    var series=[];var cum=-cost;series.push(cum);\n    for(var mm=1;mm<=12;mm++){cum+=net;series.push(cum);}\n    return {cost:cost,value:value,net:net,roi:roi,payback:payback,series:series};\n  }\n  var svg=document.getElementById('svg');\n  function paint(){\n    var r=compute();\n    document.getElementById('v_price').textContent=money(r.cost)+'/mo';\n    document.getElementById('v_hours').textContent=state.hours+'h';\n    document.getElementById('v_rate').textContent=money(state.rate)+'/h';\n    document.getElementById('v_seats').textContent=state.seats+(state.seats===1?' person':' people');\n    document.getElementById('oCost').textContent=money(r.cost);\n    document.getElementById('oSave').textContent=money(r.net);\n    document.getElementById('oSave').style.color=r.net>=0?'var(--good)':'var(--bad)';\n    document.getElementById('oRoi').textContent=r.roi?r.roi.toFixed(1)+'×':'n/a';\n    document.getElementById('oRoi').style.color=r.roi>=1?'var(--accent)':'var(--bad)';\n\n    var W=300,H=88,pad=6;\n    var s=r.series,max=Math.max.apply(null,s),min=Math.min.apply(null,s);\n    if(max===min){max=min+1;}\n    var span=max-min;\n    var bw=(W-pad*2)/s.length;\n    var zeroY=H-pad-((0-min)/span)*(H-pad*2);\n    var bars=s.map(function(v,i){\n      var y=H-pad-((v-min)/span)*(H-pad*2);\n      var top=Math.min(y,zeroY),h=Math.max(1.5,Math.abs(y-zeroY));\n      return '<rect class=\"bar'+(v>=0?' pos':'')+'\" x=\"'+(pad+i*bw+bw*0.16).toFixed(1)+'\" y=\"'+top.toFixed(1)+'\" width=\"'+(bw*0.68).toFixed(1)+'\" height=\"'+h.toFixed(1)+'\" rx=\"1.5\"></rect>';\n    }).join('');\n    var zl=(zeroY>pad&&zeroY<H-pad)?'<line class=\"be\" x1=\"0\" y1=\"'+zeroY.toFixed(1)+'\" x2=\"'+W+'\" y2=\"'+zeroY.toFixed(1)+'\"></line>':'';\n    svg.innerHTML=zl+bars;\n    window.__roi=r;\n  }\n  document.getElementById('copy').addEventListener('click',function(e){\n    var r=window.__roi||compute(),b=e.currentTarget;\n    var txt='ROI estimate\\n'\n      +'Cost: '+money(r.cost)+' / month ('+state.seats+' × '+money(state.price)+')\\n'\n      +'Time saved: '+state.hours+'h / month at '+money(state.rate)+'/h = '+money(r.value)+'\\n'\n      +'Net saving: '+money(r.net)+' / month\\n'\n      +'ROI: '+(r.roi?r.roi.toFixed(1)+'×':'n/a')+'\\n'\n      +(r.payback!==null?'Payback: month '+Math.max(1,Math.ceil(r.payback)):'Payback: not reached in 12 months');\n    function done(t){b.textContent=t;setTimeout(function(){b.textContent='Copy summary';},1400);}\n    try{navigator.clipboard.writeText(txt).then(function(){done('Copied ✓');},function(){done('Press ⌘C');});}catch(err){done('Press ⌘C');}\n    try{console.log(txt);}catch(err){}\n  });\n  paint();\n})();\n</script>\n</body>\n</html>\n"}]}