{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"goal-ring","type":"registry:component","title":"Goal Progress Ring","description":"One big number, one ring, and the milestones that feed it.","author":"Notion Arena","categories":["dashboards"],"tags":["widget","free","goal","progress","ring","okr"],"meta":{"kind":"widget","tier":"free","version":"1.0.2","updatedAt":"2026-08-19","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/goal-ring","fileUrl":"https://notionarena.vercel.app/widgets/goal-ring.html","downloadUrl":"https://notionarena.vercel.app/api/widget/goal-ring","height":230,"minHeight":180,"maxHeight":400,"params":[{"key":"label","label":"Goal","type":"text","default":"Q4 revenue"},{"key":"value","label":"Current","type":"number","default":68},{"key":"target","label":"Target","type":"number","default":100},{"key":"unit","label":"Unit","type":"text","default":"k"},{"key":"milestones","label":"Milestones","type":"text","default":"Kickoff,Beta,GA,Scale,Review"},{"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/goal-ring.html","type":"registry:file","target":"notion/widgets/goal-ring.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>Goal Progress Ring</title>\n<style>\n:root{--bg:#ffffff;--fg:#37352f;--muted:#787774;--surface:#f7f7f5;--border:#e9e9e7;--accent:#6940a5;--track:#e9e9e7;\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;--track:#2f2f2f}\n@media (prefers-color-scheme:dark){html[data-theme=\"auto\"]{--bg:#191919;--fg:#e6e6e4;--muted:#9b9a97;--surface:#252525;--border:#2f2f2f;--track:#2f2f2f}}\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%;display:flex;align-items:center;justify-content:center;gap:24px;padding:16px;flex-wrap:wrap}\n.ringbox{position:relative;width:158px;height:158px;flex:0 0 auto}\n.ringbox svg{width:100%;height:100%;transform:rotate(-90deg)}\n.ringbox circle{fill:none;stroke-width:11}\n.ringbox .bg{stroke:var(--track)}\n.ringbox .fg{stroke:var(--accent);stroke-linecap:round;transition:stroke-dashoffset 1s cubic-bezier(.16,1,.3,1)}\n.center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px}\n.pct{font-size:34px;font-weight:650;letter-spacing:-.04em;line-height:1;font-variant-numeric:tabular-nums}\n.of{font-size:11.5px;color:var(--muted);font-variant-numeric:tabular-nums}\n.side{min-width:190px;max-width:280px;display:flex;flex-direction:column;gap:11px}\n.goal{font-size:15px;font-weight:600;letter-spacing:-.01em}\n.ms{display:flex;flex-direction:column;gap:6px}\n.m{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--muted)}\n.m .box{width:15px;height:15px;border-radius:4px;border:1.5px solid var(--border);flex:0 0 auto;display:flex;align-items:center;justify-content:center;transition:.2s}\n.m.hit{color:var(--fg)}\n.m.hit .box{background:var(--accent);border-color:var(--accent)}\n.m .box svg{width:9px;height:9px;opacity:0;transition:.2s}\n.m.hit .box svg{opacity:1}\n.remain{font-size:12px;color:var(--muted);border-top:1px solid var(--border);padding-top:9px}\n.remain b{color:var(--fg);font-weight:600}\n@media (max-width:520px){.wrap{gap:14px}.ringbox{width:126px;height:126px}.pct{font-size:27px}}\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=\"ringbox\">\n    <svg viewBox=\"0 0 120 120\" aria-hidden=\"true\">\n      <circle class=\"bg\" cx=\"60\" cy=\"60\" r=\"52\"></circle>\n      <circle class=\"fg\" id=\"fg\" cx=\"60\" cy=\"60\" r=\"52\" stroke-dasharray=\"326.7\" stroke-dashoffset=\"326.7\"></circle>\n    </svg>\n    <div class=\"center\">\n      <div class=\"pct\" id=\"pct\">0%</div>\n      <div class=\"of\" id=\"of\"></div>\n    </div>\n  </div>\n  <div class=\"side\">\n    <div class=\"goal\" id=\"goal\">Goal</div>\n    <div class=\"ms\" id=\"ms\"></div>\n    <div class=\"remain\" id=\"remain\"></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||'#6940a5';\n  if(c.theme)document.documentElement.setAttribute('data-theme',c.theme);\n  document.documentElement.style.setProperty('--accent',ACCENT);\n\n  var label=c.label||'Goal', unit=c.unit||'', value=num('value',0), target=num('target',100);\n  var miles=(c.milestones||'').split(',').map(function(s){return s.trim();}).filter(Boolean);\n  if(target<=0)target=100;\n  var pct=Math.max(0,value/target);\n  document.getElementById('goal').textContent=label;\n  document.getElementById('pct').textContent=Math.round(pct*100)+'%';\n  document.getElementById('of').textContent=value+unit+' of '+target+unit;\n  var left=Math.max(0,target-value);\n  document.getElementById('remain').innerHTML=left>0\n    ? '<b>'+left+unit+'</b> left to go'\n    : 'Target reached: <b>'+Math.round((pct-1)*100)+'%</b> over';\n\n  var CIRC=2*Math.PI*52;\n  var fg=document.getElementById('fg');\n  fg.setAttribute('stroke-dashoffset',String(CIRC));\n  requestAnimationFrame(function(){\n    setTimeout(function(){fg.setAttribute('stroke-dashoffset',String(CIRC*(1-Math.min(1,pct))));},80);\n  });\n\n  var box=document.getElementById('ms');\n  if(miles.length){\n    box.innerHTML=miles.map(function(name,i){\n      var threshold=(i+1)/miles.length;\n      var hit=pct>=threshold-1e-9;\n      return '<div class=\"m'+(hit?' hit':'')+'\"><span class=\"box\"><svg viewBox=\"0 0 12 12\" fill=\"none\"><path d=\"M2 6.4l2.6 2.6L10 3.6\" stroke=\"#fff\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg></span><span>'+name.replace(/[<>&]/g,'')+'</span></div>';\n    }).join('');\n  }else{box.style.display='none';}\n})();\n</script>\n</body>\n</html>\n"}]}