{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"flashcards","type":"registry:component","title":"Flashcards","description":"A focused question-and-answer deck with active-recall progress.","author":"Notion Arena","categories":["docs"],"tags":["widget","free","flashcards","study","learning","active recall","education"],"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":true,"writesToNotion":false,"needsNetwork":false},"embedUrl":"https://notionarena.vercel.app/embed/flashcards","fileUrl":"https://notionarena.vercel.app/widgets/flashcards.html","downloadUrl":"https://notionarena.vercel.app/api/widget/flashcards","height":310,"minHeight":220,"maxHeight":500,"params":[{"key":"deck","label":"Deck name","type":"text","default":"Quick Review"},{"key":"cards","label":"Cards","type":"text","default":"","help":"Separate cards with || and front from back with ::. Example: Prompt::Answer||Prompt 2::Answer 2. Up to 100 cards."},{"key":"shuffle","label":"Shuffle cards","type":"boolean","default":false},{"key":"accent","label":"Accent","type":"color","default":"#0075de"},{"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/flashcards.html","type":"registry:file","target":"notion/widgets/flashcards.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>Flashcards</title>\n<style>\n:root{--bg:#ffffff;--fg:#37352f;--muted:#787774;--surface:#f7f7f5;--border:#e9e9e7;--accent:#0075de;--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:220px;display:flex;flex-direction:column;gap:10px;padding:14px;overflow:auto}\n.head{display:flex;align-items:center;justify-content:space-between;gap:10px}\n.kicker{font-size:9.5px;text-transform:uppercase;letter-spacing:.09em;color:var(--muted);font-weight:650}\n.title{margin-top:2px;font-size:14px;font-weight:650;letter-spacing:-.02em}\n.counter{font-size:11px;color:var(--muted);font-variant-numeric:tabular-nums;white-space:nowrap}\n.progress{height:5px;width:100%;overflow:hidden;border-radius:99px;background:var(--track)}\n.progress i{display:block;height:100%;width:0;border-radius:inherit;background:var(--accent);transition:width .2s ease}\n.stage{display:flex;flex:1;min-height:120px;align-items:stretch}\n.card{position:relative;display:flex;width:100%;min-height:126px;flex-direction:column;align-items:stretch;justify-content:center;gap:12px;border:1px solid var(--border);border-radius:12px;background:var(--surface);color:var(--fg);padding:16px 18px;text-align:left;cursor:pointer;transition:border-color .15s,background .15s}\n.card:hover{border-color:color-mix(in oklab,var(--accent) 48%,var(--border))}\n.card:focus-visible{outline:2px solid var(--accent);outline-offset:2px}\n.card.complete{align-items:center;text-align:center;background:color-mix(in oklab,var(--accent) 8%,var(--surface))}\n.side{font-size:9.5px;text-transform:uppercase;letter-spacing:.09em;color:var(--accent);font-weight:700}\n.prompt{font-size:16px;font-weight:550;line-height:1.5;letter-spacing:-.01em;overflow-wrap:anywhere}\n.helper{font-size:10.5px;color:var(--muted)}\n.controls{display:flex;align-items:center;gap:6px;flex-wrap:wrap}\nbutton{min-height:30px;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:var(--fg);font:inherit;font-size:11.5px;font-weight:550;padding:5px 10px;cursor:pointer;transition:border-color .15s,opacity .15s}\nbutton:hover{border-color:color-mix(in oklab,var(--accent) 45%,var(--border))}\nbutton:focus-visible{outline:2px solid var(--accent);outline-offset:2px}\nbutton:disabled{opacity:.45;cursor:not-allowed}\nbutton.primary{border-color:var(--accent);background:var(--accent);color:#fff}\nbutton.primary:hover{filter:brightness(.94)}\nbutton.again{color:var(--muted)}\n.spacer{flex:1}\n.stats{font-size:10.5px;color:var(--muted);font-variant-numeric:tabular-nums}\n.status{min-height:14px;font-size:10.5px;color:var(--muted)}\n@media(max-width:360px){.wrap{padding:11px}.card{padding:13px}.prompt{font-size:14px}}\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\">Active recall</div><div class=\"title\" id=\"title\">Quick Review</div></div>\n    <div class=\"counter\" id=\"counter\">Card 1 of 4</div>\n  </header>\n  <div class=\"progress\" id=\"progress\" role=\"progressbar\" aria-label=\"Deck progress\" aria-valuemin=\"0\" aria-valuemax=\"4\" aria-valuenow=\"0\"><i id=\"fill\"></i></div>\n  <main class=\"stage\">\n    <button class=\"card\" id=\"card\" type=\"button\" aria-expanded=\"false\">\n      <span class=\"side\" id=\"side\">Question</span>\n      <span class=\"prompt\" id=\"prompt\">Loading card</span>\n      <span class=\"helper\" id=\"helper\">Select to reveal the answer</span>\n    </button>\n  </main>\n  <div class=\"controls\">\n    <button id=\"previous\" type=\"button\" aria-label=\"Previous card\">Previous</button>\n    <button id=\"next\" type=\"button\" aria-label=\"Next card\">Next</button>\n    <span class=\"spacer\"></span>\n    <button class=\"again\" id=\"again\" type=\"button\" disabled>Review again</button>\n    <button class=\"primary\" id=\"know\" type=\"button\" disabled>I know this</button>\n    <button id=\"restart\" type=\"button\">Review all</button>\n  </div>\n  <div class=\"stats\" id=\"stats\"></div>\n  <div class=\"status\" id=\"status\" aria-live=\"polite\">Tap a card to reveal the answer.</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  function safeColor(v,d){return /^#[0-9a-f]{6}$/i.test(String(v||''))?String(v):d;}\n  var theme=['auto','light','dark'].indexOf(String(c.theme||'auto'))>-1?String(c.theme||'auto'):'auto';\n  var ACCENT=safeColor(c.accent,'#0075de');\n  var DECK=String(c.deck||'Quick Review').slice(0,50);\n  var raw=String(c.cards||'');\n  var DEFAULT=[\n    {front:'What are Notion pages built from?',back:'Blocks. Text, headings, embeds, database views, and other content are all blocks.'},\n    {front:'What does a database relation connect?',back:'Records in separate databases, so related information can be viewed together.'},\n    {front:'What is active recall?',back:'Trying to retrieve an idea from memory before looking at the answer.'},\n    {front:'What makes a useful study card?',back:'One clear prompt with one focused answer, not a whole page of notes.'}\n  ];\n  var cards=[];\n  if(raw){\n    raw.split('||').forEach(function(entry){\n      var cut=entry.indexOf('::');\n      if(cut<0)return;\n      var front=entry.slice(0,cut).trim(),back=entry.slice(cut+2).trim();\n      if(front&&back)cards.push({front:front,back:back});\n    });\n  }\n  if(!cards.length)cards=DEFAULT.slice(0);\n  cards=cards.slice(0,100);\n  var SHUFFLE=bool('shuffle',false);\n  if(theme)document.documentElement.setAttribute('data-theme',theme);\n  document.documentElement.style.setProperty('--accent',ACCENT);\n  document.getElementById('title').textContent=DECK;\n  document.title=DECK;\n\n  var KEY='notion-arena.flashcards.v1';\n  function fingerprint(value){var h=5381;for(var i=0;i<value.length;i++)h=((h<<5)-h+value.charCodeAt(i))|0;return 'd'+(h>>>0).toString(36);}\n  var deckId=fingerprint(DECK+'\\n'+cards.map(function(card){return card.front+'\\n'+card.back;}).join('\\n'));\n  var store={version:1,decks:{}};\n  try{var saved=JSON.parse(localStorage.getItem(KEY)||'{}');if(saved&&saved.decks&&typeof saved.decks==='object')store=saved;}catch(e){}\n  var state=store.decks[deckId];\n  function makeOrder(){\n    var out=[];for(var i=0;i<cards.length;i++)out.push(i);\n    if(SHUFFLE){\n      var seed=parseInt(deckId.slice(1),36)||1;\n      function random(){seed=(seed*1664525+1013904223)>>>0;return seed/4294967296;}\n      for(var j=out.length-1;j>0;j--){var pick=Math.floor(random()*(j+1)),tmp=out[j];out[j]=out[pick];out[pick]=tmp;}\n    }\n    return out;\n  }\n  if(!state||typeof state!=='object')state={index:0,known:[],again:0,order:makeOrder()};\n  function validOrder(order){\n    if(!Array.isArray(order)||order.length!==cards.length)return false;\n    var seen=[];\n    for(var i=0;i<order.length;i++){\n      if(!Number.isInteger(order[i])||order[i]<0||order[i]>=cards.length||seen.indexOf(order[i])>=0)return false;\n      seen.push(order[i]);\n    }\n    return true;\n  }\n  if(!validOrder(state.order)||state.shuffle!==SHUFFLE)state.order=makeOrder();\n  state.shuffle=SHUFFLE;\n  if(!Array.isArray(state.known))state.known=[];\n  var knownSeen=[];\n  state.known=state.known.filter(function(n){\n    if(!Number.isInteger(n)||n<0||n>=cards.length||knownSeen.indexOf(n)>=0)return false;\n    knownSeen.push(n);return true;\n  });\n  state.again=Math.max(0,parseInt(state.again,10)||0);\n  state.index=Math.max(0,Math.min(cards.length-1,parseInt(state.index,10)||0));\n  var revealed=false;\n  var cardButton=document.getElementById('card'),side=document.getElementById('side'),prompt=document.getElementById('prompt');\n  var helper=document.getElementById('helper'),counter=document.getElementById('counter'),fill=document.getElementById('fill');\n  var progress=document.getElementById('progress'),stats=document.getElementById('stats'),statusEl=document.getElementById('status');\n  progress.setAttribute('aria-valuemax',String(cards.length));\n  var againButton=document.getElementById('again'),knowButton=document.getElementById('know'),previousButton=document.getElementById('previous'),nextButton=document.getElementById('next');\n  function save(){\n    store.decks[deckId]=state;\n    var ids=Object.keys(store.decks);\n    while(ids.length>8){var old=ids.shift();if(old!==deckId)delete store.decks[old];}\n    try{localStorage.setItem(KEY,JSON.stringify(store));}catch(e){}\n  }\n  function currentOriginalIndex(){return state.order[state.index];}\n  function countKnown(){return state.known.length;}\n  function nextUnlearned(from){\n    for(var step=1;step<=cards.length;step++){\n      var pos=(from+step)%cards.length,original=state.order[pos];\n      if(state.known.indexOf(original)<0)return pos;\n    }\n    return from;\n  }\n  function render(){\n    var mastered=countKnown(),done=mastered>=cards.length;\n    if(done){\n      cardButton.classList.add('complete');cardButton.setAttribute('aria-expanded','true');\n      side.textContent='Deck complete';prompt.textContent='You know every card.';\n      helper.textContent='Use Review all to start another pass.';\n      counter.textContent=cards.length+' of '+cards.length+' mastered';\n      fill.style.width='100%';progress.setAttribute('aria-valuenow',String(cards.length));\n      cardButton.disabled=true;previousButton.disabled=true;nextButton.disabled=true;\n      againButton.disabled=true;knowButton.disabled=true;\n      statusEl.textContent='Nice work. Your progress is saved on this device.';\n    }else{\n      var original=currentOriginalIndex(),item=cards[original];\n      cardButton.classList.remove('complete');cardButton.disabled=false;\n      cardButton.setAttribute('aria-expanded',revealed?'true':'false');\n      side.textContent=revealed?'Answer':'Question';prompt.textContent=revealed?item.back:item.front;\n      helper.textContent=revealed?'Select to hide the answer':'Select to reveal the answer';\n      counter.textContent='Card '+(state.index+1)+' of '+cards.length;\n      fill.style.width=(mastered/cards.length*100).toFixed(1)+'%';progress.setAttribute('aria-valuenow',String(mastered));\n      previousButton.disabled=state.index===0;nextButton.disabled=state.index>=cards.length-1;\n      againButton.disabled=!revealed;knowButton.disabled=!revealed;\n      statusEl.textContent=revealed?'Choose Review again or I know this.':'Tap a card to reveal the answer.';\n    }\n    stats.textContent=mastered+' mastered · '+state.again+' review attempts';\n  }\n  function moveTo(position){state.index=(position+cards.length)%cards.length;revealed=false;save();render();}\n  cardButton.addEventListener('click',function(){if(cardButton.disabled)return;revealed=!revealed;render();});\n  previousButton.addEventListener('click',function(){moveTo(Math.max(0,state.index-1));});\n  nextButton.addEventListener('click',function(){moveTo(Math.min(cards.length-1,state.index+1));});\n  againButton.addEventListener('click',function(){\n    var original=currentOriginalIndex(),at=state.known.indexOf(original);\n    if(at>=0)state.known.splice(at,1);\n    state.again++;state.index=nextUnlearned(state.index);revealed=false;save();render();\n  });\n  knowButton.addEventListener('click',function(){\n    var original=currentOriginalIndex();\n    if(state.known.indexOf(original)<0)state.known.push(original);\n    state.index=nextUnlearned(state.index);revealed=false;save();render();\n  });\n  document.getElementById('restart').addEventListener('click',function(){state.known=[];state.index=0;revealed=false;save();render();});\n  save();render();\n})();\n</script>\n</body>\n</html>\n"}]}