{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"whiteboard","type":"registry:component","title":"Mini Whiteboard","description":"Freehand drawing, shapes and sticky notes inside a Notion page.","author":"Notion Arena","categories":["visuals"],"tags":["widget","pro","whiteboard","drawing","canvas","diagram"],"meta":{"kind":"widget","tier":"pro","version":"0.9.0","updatedAt":"2026-10-04","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/whiteboard","fileUrl":"https://notionarena.vercel.app/widgets/whiteboard.html","downloadUrl":"https://notionarena.vercel.app/api/widget/whiteboard","height":420,"minHeight":320,"maxHeight":800,"params":[{"key":"grid","label":"Show grid","type":"boolean","default":true},{"key":"accent","label":"Accent","type":"color","default":"#ad1a72"},{"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/whiteboard.html","type":"registry:file","target":"notion/widgets/whiteboard.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>Mini Whiteboard</title>\n<style>\n:root{--bg:#ffffff;--fg:#37352f;--muted:#787774;--surface:#f7f7f5;--border:#e9e9e7;--accent:#ad1a72;--canvas:#ffffff;--grid:#f0efed;\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:#202020;--border:#2f2f2f;--canvas:#1f1f1f;--grid:#282828}\n@media (prefers-color-scheme:dark){html[data-theme=\"auto\"]{--bg:#191919;--fg:#e6e6e4;--muted:#9b9a97;--surface:#202020;--border:#2f2f2f;--canvas:#1f1f1f;--grid:#282828}}\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}\n.bar{display:flex;gap:4px;align-items:center;padding:7px 10px;border-bottom:1px solid var(--border);background:var(--surface);flex-wrap:wrap}\n.grp{display:flex;gap:2px;background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:2px}\n.tool{border:none;background:transparent;color:var(--muted);border-radius:6px;width:26px;height:24px;cursor:pointer;font-size:13px;\ndisplay:flex;align-items:center;justify-content:center;padding:0;line-height:1}\n.tool:hover{color:var(--fg);background:var(--surface)}\n.tool.on{background:color-mix(in oklab,var(--accent) 16%,transparent);color:var(--accent)}\n.sep{width:1px;height:18px;background:var(--border);margin:0 4px}\n.swatches{display:flex;gap:3px;align-items:center}\n.sw{width:15px;height:15px;border-radius:50%;cursor:pointer;border:2px solid transparent}\n.sw.on{border-color:var(--fg)}\n.size{width:52px}\ninput[type=range]{-webkit-appearance:none;height:3px;border-radius:99px;background:var(--border);width:52px}\ninput[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:11px;height:11px;border-radius:50%;background:var(--accent);cursor:pointer}\nbutton.act{font:inherit;font-size:11px;border:1px solid var(--border);background:var(--bg);color:var(--muted);border-radius:7px;padding:3px 8px;cursor:pointer}\nbutton.act:hover{color:var(--fg)}\n.spacer{margin-left:auto}\n.stage{flex:1;min-height:0;position:relative;overflow:hidden;background:var(--canvas)}\n.stage.grid{background-image:radial-gradient(circle at 1px 1px,var(--grid) 1px,transparent 0);background-size:22px 22px}\ncanvas{position:absolute;inset:0;width:100%;height:100%;touch-action:none;cursor:crosshair}\n.hint{position:absolute;left:50%;bottom:12px;transform:translateX(-50%);font-size:11px;color:var(--muted);\nbackground:color-mix(in oklab,var(--bg) 88%,transparent);border:1px solid var(--border);border-radius:99px;padding:3px 11px;pointer-events:none;\nopacity:1;transition:opacity .4s}\n.hint.gone{opacity:0}\n.note{position:absolute;min-width:96px;max-width:190px;padding:8px 9px;border-radius:3px;font-size:12px;line-height:1.35;\nbackground:#fdf3b8;color:#37352f;box-shadow:0 2px 6px rgba(15,15,15,.14);cursor:move;outline:none;transform:rotate(-.6deg)}\n.note:focus{box-shadow:0 0 0 2px var(--accent)}\n.note .rm{position:absolute;top:-7px;right:-7px;width:16px;height:16px;border-radius:50%;background:var(--fg);color:var(--bg);\nfont-size:11px;line-height:16px;text-align:center;cursor:pointer;opacity:0}\n.note:hover .rm{opacity:.85}\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=\"bar\">\n    <div class=\"grp\" id=\"tools\">\n      <button class=\"tool on\" data-t=\"pen\" title=\"Pen (P)\">✎</button>\n      <button class=\"tool\" data-t=\"line\" title=\"Line (L)\">╱</button>\n      <button class=\"tool\" data-t=\"rect\" title=\"Rectangle (R)\">▭</button>\n      <button class=\"tool\" data-t=\"ellipse\" title=\"Ellipse (O)\">◯</button>\n      <button class=\"tool\" data-t=\"arrow\" title=\"Arrow (A)\">→</button>\n      <button class=\"tool\" data-t=\"note\" title=\"Sticky note (N)\">▤</button>\n      <button class=\"tool\" data-t=\"eraser\" title=\"Eraser (E)\">⌫</button>\n      <button class=\"tool\" data-t=\"pan\" title=\"Pan (H)\">✥</button>\n    </div>\n    <div class=\"sep\"></div>\n    <div class=\"swatches\" id=\"swatches\"></div>\n    <input type=\"range\" id=\"size\" class=\"size\" min=\"1\" max=\"16\" value=\"2.5\" title=\"Stroke width\" />\n    <div class=\"sep\"></div>\n    <button class=\"act\" id=\"undo\" title=\"Undo (⌘Z)\">↶</button>\n    <button class=\"act\" id=\"redo\" title=\"Redo\">↷</button>\n    <div class=\"spacer\"></div>\n    <button class=\"act\" id=\"png\">PNG</button>\n    <button class=\"act\" id=\"json\">JSON</button>\n    <button class=\"act\" id=\"clear\">Clear</button>\n  </div>\n  <div class=\"stage grid\" id=\"stage\">\n    <canvas id=\"cv\"></canvas>\n    <div class=\"hint\" id=\"hint\">Pick a tool and draw · strokes save to this browser only</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 bool(k,d){var v=c[k];return v===undefined?d:(v===true||v==='true'||v==='1');}\n\n  var ACCENT=c.accent||'#ad1a72';\n  if(c.theme)document.documentElement.setAttribute('data-theme',c.theme);\n  document.documentElement.style.setProperty('--accent',ACCENT);\n  if(!bool('grid',true))document.getElementById('stage').classList.remove('grid');\n\n  var COLORS=['#37352f','#e03e3e','#d9730d','#dfab01','#0f7b6c','#2383e2','#6940a5','#ad1a72'];\n  var dark=document.documentElement.getAttribute('data-theme')==='dark'||\n    (document.documentElement.getAttribute('data-theme')==='auto'&&window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches);\n  if(dark)COLORS[0]='#e6e6e4';\n\n  var sw=document.getElementById('swatches');\n  var color=COLORS[0];\n  COLORS.forEach(function(col,i){\n    var b=document.createElement('span');\n    b.className='sw'+(i===0?' on':'');b.style.background=col;b.title=col;\n    b.addEventListener('click',function(){\n      color=col;sw.querySelectorAll('.sw').forEach(function(n){n.classList.remove('on');});b.classList.add('on');\n    });\n    sw.appendChild(b);\n  });\n\n  var stage=document.getElementById('stage'),cv=document.getElementById('cv'),ctx=cv.getContext('2d');\n  var tool='pen',size=2.5;\n  var shapes=[],redoStack=[],notes=[];\n  var drawing=false,last=null,preview=null,pan={x:0,y:0},panning=false,panStart=null;\n\n  var KEY='notion-arena.whiteboard.v1';\n  try{\n    var s=JSON.parse(localStorage.getItem(KEY)||'null');\n    if(s&&typeof s==='object'){shapes=s.shapes||[];notes=s.notes||[];pan=s.pan||pan;}\n  }catch(e){}\n  function save(){try{localStorage.setItem(KEY,JSON.stringify({shapes:shapes,notes:notes,pan:pan}));}catch(e){}}\n\n  function resize(){\n    var r=stage.getBoundingClientRect(),dpr=Math.min(window.devicePixelRatio||1,2);\n    cv.width=Math.max(1,Math.floor(r.width*dpr));cv.height=Math.max(1,Math.floor(r.height*dpr));\n    ctx.setTransform(dpr,0,0,dpr,0,0);\n    paint();\n  }\n  function arrowHead(x1,y1,x2,y2,col,w){\n    var a=Math.atan2(y2-y1,x2-x1),L=6+w*1.6;\n    ctx.beginPath();\n    ctx.moveTo(x2,y2);\n    ctx.lineTo(x2-L*Math.cos(a-0.42),y2-L*Math.sin(a-0.42));\n    ctx.lineTo(x2-L*Math.cos(a+0.42),y2-L*Math.sin(a+0.42));\n    ctx.closePath();ctx.fillStyle=col;ctx.fill();\n  }\n  function drawShape(s){\n    ctx.save();\n    ctx.translate(pan.x,pan.y);\n    ctx.strokeStyle=s.color;ctx.fillStyle=s.color;ctx.lineWidth=s.w;ctx.lineCap='round';ctx.lineJoin='round';\n    if(s.t==='pen'){\n      ctx.beginPath();\n      var pts=s.p;if(!pts||pts.length<2){if(pts&&pts.length===1){ctx.arc(pts[0][0],pts[0][1],s.w/2,0,7);ctx.fill();}ctx.restore();return;}\n      ctx.moveTo(pts[0][0],pts[0][1]);\n      for(var i=1;i<pts.length-1;i++){\n        var mx=(pts[i][0]+pts[i+1][0])/2,my=(pts[i][1]+pts[i+1][1])/2;\n        ctx.quadraticCurveTo(pts[i][0],pts[i][1],mx,my);\n      }\n      ctx.lineTo(pts[pts.length-1][0],pts[pts.length-1][1]);\n      ctx.stroke();\n    }else if(s.t==='line'){\n      ctx.beginPath();ctx.moveTo(s.x1,s.y1);ctx.lineTo(s.x2,s.y2);ctx.stroke();\n    }else if(s.t==='arrow'){\n      ctx.beginPath();ctx.moveTo(s.x1,s.y1);ctx.lineTo(s.x2,s.y2);ctx.stroke();\n      arrowHead(s.x1,s.y1,s.x2,s.y2,s.color,s.w);\n    }else if(s.t==='rect'){\n      ctx.beginPath();ctx.rect(Math.min(s.x1,s.x2),Math.min(s.y1,s.y2),Math.abs(s.x2-s.x1),Math.abs(s.y2-s.y1));ctx.stroke();\n    }else if(s.t==='ellipse'){\n      var cx=(s.x1+s.x2)/2,cy=(s.y1+s.y2)/2,rx=Math.abs(s.x2-s.x1)/2,ry=Math.abs(s.y2-s.y1)/2;\n      ctx.beginPath();ctx.ellipse(cx,cy,Math.max(rx,.5),Math.max(ry,.5),0,0,Math.PI*2);ctx.stroke();\n    }else if(s.t==='eraser'){\n      ctx.save();ctx.globalCompositeOperation='destination-out';ctx.lineWidth=s.w*4;\n      ctx.beginPath();ctx.moveTo(s.x1,s.y1);ctx.lineTo(s.x2,s.y2);ctx.stroke();ctx.restore();\n    }\n    ctx.restore();\n  }\n  function paint(){\n    ctx.clearRect(0,0,cv.width,cv.height);\n    shapes.forEach(drawShape);\n    if(preview)drawShape(preview);\n  }\n  function renderNotes(){\n    stage.querySelectorAll('.note').forEach(function(n){n.remove();});\n    notes.forEach(function(n,i){\n      var el=document.createElement('div');\n      el.className='note';el.contentEditable='true';el.spellcheck=false;\n      el.style.left=(n.x+pan.x)+'px';el.style.top=(n.y+pan.y)+'px';\n      if(n.color)el.style.background=n.color;\n      el.textContent=n.text||'';\n      el.addEventListener('input',function(){notes[i].text=el.textContent;save();});\n      el.addEventListener('blur',function(){save();});\n      var rm=document.createElement('span');rm.className='rm';rm.textContent='×';\n      rm.addEventListener('click',function(e){e.stopPropagation();notes.splice(i,1);save();renderNotes();});\n      el.appendChild(rm);\n      var dragging=false,ox=0,oy=0;\n      el.addEventListener('pointerdown',function(e){\n        if(e.target===rm)return;\n        if(document.activeElement===el&&e.detail>1)return;\n        dragging=true;ox=e.clientX-el.offsetLeft;oy=e.clientY-el.offsetTop;\n        el.setPointerCapture(e.pointerId);el.blur();\n      });\n      el.addEventListener('pointermove',function(e){\n        if(!dragging)return;\n        el.style.left=(e.clientX-ox)+'px';el.style.top=(e.clientY-oy)+'px';\n        notes[i].x=e.clientX-ox-pan.x;notes[i].y=e.clientY-oy-pan.y;\n      });\n      el.addEventListener('pointerup',function(){dragging=false;save();});\n      stage.appendChild(el);\n    });\n  }\n  function pos(e){var r=cv.getBoundingClientRect();return [e.clientX-r.left-pan.x,e.clientY-r.top-pan.y];}\n\n  cv.addEventListener('pointerdown',function(e){\n    cv.setPointerCapture(e.pointerId);\n    var hint=document.getElementById('hint');hint.classList.add('gone');\n    var p=pos(e);\n    if(tool==='pan'||e.button===1||e.shiftKey){\n      panning=true;panStart=[e.clientX-pan.x,e.clientY-pan.y];cv.style.cursor='grabbing';return;\n    }\n    if(tool==='note'){\n      notes.push({x:p[0],y:p[1],text:'Note',color:dark?'#4a4433':'#fdf3b8'});\n      save();renderNotes();return;\n    }\n    drawing=true;redoStack=[];\n    if(tool==='pen')preview={t:'pen',p:[[p[0],p[1]]],color:color,w:size};\n    else preview={t:tool,x1:p[0],y1:p[1],x2:p[0],y2:p[1],color:color,w:size};\n    last=p;paint();\n  });\n  cv.addEventListener('pointermove',function(e){\n    if(panning){pan.x=e.clientX-panStart[0];pan.y=e.clientY-panStart[1];paint();renderNotes();return;}\n    if(!drawing||!preview)return;\n    var p=pos(e);\n    if(preview.t==='pen'){\n      var lp=preview.p[preview.p.length-1];\n      if(Math.hypot(p[0]-lp[0],p[1]-lp[1])>1.2)preview.p.push([p[0],p[1]]);\n    }else{preview.x2=p[0];preview.y2=p[1];}\n    paint();\n  });\n  function end(){\n    if(panning){panning=false;cv.style.cursor=tool==='pan'?'grab':'crosshair';save();return;}\n    if(!drawing)return;\n    drawing=false;\n    if(preview){shapes.push(preview);preview=null;}\n    save();paint();\n  }\n  cv.addEventListener('pointerup',end);\n  cv.addEventListener('pointercancel',end);\n  cv.addEventListener('pointerleave',end);\n\n  document.getElementById('tools').addEventListener('click',function(e){\n    var b=e.target.closest('.tool');if(!b)return;\n    tool=b.dataset.t;\n    document.querySelectorAll('.tool').forEach(function(n){n.classList.toggle('on',n===b);});\n    cv.style.cursor=tool==='pan'?'grab':'crosshair';\n  });\n  document.getElementById('size').addEventListener('input',function(e){size=parseFloat(e.target.value)||2;});\n  function undo(){if(!shapes.length)return;redoStack.push(shapes.pop());save();paint();}\n  function redo(){if(!redoStack.length)return;shapes.push(redoStack.pop());save();paint();}\n  document.getElementById('undo').addEventListener('click',undo);\n  document.getElementById('redo').addEventListener('click',redo);\n  document.getElementById('clear').addEventListener('click',function(){\n    if(!confirm('Clear the whole board?'))return;\n    shapes=[];notes=[];redoStack=[];pan={x:0,y:0};save();paint();renderNotes();\n  });\n  document.getElementById('png').addEventListener('click',function(){\n    try{\n      var url=cv.toDataURL('image/png');\n      var a=document.createElement('a');a.href=url;a.download='whiteboard.png';a.click();\n    }catch(e){alert('Could not export PNG in this sandbox.');}\n  });\n  document.getElementById('json').addEventListener('click',function(e){\n    var b=e.currentTarget,txt=JSON.stringify({shapes:shapes,notes:notes},null,2);\n    function done(t){b.textContent=t;setTimeout(function(){b.textContent='JSON';},1400);}\n    try{navigator.clipboard.writeText(txt).then(function(){done('Copied ✓');},function(){done('See console');});}catch(err){done('See console');}\n    try{console.log(txt);}catch(err){}\n  });\n  document.addEventListener('keydown',function(e){\n    if(e.target&&e.target.isContentEditable)return;\n    var k=e.key.toLowerCase();\n    if((e.metaKey||e.ctrlKey)&&k==='z'){e.preventDefault();e.shiftKey?redo():undo();return;}\n    var map={p:'pen',l:'line',r:'rect',o:'ellipse',a:'arrow',n:'note',e:'eraser',h:'pan'};\n    if(map[k]){var btn=document.querySelector('.tool[data-t=\"'+map[k]+'\"]');if(btn)btn.click();}\n  });\n  window.addEventListener('resize',resize);\n  resize();renderNotes();\n  setTimeout(function(){document.getElementById('hint').classList.add('gone');},5200);\n})();\n</script>\n</body>\n</html>\n"}]}