{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"month-calendar","type":"registry:component","title":"Month Calendar","description":"A navigable month grid with a small static agenda for key dates.","author":"Notion Arena","categories":["time"],"tags":["widget","free","calendar","monthly","events","dates","schedule"],"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":false,"writesToNotion":false,"needsNetwork":false},"embedUrl":"https://notionarena.vercel.app/embed/month-calendar","fileUrl":"https://notionarena.vercel.app/widgets/month-calendar.html","downloadUrl":"https://notionarena.vercel.app/api/widget/month-calendar","height":360,"minHeight":270,"maxHeight":480,"params":[{"key":"label","label":"Widget title","type":"text","default":"Calendar"},{"key":"month","label":"Starting month","type":"text","default":"","help":"Optional ISO month, YYYY-MM. Blank starts at the viewer's current month."},{"key":"events","label":"Event notes","type":"text","default":"","help":"Optional, up to 80: 2026-10-11::Release||2026-10-20::Exam. Static, not synced. Avoid sensitive details in public embed URLs."},{"key":"weekStart","label":"Week starts on","type":"select","default":"mon","options":[{"value":"mon","label":"Monday"},{"value":"sun","label":"Sunday"}]},{"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/month-calendar.html","type":"registry:file","target":"notion/widgets/month-calendar.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>Month Calendar</title>\n<style>\n:root{--bg:#ffffff;--fg:#37352f;--muted:#787774;--surface:#f7f7f5;--border:#e9e9e7;--accent:#2383e2;--track:#e9e9e7;--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;--track:#343434}\n@media (prefers-color-scheme:dark){html[data-theme=\"auto\"]{--bg:#191919;--fg:#e6e6e4;--muted:#9b9a97;--surface:#252525;--border:#343434;--track:#343434}}\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%;min-height:270px;display:flex;flex-direction:column;gap:9px;padding:13px;overflow:auto}\n.head{display:flex;align-items:center;justify-content:space-between;gap:10px;min-height:30px}\n.kicker{font-size:9px;text-transform:uppercase;letter-spacing:.09em;color:var(--muted);font-weight:650}\n.title{margin-top:1px;font-size:13.5px;font-weight:650;letter-spacing:-.02em}\n.month-controls{display:flex;align-items:center;gap:4px}\n.month-label{min-width:94px;text-align:center;font-size:12px;font-weight:600;font-variant-numeric:tabular-nums}\nbutton{font-family:inherit;color:var(--fg);cursor:pointer}\n.nav{display:grid;place-items:center;width:27px;height:27px;border:1px solid var(--border);border-radius:7px;background:var(--surface);font-size:14px;line-height:1;transition:border-color .15s,background .15s}\n.nav:hover,.today:hover{border-color:color-mix(in oklab,var(--accent) 45%,var(--border))}\n.nav:focus-visible,.today:focus-visible,.day:focus-visible{outline:2px solid var(--accent);outline-offset:2px}\n.today{height:25px;padding:0 7px;border:1px solid var(--border);border-radius:7px;background:var(--surface);font-size:10px;font-weight:550;transition:border-color .15s}\n.calendar{display:flex;flex-direction:column;gap:4px}\n.weekdays,.days{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:3px}\n.weekday{display:grid;place-items:center;height:17px;color:var(--muted);font-size:9px;font-weight:600}\n.day{position:relative;display:grid;place-items:center;width:100%;height:27px;border:1px solid transparent;border-radius:7px;background:transparent;font-size:11px;font-weight:500;font-variant-numeric:tabular-nums;transition:background .12s,border-color .12s,color .12s}\n.day:hover{background:var(--surface);border-color:var(--border)}\n.day.outside{color:var(--muted);opacity:.48}\n.day.today-day{border-color:color-mix(in oklab,var(--accent) 58%,var(--border));font-weight:700}\n.day.selected{background:var(--accent);color:#fff;border-color:var(--accent);font-weight:700}\n.day.has-events::after{content:\"\";position:absolute;bottom:3px;width:3px;height:3px;border-radius:50%;background:currentColor}\n.agenda{display:flex;min-height:52px;flex:1;flex-direction:column;gap:5px;border-top:1px solid var(--border);padding-top:7px}\n.agenda-head{display:flex;align-items:baseline;justify-content:space-between;gap:8px}\n.selected-date{font-size:11px;font-weight:650}\n.event-count{color:var(--muted);font-size:10px;font-variant-numeric:tabular-nums}\n.events{display:flex;flex-direction:column;gap:3px;margin:0;padding:0;list-style:none;overflow:auto}\n.events li{display:flex;align-items:flex-start;gap:7px;min-height:20px;padding:3px 6px;border-radius:6px;background:var(--surface);font-size:10.5px;line-height:1.35;overflow-wrap:anywhere}\n.events li::before{content:\"\";width:6px;height:6px;margin-top:3px;flex:0 0 auto;border-radius:50%;background:var(--accent)}\n.empty{color:var(--muted);font-size:10.5px;line-height:1.4}\n@media(max-width:420px){.wrap{padding:10px;gap:7px}.head{align-items:flex-start}.month-controls{gap:2px}.nav{width:25px;height:25px}.month-label{min-width:78px;font-size:11px}.today{padding:0 5px}.weekdays,.days{gap:2px}.day{height:24px;font-size:10px}}\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  <header class=\"head\">\n    <div><div class=\"kicker\">Static month view</div><div class=\"title\" id=\"title\">Calendar</div></div>\n    <div class=\"month-controls\">\n      <button class=\"nav\" id=\"previous\" type=\"button\" aria-label=\"Previous month\">‹</button>\n      <div class=\"month-label\" id=\"monthLabel\" aria-live=\"polite\"></div>\n      <button class=\"nav\" id=\"next\" type=\"button\" aria-label=\"Next month\">›</button>\n      <button class=\"today\" id=\"today\" type=\"button\">Today</button>\n    </div>\n  </header>\n  <section class=\"calendar\" aria-label=\"Calendar dates\">\n    <div class=\"weekdays\" id=\"weekdays\" aria-hidden=\"true\"></div>\n    <div class=\"days\" id=\"days\" role=\"group\" aria-label=\"Select a date\"></div>\n  </section>\n  <section class=\"agenda\" aria-label=\"Selected date events\">\n    <div class=\"agenda-head\"><span class=\"selected-date\" id=\"selectedDate\"></span><span class=\"event-count\" id=\"eventCount\"></span></div>\n    <ul class=\"events\" id=\"events\"></ul>\n    <div class=\"empty\" id=\"empty\" aria-live=\"polite\"></div>\n  </section>\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 safeColor(v,d){return /^#[0-9a-f]{6}$/i.test(String(v||''))?String(v):d;}\n  function dateKey(d){return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0');}\n  function validDate(value){\n    if(!/^\\d{4}-\\d{2}-\\d{2}$/.test(value))return false;\n    var p=value.split('-').map(Number),d=new Date(p[0],p[1]-1,p[2]);\n    return p[0]>=1000&&d.getFullYear()===p[0]&&d.getMonth()===p[1]-1&&d.getDate()===p[2];\n  }\n  var theme=['auto','light','dark'].indexOf(String(c.theme||'auto'))>-1?String(c.theme||'auto'):'auto';\n  var ACCENT=safeColor(c.accent,'#2383e2');\n  var LABEL=String(c.label||'Calendar').slice(0,40);\n  var WEEK_START=String(c.weekStart||'mon')==='sun'?0:1;\n  var rawMonth=String(c.month||''),now=new Date(),monthDate=new Date(now.getFullYear(),now.getMonth(),1);\n  var monthMatch=rawMonth.match(/^(\\d{4})-(0[1-9]|1[0-2])$/);\n  if(monthMatch&&Number(monthMatch[1])>=1000)monthDate=new Date(Number(monthMatch[1]),Number(monthMatch[2])-1,1);\n  var monthIsCurrent=now.getFullYear()===monthDate.getFullYear()&&now.getMonth()===monthDate.getMonth();\n  var selectedDate=monthIsCurrent?new Date(now.getFullYear(),now.getMonth(),now.getDate()):new Date(monthDate.getFullYear(),monthDate.getMonth(),1);\n  var selectedKey=dateKey(selectedDate),events=[];\n  String(c.events||'').split('||').slice(0,80).forEach(function(entry){\n    var cut=entry.indexOf('::');if(cut<0)return;\n    var date=entry.slice(0,cut).trim(),title=entry.slice(cut+2).trim();\n    if(validDate(date)&&title)events.push({date:date,title:title.slice(0,120)});\n  });\n  events.sort(function(a,b){return a.date.localeCompare(b.date)||a.title.localeCompare(b.title);});\n  if(theme)document.documentElement.setAttribute('data-theme',theme);\n  document.documentElement.style.setProperty('--accent',ACCENT);\n  document.getElementById('title').textContent=LABEL;\n  document.title=LABEL;\n\n  var weekdayRow=document.getElementById('weekdays'),days=document.getElementById('days');\n  var monthLabel=document.getElementById('monthLabel'),selectedLabel=document.getElementById('selectedDate');\n  var countLabel=document.getElementById('eventCount'),eventList=document.getElementById('events'),empty=document.getElementById('empty');\n  var weekdayBase=new Date(2024,0,7+WEEK_START);\n  for(var w=0;w<7;w++){\n    var weekday=document.createElement('div');weekday.className='weekday';\n    weekday.textContent=new Date(weekdayBase.getFullYear(),weekdayBase.getMonth(),weekdayBase.getDate()+w).toLocaleDateString(undefined,{weekday:'short'}).slice(0,2);\n    weekdayRow.appendChild(weekday);\n  }\n  function eventsOn(key){return events.filter(function(event){return event.date===key;});}\n  function sameMonth(a,b){return a.getFullYear()===b.getFullYear()&&a.getMonth()===b.getMonth();}\n  function drawAgenda(){\n    var date=new Date(Number(selectedKey.slice(0,4)),Number(selectedKey.slice(5,7))-1,Number(selectedKey.slice(8,10)));\n    var matches=eventsOn(selectedKey);\n    selectedLabel.textContent=date.toLocaleDateString(undefined,{weekday:'long',month:'long',day:'numeric'});\n    countLabel.textContent=matches.length+(matches.length===1?' event':' events');\n    while(eventList.firstChild)eventList.removeChild(eventList.firstChild);\n    matches.forEach(function(event){var item=document.createElement('li');item.textContent=event.title;eventList.appendChild(item);});\n    empty.textContent=matches.length?'':(events.length?'No events on this date.':'Add dates with the events setting to show them here.');\n    empty.hidden=matches.length>0;\n  }\n  function render(refocusDay){\n    monthLabel.textContent=monthDate.toLocaleDateString(undefined,{month:'long',year:'numeric'});\n    while(days.firstChild)days.removeChild(days.firstChild);\n    var first=new Date(monthDate.getFullYear(),monthDate.getMonth(),1);\n    var offset=(first.getDay()-WEEK_START+7)%7;\n    var gridStart=new Date(first.getFullYear(),first.getMonth(),first.getDate()-offset);\n    var nowDate=new Date(),todayKey=dateKey(nowDate);\n    for(var i=0;i<42;i++){\n      var cellDate=new Date(gridStart.getFullYear(),gridStart.getMonth(),gridStart.getDate()+i);\n      var key=dateKey(cellDate),outside=!sameMonth(cellDate,monthDate),count=eventsOn(key).length;\n      var button=document.createElement('button');button.type='button';button.className='day';\n      if(outside)button.className+=' outside';\n      if(key===todayKey)button.className+=' today-day';\n      if(key===selectedKey)button.className+=' selected';\n      if(count)button.className+=' has-events';\n      button.textContent=String(cellDate.getDate());\n      button.setAttribute('aria-pressed',key===selectedKey?'true':'false');\n      if(key===todayKey)button.setAttribute('aria-current','date');\n      button.setAttribute('aria-label',cellDate.toLocaleDateString(undefined,{weekday:'long',month:'long',day:'numeric',year:'numeric'})+(count?', '+count+' events':''));\n      button.addEventListener('click',(function(date,keyValue){return function(){selectedKey=keyValue;if(!sameMonth(date,monthDate))monthDate=new Date(date.getFullYear(),date.getMonth(),1);render(true);};})(cellDate,key));\n      days.appendChild(button);\n    }\n    drawAgenda();\n    if(refocusDay){var activeDay=days.querySelector('.day.selected');if(activeDay)activeDay.focus();}\n  }\n  document.getElementById('previous').addEventListener('click',function(){monthDate=new Date(monthDate.getFullYear(),monthDate.getMonth()-1,1);selectedKey=dateKey(new Date(monthDate.getFullYear(),monthDate.getMonth(),1));render();});\n  document.getElementById('next').addEventListener('click',function(){monthDate=new Date(monthDate.getFullYear(),monthDate.getMonth()+1,1);selectedKey=dateKey(new Date(monthDate.getFullYear(),monthDate.getMonth(),1));render();});\n  document.getElementById('today').addEventListener('click',function(){var today=new Date();monthDate=new Date(today.getFullYear(),today.getMonth(),1);selectedKey=dateKey(today);render();});\n  render();\n})();\n</script>\n</body>\n</html>\n"}]}