{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"gantt-timeline","type":"registry:component","title":"Gantt Timeline","description":"A dependency-aware timeline you can hand to a client.","author":"Notion Arena","categories":["planning"],"tags":["widget","pro","gantt","timeline","roadmap","project"],"meta":{"kind":"widget","tier":"pro","version":"1.2.0","updatedAt":"2026-09-25","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/gantt-timeline","fileUrl":"https://notionarena.vercel.app/widgets/gantt-timeline.html","downloadUrl":"https://notionarena.vercel.app/api/widget/gantt-timeline","height":340,"minHeight":240,"maxHeight":700,"params":[{"key":"data","label":"Tasks (JSON)","type":"text","default":"","help":"[{name,start,end,progress,group,color,dependsOn}]"},{"key":"today","label":"Show today line","type":"boolean","default":true},{"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/gantt-timeline.html","type":"registry:file","target":"notion/widgets/gantt-timeline.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>Gantt Timeline</title>\n<style>\n:root{--bg:#ffffff;--fg:#37352f;--muted:#787774;--surface:#f7f7f5;--border:#e9e9e7;--accent:#d9730d;--grid:#f0efed;--today:#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;--grid:#232323;--today:#ff7369}\n@media (prefers-color-scheme:dark){html[data-theme=\"auto\"]{--bg:#191919;--fg:#e6e6e4;--muted:#9b9a97;--surface:#252525;--border:#2f2f2f;--grid:#232323;--today:#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:12.5px;-webkit-font-smoothing:antialiased;overflow:hidden}\n.wrap{height:100%;display:flex;flex-direction:column;padding:11px 13px;gap:6px;overflow:hidden}\n.head{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap}\n.title{font-size:13px;font-weight:600}\n.legend{display:flex;gap:9px;align-items:center;font-size:10.5px;color:var(--muted);flex-wrap:wrap}\n.legend i{width:9px;height:9px;border-radius:2px;display:inline-block;margin-right:3px;vertical-align:-1px}\n.scroller{flex:1;min-height:0;overflow:auto;border:1px solid var(--border);border-radius:10px;background:var(--bg)}\n.grid{position:relative;min-width:620px}\n.axis{position:sticky;top:0;z-index:3;background:var(--surface);border-bottom:1px solid var(--border);display:flex;height:26px}\n.axis .names{flex:0 0 var(--labelw,150px);border-right:1px solid var(--border);display:flex;align-items:center;padding:0 8px;font-size:10px;\ntext-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:600}\n.axis .ticks{flex:1;position:relative}\n.axis .tick{position:absolute;top:0;bottom:0;border-left:1px solid var(--border);padding:0 5px;font-size:10px;color:var(--muted);\ndisplay:flex;align-items:center;white-space:nowrap;font-variant-numeric:tabular-nums}\n.rows{position:relative}\n.row{display:flex;height:32px;border-bottom:1px solid var(--grid);position:relative}\n.row:last-child{border-bottom:none}\n.row.group{background:var(--surface)}\n.row.group .names{font-weight:650;font-size:11.5px;text-transform:none;letter-spacing:0;color:var(--fg)}\n.names{flex:0 0 var(--labelw,150px);border-right:1px solid var(--border);display:flex;align-items:center;gap:5px;padding:0 8px;\nfont-size:11.5px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;position:sticky;left:0;background:inherit;z-index:2}\n.row .names{background:var(--bg)}\n.row.group .names{background:var(--surface)}\n.track{flex:1;position:relative}\n.vline{position:absolute;top:0;bottom:0;border-left:1px solid var(--grid)}\n.bar{position:absolute;top:7px;height:18px;border-radius:5px;background:color-mix(in oklab,var(--c,var(--accent)) 26%,transparent);\nborder:1px solid color-mix(in oklab,var(--c,var(--accent)) 55%,transparent);overflow:hidden;cursor:default;min-width:4px}\n.bar .fill{position:absolute;left:0;top:0;bottom:0;background:var(--c,var(--accent));opacity:.85}\n.bar .cap{position:absolute;inset:0;display:flex;align-items:center;padding:0 6px;font-size:10px;font-weight:600;color:var(--fg);\nwhite-space:nowrap;overflow:hidden;text-overflow:ellipsis;mix-blend-mode:normal}\n.bar.done{opacity:.6}\n.bar.late{border-color:var(--today);box-shadow:0 0 0 1px color-mix(in oklab,var(--today) 30%,transparent)}\n.today{position:absolute;top:0;bottom:0;width:0;border-left:1.5px solid var(--today);z-index:2;pointer-events:none}\n.today::before{content:\"Today\";position:absolute;top:2px;left:3px;font-size:9px;font-weight:700;color:var(--today);text-transform:uppercase;letter-spacing:.05em}\n.dep{position:absolute;pointer-events:none;z-index:1}\n.dep path{fill:none;stroke:var(--muted);stroke-width:1.2;opacity:.55;marker-end:url(#ah)}\n.milestone{position:absolute;top:9px;width:14px;height:14px;background:var(--c,var(--accent));transform:rotate(45deg);border-radius:2px}\n.foot{font-size:10.5px;color:var(--muted);display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap}\n@media (max-width:520px){.wrap{padding:9px}.grid{min-width:520px}}\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\" id=\"title\">Project timeline</div>\n    <div class=\"legend\" id=\"legend\"></div>\n  </div>\n  <div class=\"scroller\"><div class=\"grid\" id=\"grid\"></div></div>\n  <div class=\"foot\"><span id=\"range\"></span><span id=\"stats\"></span></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||'#d9730d';\n  if(c.theme)document.documentElement.setAttribute('data-theme',c.theme);\n  document.documentElement.style.setProperty('--accent',ACCENT);\n  if(c.title)document.getElementById('title').textContent=c.title;\n\n  function d(s){var x=new Date(String(s).length===10?String(s)+'T00:00:00':String(s));return isNaN(x)?null:x;}\n  function iso(x){return x.getFullYear()+'-'+String(x.getMonth()+1).padStart(2,'0')+'-'+String(x.getDate()).padStart(2,'0');}\n  function addDays(x,n){return new Date(x.getTime()+n*864e5);}\n\n  var today=new Date();today=new Date(today.getFullYear(),today.getMonth(),today.getDate());\n  var DEMO=(function(){\n    var s=addDays(today,-14);\n    function at(off,len){return [iso(addDays(s,off)),iso(addDays(s,off+len))];}\n    var t=[\n      {name:'Discovery',group:'Plan',progress:100},\n      {name:'Stakeholder interviews',group:'Plan',progress:100},\n      {name:'Scope & estimate',group:'Plan',progress:80},\n      {name:'Design system',group:'Build',progress:55},\n      {name:'Core flows',group:'Build',progress:35},\n      {name:'Integrations',group:'Build',progress:10,dependsOn:'Core flows'},\n      {name:'Beta with 5 clients',group:'Launch',progress:0,dependsOn:'Core flows'},\n      {name:'Public launch',group:'Launch',progress:0,milestone:true,dependsOn:'Beta with 5 clients'}\n    ];\n    var spans=[at(0,7),at(3,8),at(9,6),at(12,16),at(20,22),at(30,14),at(40,12),at(54,0)];\n    t.forEach(function(x,i){x.start=spans[i][0];x.end=spans[i][1];});\n    var colors={Plan:'#2383e2',Build:'#6940a5',Launch:'#0f7b6c'};\n    t.forEach(function(x){x.color=colors[x.group];});\n    return t;\n  })();\n\n  var tasks=DEMO;\n  if(c.data){\n    try{\n      var parsed=JSON.parse(decodeURIComponent(String(c.data)));\n      if(Array.isArray(parsed)&&parsed.length)tasks=parsed;\n    }catch(e){}\n  }\n  tasks=tasks.filter(function(t){return t.name;}).map(function(t){\n    var st=d(t.start),en=d(t.end||t.start);\n    if(!st)st=today;if(!en||en<st)en=st;\n    return {name:String(t.name),group:t.group||'',start:st,end:en,\n      progress:Math.max(0,Math.min(100,parseFloat(t.progress)||0)),\n      color:t.color||ACCENT,milestone:!!t.milestone,dependsOn:t.dependsOn||null};\n  });\n\n  var groups=[],byGroup={};\n  tasks.forEach(function(t){\n    var g=t.group||'';\n    if(!byGroup[g]){byGroup[g]=[];groups.push(g);}\n    byGroup[g].push(t);\n  });\n\n  var min=tasks.reduce(function(a,t){return t.start<a?t.start:a;},tasks[0]?tasks[0].start:today);\n  var max=tasks.reduce(function(a,t){return t.end>a?t.end:a;},tasks[0]?tasks[0].end:today);\n  min=addDays(min,-3);max=addDays(max,4);\n  var days=Math.max(1,Math.round((max-min)/864e5));\n\n  var LABELW=150, TRACK=Math.max(460,days*16);\n  var grid=document.getElementById('grid');\n  grid.style.setProperty('--labelw',LABELW+'px');\n  grid.style.minWidth=(LABELW+TRACK)+'px';\n\n  function x(date){return ((date-min)/864e5)*(TRACK/days);}\n\n  // axis\n  var ticks=[];\n  var step=days<=45?7:days<=120?14:30;\n  var cur=new Date(min);\n  cur.setDate(cur.getDate()+(step-(cur.getDay()%step||step))%step||0);\n  var i=0;\n  while(cur<=max&&i<200){\n    ticks.push(new Date(cur));cur=addDays(cur,step);i++;\n  }\n  if(!ticks.length)ticks=[min];\n\n  var axisHtml='<div class=\"axis\"><div class=\"names\">Task</div><div class=\"ticks\">';\n  ticks.forEach(function(t){\n    axisHtml+='<div class=\"tick\" style=\"left:'+x(t).toFixed(1)+'px\">'+t.toLocaleDateString(undefined,{month:'short',day:'numeric'})+'</div>';\n  });\n  axisHtml+='</div></div>';\n\n  var rowsHtml='<div class=\"rows\">';\n  var vlines='';\n  ticks.forEach(function(t){vlines+='<div class=\"vline\" style=\"left:'+x(t).toFixed(1)+'px\"></div>';});\n\n  groups.forEach(function(g){\n    if(g){rowsHtml+='<div class=\"row group\"><div class=\"names\">'+g.replace(/[<>&]/g,'')+'</div><div class=\"track\">'+vlines+'</div></div>';}\n    byGroup[g].forEach(function(t,idx){\n      var l=x(t.start),r=x(t.end);\n      var w=Math.max(t.milestone?0:r-l,2);\n      var late=!t.milestone&&t.progress<100&&t.end<today;\n      var inner='';\n      if(t.milestone){\n        inner='<div class=\"milestone\" style=\"left:'+(l-7).toFixed(1)+'px;--c:'+t.color+'\" title=\"'+t.name+' · '+iso(t.start)+'\"></div>';\n      }else{\n        inner='<div class=\"bar'+(t.progress>=100?' done':'')+(late?' late':'')+'\" style=\"left:'+l.toFixed(1)+'px;width:'+w.toFixed(1)+'px;--c:'+t.color+'\" '+\n          'title=\"'+t.name+' · '+iso(t.start)+' → '+iso(t.end)+' · '+t.progress+'%'+(late?' · overdue':'')+'\">'+\n          '<div class=\"fill\" style=\"width:'+t.progress+'%\"></div>'+\n          '<div class=\"cap\">'+(w>52?t.name.replace(/[<>&]/g,''):'')+'</div></div>';\n      }\n      rowsHtml+='<div class=\"row\" data-task=\"'+idx+'\"><div class=\"names\" title=\"'+t.name.replace(/\"/g,'')+'\">'+\n        '<span style=\"width:6px;height:6px;border-radius:2px;background:'+t.color+';flex:0 0 auto\"></span>'+\n        t.name.replace(/[<>&]/g,'')+'</div><div class=\"track\">'+vlines+inner+'</div></div>';\n    });\n  });\n  if(today>=min&&today<=max){\n    rowsHtml+='<div class=\"today\" style=\"left:'+(LABELW+x(today)).toFixed(1)+'px\"></div>';\n  }\n  rowsHtml+='</div>';\n  grid.innerHTML=axisHtml+rowsHtml;\n\n  // dependency arrows\n  var svgNS='http://www.w3.org/2000/svg';\n  var tracks=grid.querySelectorAll('.row[data-task] .track');\n  var flat=[];\n  groups.forEach(function(g){byGroup[g].forEach(function(t){flat.push(t);});});\n  var rows=grid.querySelectorAll('.row[data-task]');\n  flat.forEach(function(t,i){\n    if(!t.dependsOn)return;\n    var src=null,si=-1;\n    flat.forEach(function(o,j){if(o.name===t.dependsOn){src=o;si=j;}});\n    if(!src||si<0||!rows[si]||!rows[i])return;\n    var track=rows[i].querySelector('.track');\n    var x1=x(src.milestone?src.start:src.end), y1=rows[si].offsetTop-rows[i].offsetTop+16;\n    var x2=x(t.start), y2=16;\n    var svg=document.createElementNS(svgNS,'svg');\n    svg.setAttribute('class','dep');\n    var lo=Math.min(x1,x2)-20,hi=Math.max(x1,x2)+20,to=Math.min(y1,y2)-20,bo=Math.max(y1,y2)+20;\n    svg.setAttribute('viewBox',lo+' '+to+' '+(hi-lo)+' '+(bo-to));\n    svg.style.cssText='left:'+lo+'px;top:'+to+'px;width:'+(hi-lo)+'px;height:'+(bo-to)+'px';\n    var mid=(x1+x2)/2;\n    var p=document.createElementNS(svgNS,'path');\n    p.setAttribute('d','M'+x1+' '+y1+' C'+mid+' '+y1+', '+mid+' '+y2+', '+(x2-4)+' '+y2);\n    svg.appendChild(p);\n    track.appendChild(svg);\n  });\n\n  var legend=document.getElementById('legend');\n  legend.innerHTML=groups.filter(Boolean).map(function(g){\n    var col=(byGroup[g][0]||{}).color||ACCENT;\n    return '<span><i style=\"background:'+col+'\"></i>'+g.replace(/[<>&]/g,'')+'</span>';\n  }).join('')+'<span><i style=\"background:var(--today)\"></i>today</span>';\n\n  document.getElementById('range').textContent=iso(min)+' → '+iso(max)+' · '+days+' days';\n  var done=tasks.filter(function(t){return t.progress>=100;}).length;\n  var avg=tasks.length?Math.round(tasks.reduce(function(a,t){return a+t.progress;},0)/tasks.length):0;\n  document.getElementById('stats').textContent=done+'/'+tasks.length+' complete · '+avg+'% overall';\n})();\n</script>\n</body>\n</html>\n"}]}