{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cookie-sheet",
  "type": "registry:block",
  "title": "Cookie Sheet",
  "description": "A consent sheet that rises on a spring and grows, on the same spring, into per-category switches whose thumbs stretch and squash as they travel. Drag or flick it down to keep essentials only; after a choice a small Privacy pill reopens it, and the choice can persist in localStorage.",
  "docs": "Cookie Sheet was added to public/vividsites/cookie-sheet.html. Paste its markup, <style> and <script> where you want it on the page (or open the file to see it alone). Customise it with the CSS variables at the top. Docs and live demo: https://vividsites.app/c/cookie-sheet",
  "categories": [
    "ui",
    "marketing-blocks"
  ],
  "files": [
    {
      "path": "components/vividsites/cookie-sheet.html",
      "type": "registry:file",
      "target": "public/vividsites/cookie-sheet.html",
      "content": "<!--\n  Cookie Sheet — vividsites.app\n  A consent sheet that rises from the bottom on a spring. \"Choose\" grows it, on the same spring,\n  into a list of categories with spring switches (the thumb stretches as it travels and squashes\n  as it lands). Drag the sheet down or flick it to close (that keeps essentials only); drag it up\n  and it rubber-bands. After a choice, a small \"Privacy\" pill stays in the corner to reopen it.\n\n  Markup: section.vs-cookie-sheet > .vs-cookie-sheet-panel > (.vs-cookie-sheet-short + .vs-cookie-sheet-long)\n          each category: .vs-cookie-sheet-row[data-cat=\"analytics\"] with a button.vs-cookie-sheet-switch\n  Options (data attributes on the section):\n    data-position=\"center|left|right\"   where the sheet sits\n    data-key=\"consent-v1\"               remember the choice in localStorage (and skip the sheet next visit)\n    data-delay=\"500\"                    ms before it rises on first load\n  JS: el.vsCookie.open({ expanded }) · .close() · .consent\n  Event: \"vs-cookie-sheet:change\" with detail { essential, analytics, marketing, personal }\n  CSS variables:\n    --vs-cookie-sheet-accent    primary button, switches, focus\n    --vs-cookie-sheet-on-accent label on the accent\n    --vs-cookie-sheet-bg        sheet fill\n    --vs-cookie-sheet-ink       text\n    --vs-cookie-sheet-muted     secondary text\n    --vs-cookie-sheet-line      hairlines\n    --vs-cookie-sheet-radius    sheet radius\n    --vs-cookie-sheet-width     max width\n    --vs-cookie-sheet-gap       distance from the viewport edge\n  Reduced motion: no springs or drag; the sheet fades in and out.\n-->\n<style>\n.vs-cookie-sheet {\n  --vs-cookie-sheet-accent: #0f5f56;\n  --vs-cookie-sheet-on-accent: #f3fbf8;\n  --vs-cookie-sheet-bg: #fdfdfb;\n  --vs-cookie-sheet-ink: #15201d;\n  --vs-cookie-sheet-muted: rgba(21,32,29,.6);\n  --vs-cookie-sheet-line: rgba(21,32,29,.1);\n  --vs-cookie-sheet-radius: 22px;\n  --vs-cookie-sheet-width: 30rem;\n  --vs-cookie-sheet-gap: 24px;\n  --vs-cookie-sheet-font: inherit;\n\n  position: fixed; z-index: 1000; bottom: var(--vs-cookie-sheet-gap); left: 50%;\n  width: min(var(--vs-cookie-sheet-width), calc(100vw - var(--vs-cookie-sheet-gap) * 2));\n  font-family: var(--vs-cookie-sheet-font); font-size: 1rem; color: var(--vs-cookie-sheet-ink);\n  background: var(--vs-cookie-sheet-bg); border-radius: var(--vs-cookie-sheet-radius);\n  box-shadow: 0 0 0 1px var(--vs-cookie-sheet-line), inset 0 1px 0 rgba(255,255,255,.8),\n    0 2px 4px rgba(10,20,18,.04), 0 12px 24px -8px rgba(10,20,18,.12), 0 40px 80px -24px rgba(10,20,18,.28);\n  transform: translate3d(-50%, 140%, 0); visibility: hidden; touch-action: none; will-change: transform;\n}\n.vs-cookie-sheet[data-position=\"left\"] { left: var(--vs-cookie-sheet-gap); }\n.vs-cookie-sheet[data-position=\"right\"] { left: auto; right: var(--vs-cookie-sheet-gap); }\n.vs-cookie-sheet, .vs-cookie-sheet * { box-sizing: border-box; }\n.vs-cookie-sheet.is-open { visibility: visible; }\n.vs-cookie-sheet-grip { display: block; width: 36px; height: 4px; margin: 8px auto 0; border-radius: 4px; background: var(--vs-cookie-sheet-line); cursor: grab; }\n.vs-cookie-sheet-panel { overflow: hidden; border-radius: inherit; }\n.vs-cookie-sheet-short, .vs-cookie-sheet-long { padding: 1.1rem 1.5rem 1.5rem; }\n.vs-cookie-sheet-long { display: none; padding-top: .5rem; }\n.vs-cookie-sheet.is-long .vs-cookie-sheet-long { display: block; }\n.vs-cookie-sheet.is-long .vs-cookie-sheet-short .vs-cookie-sheet-actions { display: none; }\n.vs-cookie-sheet.is-long .vs-cookie-sheet-short { padding-bottom: .9rem; }\n.vs-cookie-sheet-head { display: flex; gap: .9rem; align-items: flex-start; }\n.vs-cookie-sheet-icon { flex: none; width: 2.5rem; height: 2.5rem; border-radius: 12px; display: grid; place-items: center; color: var(--vs-cookie-sheet-accent);\n  background: color-mix(in srgb, var(--vs-cookie-sheet-accent) 10%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--vs-cookie-sheet-accent) 14%, transparent); }\n.vs-cookie-sheet-icon svg { width: 1.35rem; height: 1.35rem; }\n.vs-cookie-sheet h2 { margin: .15rem 0 .3rem; font-size: 1.0625rem; font-weight: 600; letter-spacing: -.012em; line-height: 1.3; }\n.vs-cookie-sheet p { margin: 0; font-size: .875rem; line-height: 1.5; color: var(--vs-cookie-sheet-muted); text-wrap: pretty; }\n.vs-cookie-sheet-actions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1.25rem; align-items: center; }\n.vs-cookie-sheet-btn {\n  display: inline-flex; align-items: center; justify-content: center; gap: .35em; height: 2.5rem; padding: 0 1.05rem; border: 0; border-radius: 12px;\n  font: inherit; font-size: .875rem; font-weight: 600; letter-spacing: -.005em; cursor: pointer; outline: none; -webkit-tap-highlight-color: transparent;\n  color: var(--vs-cookie-sheet-ink); background: color-mix(in srgb, var(--vs-cookie-sheet-ink) 6%, transparent);\n  transition: background-color .2s cubic-bezier(.2,0,0,1), transform .4s cubic-bezier(.3,1.5,.5,1), filter .2s;\n}\n.vs-cookie-sheet-btn:hover { background: color-mix(in srgb, var(--vs-cookie-sheet-ink) 10%, transparent); }\n.vs-cookie-sheet-btn:active { transform: scale(.96); transition-duration: .1s; }\n.vs-cookie-sheet-btn:focus-visible, .vs-cookie-sheet-more:focus-visible { box-shadow: 0 0 0 2px var(--vs-cookie-sheet-bg), 0 0 0 4px var(--vs-cookie-sheet-accent); }\n.vs-cookie-sheet-btn[data-primary] { color: var(--vs-cookie-sheet-on-accent); background: var(--vs-cookie-sheet-accent);\n  box-shadow: inset 0 1px 0 rgba(255,255,255,.18), 0 1px 2px rgba(0,0,0,.12), 0 6px 14px -8px var(--vs-cookie-sheet-accent); }\n.vs-cookie-sheet-btn[data-primary]:hover { filter: brightness(1.1); background: var(--vs-cookie-sheet-accent); }\n.vs-cookie-sheet-btn[data-primary]:focus-visible { box-shadow: 0 0 0 2px var(--vs-cookie-sheet-bg), 0 0 0 4px var(--vs-cookie-sheet-accent); }\n.vs-cookie-sheet-more { margin-left: auto; display: inline-flex; align-items: center; gap: .3em; padding: .5rem .25rem; border: 0; background: none; border-radius: 8px;\n  font: inherit; font-size: .875rem; font-weight: 600; color: var(--vs-cookie-sheet-ink); cursor: pointer; outline: none; opacity: .8; transition: opacity .2s; }\n.vs-cookie-sheet-more:hover { opacity: 1; }\n.vs-cookie-sheet-more svg { width: 1em; height: 1em; transition: transform .45s cubic-bezier(.3,1.5,.5,1); }\n.vs-cookie-sheet-more:hover svg { transform: translateY(2px); }\n/* category rows */\n.vs-cookie-sheet-rows { margin: 0 0 1.25rem; padding: 0; list-style: none; border-top: 1px solid var(--vs-cookie-sheet-line); }\n.vs-cookie-sheet-row { display: grid; grid-template-columns: 1fr auto; gap: .2rem 1rem; align-items: center; padding: .9rem 0; border-bottom: 1px solid var(--vs-cookie-sheet-line); }\n.vs-cookie-sheet-row b { font-size: .9375rem; font-weight: 600; }\n.vs-cookie-sheet-row p { grid-column: 1; font-size: .8125rem; }\n.vs-cookie-sheet-row .vs-cookie-sheet-switch { grid-column: 2; grid-row: 1 / span 2; }\n.vs-cookie-sheet-lock { font-size: .75rem; font-weight: 600; letter-spacing: .02em; color: var(--vs-cookie-sheet-muted); margin-right: .5rem; }\n/* spring switch */\n.vs-cookie-sheet-switch {\n  --w: 2.75rem; --h: 1.625rem; --p: 3px;\n  position: relative; flex: none; width: var(--w); height: var(--h); padding: 0; border: 0; border-radius: 999px; cursor: pointer; outline: none;\n  background: color-mix(in srgb, var(--vs-cookie-sheet-ink) 14%, transparent); transition: background-color .3s cubic-bezier(.2,0,0,1);\n  -webkit-tap-highlight-color: transparent; touch-action: manipulation;\n  box-shadow: inset 0 1px 2px rgba(0,0,0,.08);\n}\n.vs-cookie-sheet-switch[aria-checked=\"true\"] { background: var(--vs-cookie-sheet-accent); }\n.vs-cookie-sheet-switch:focus-visible { box-shadow: 0 0 0 2px var(--vs-cookie-sheet-bg), 0 0 0 4px var(--vs-cookie-sheet-accent); }\n.vs-cookie-sheet-switch:disabled { cursor: not-allowed; opacity: .55; }\n.vs-cookie-sheet-switch i {\n  position: absolute; left: var(--p); top: var(--p); width: calc(var(--h) - var(--p) * 2); height: calc(var(--h) - var(--p) * 2); border-radius: 999px;\n  background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.2), 0 2px 6px rgba(0,0,0,.12); transform-origin: 50% 50%; will-change: transform;\n}\n.vs-cookie-sheet-switch[aria-checked=\"true\"] i { transform: translateX(calc(var(--w) - var(--h))); }\n/* the reopen pill */\n.vs-cookie-sheet-reopen {\n  position: fixed; z-index: 999; left: var(--vs-cookie-sheet-gap); bottom: var(--vs-cookie-sheet-gap);\n  display: inline-flex; align-items: center; gap: .45em; height: 2.25rem; padding: 0 .9rem 0 .7rem; border: 0; border-radius: 999px;\n  font: 600 .8125rem/1 var(--vs-cookie-sheet-font); color: var(--vs-cookie-sheet-ink); background: var(--vs-cookie-sheet-bg); cursor: pointer; outline: none;\n  box-shadow: 0 0 0 1px var(--vs-cookie-sheet-line), 0 6px 16px -6px rgba(10,20,18,.25);\n  opacity: 0; transform: translateY(8px) scale(.9); pointer-events: none; visibility: hidden;\n  transition: opacity .3s cubic-bezier(.2,0,0,1), transform .5s cubic-bezier(.3,1.5,.5,1), visibility 0s .3s;\n}\n.vs-cookie-sheet-reopen.is-on { opacity: 1; transform: none; pointer-events: auto; visibility: visible; transition-delay: 0s; }\n.vs-cookie-sheet-reopen:hover { transform: translateY(-2px); }\n.vs-cookie-sheet-reopen:active { transform: scale(.95); transition-duration: .1s; }\n.vs-cookie-sheet-reopen:focus-visible { box-shadow: 0 0 0 1px var(--vs-cookie-sheet-line), 0 0 0 3px var(--vs-cookie-sheet-accent); }\n.vs-cookie-sheet-reopen svg { width: 1.05rem; height: 1.05rem; color: var(--vs-cookie-sheet-accent); }\n@media (max-width: 520px) {\n  .vs-cookie-sheet { --vs-cookie-sheet-gap: 8px; }\n  .vs-cookie-sheet-short, .vs-cookie-sheet-long { padding: 1rem 1.15rem 1.15rem; }\n  .vs-cookie-sheet-actions .vs-cookie-sheet-btn { flex: 1; }\n  .vs-cookie-sheet-more { margin: 0 auto; }\n  .vs-cookie-sheet-actions { gap: .4rem; }\n}\n</style>\n\n<section class=\"vs-cookie-sheet\" data-position=\"center\" role=\"dialog\" aria-modal=\"false\" aria-labelledby=\"vs-cookie-sheet-title\" aria-describedby=\"vs-cookie-sheet-text\">\n  <span class=\"vs-cookie-sheet-grip\" aria-hidden=\"true\"></span>\n  <div class=\"vs-cookie-sheet-panel\">\n    <div class=\"vs-cookie-sheet-short\">\n      <div class=\"vs-cookie-sheet-head\">\n        <span class=\"vs-cookie-sheet-icon\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20.5 12.5A8.5 8.5 0 1 1 11.5 3.5a3 3 0 0 0 4 3.6 3 3 0 0 0 5 5.4z\"/><circle cx=\"8.5\" cy=\"10\" r=\".9\" fill=\"currentColor\"/><circle cx=\"12\" cy=\"15.5\" r=\".9\" fill=\"currentColor\"/><circle cx=\"15.5\" cy=\"12\" r=\".6\" fill=\"currentColor\"/><circle cx=\"8\" cy=\"15\" r=\".6\" fill=\"currentColor\"/></svg></span>\n        <div>\n          <h2 id=\"vs-cookie-sheet-title\">A few cookies, with your say-so</h2>\n          <p id=\"vs-cookie-sheet-text\">Essential ones keep you signed in. With your OK we also count which pages help and which don’t. We never sell what we learn.</p>\n        </div>\n      </div>\n      <div class=\"vs-cookie-sheet-actions\">\n        <button class=\"vs-cookie-sheet-btn\" type=\"button\" data-primary data-act=\"all\">Accept all</button>\n        <button class=\"vs-cookie-sheet-btn\" type=\"button\" data-act=\"none\">Essentials only</button>\n        <button class=\"vs-cookie-sheet-more\" type=\"button\" aria-expanded=\"false\" data-act=\"more\">Choose<svg viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path d=\"M4 6l4 4 4-4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg></button>\n      </div>\n    </div>\n    <div class=\"vs-cookie-sheet-long\">\n      <ul class=\"vs-cookie-sheet-rows\">\n        <li class=\"vs-cookie-sheet-row\" data-cat=\"essential\"><b id=\"vs-cs-e\">Essential</b><p>Sign-in, security and your cart. The site breaks without these.</p><span style=\"grid-column:2;grid-row:1/span 2;display:flex;align-items:center\"><span class=\"vs-cookie-sheet-lock\">Always on</span><button class=\"vs-cookie-sheet-switch\" type=\"button\" role=\"switch\" aria-checked=\"true\" aria-labelledby=\"vs-cs-e\" disabled><i></i></button></span></li>\n        <li class=\"vs-cookie-sheet-row\" data-cat=\"analytics\"><b id=\"vs-cs-a\">Analytics</b><p>Anonymous page counts and load times, kept for 90 days.</p><button class=\"vs-cookie-sheet-switch\" type=\"button\" role=\"switch\" aria-checked=\"false\" aria-labelledby=\"vs-cs-a\"><i></i></button></li>\n        <li class=\"vs-cookie-sheet-row\" data-cat=\"marketing\"><b id=\"vs-cs-m\">Marketing</b><p>Tells us which campaigns brought you here. Nothing follows you around.</p><button class=\"vs-cookie-sheet-switch\" type=\"button\" role=\"switch\" aria-checked=\"false\" aria-labelledby=\"vs-cs-m\"><i></i></button></li>\n        <li class=\"vs-cookie-sheet-row\" data-cat=\"personal\"><b id=\"vs-cs-p\">Preferences</b><p>Remembers your theme, language and last workspace.</p><button class=\"vs-cookie-sheet-switch\" type=\"button\" role=\"switch\" aria-checked=\"false\" aria-labelledby=\"vs-cs-p\"><i></i></button></li>\n      </ul>\n      <div class=\"vs-cookie-sheet-actions\" style=\"margin-top:0\">\n        <button class=\"vs-cookie-sheet-btn\" type=\"button\" data-primary data-act=\"save\">Save choices</button>\n        <button class=\"vs-cookie-sheet-btn\" type=\"button\" data-act=\"all\">Accept all</button>\n      </div>\n    </div>\n  </div>\n</section>\n\n<script>\n(() => {\n  const reduce = matchMedia('(prefers-reduced-motion: reduce)');\n  const store = { get: k => { try { return JSON.parse(localStorage.getItem(k)); } catch (_) { return null; } }, set: (k, v) => { try { localStorage.setItem(k, JSON.stringify(v)); } catch (_) {} } };\n  const spr = (x, k, c) => ({ x, v: 0, g: x, k, c });\n  const step = (s, h) => { s.v += (-s.k * (s.x - s.g) - s.c * s.v) * h; s.x += s.v * h; };\n  const settled = s => Math.abs(s.v) < .01 && Math.abs(s.x - s.g) < .01;\n\n  function mount(el) {\n    if (el.vsCookie) return el.vsCookie;\n    const panel = el.querySelector('.vs-cookie-sheet-panel'), longV = el.querySelector('.vs-cookie-sheet-long');\n    const more = el.querySelector('[data-act=\"more\"]'), switches = [...el.querySelectorAll('.vs-cookie-sheet-switch')];\n    const key = el.dataset.key;\n    const reopen = document.createElement('button'); reopen.type = 'button'; reopen.className = 'vs-cookie-sheet-reopen';\n    reopen.innerHTML = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M12 3 5 6v5c0 4.2 2.9 8 7 9 4.1-1 7-4.8 7-9V6z\"/></svg>Privacy';\n    el.after(reopen);\n    // springs: y (px, sheet offset), h (panel height), and one per switch thumb\n    const Y = spr(0, 300, 27), H = spr(0, 340, 32);\n    let raf = 0, last = 0, open = false, onGone = null, drag = null, hLock = false;\n    const base = () => el.dataset.position === 'center' || !el.dataset.position ? '-50%' : '0';\n    const offY = () => el.offsetHeight + parseFloat(getComputedStyle(el).bottom) + 40;\n    const T = switches.map(sw => ({ sw, i: sw.querySelector('i'), s: spr(sw.getAttribute('aria-checked') === 'true' ? 1 : 0, 520, 30) }));\n    const paint = () => {\n      el.style.transform = `translate3d(${base()}, ${Y.x.toFixed(2)}px, 0)`;\n      if (hLock) panel.style.height = Math.max(0, H.x).toFixed(2) + 'px';\n      for (const t of T) {\n        const travel = t.sw.offsetWidth - t.sw.offsetHeight, v = Math.min(1, Math.abs(t.s.v) / 9);\n        t.i.style.transform = `translateX(${(t.s.x * travel).toFixed(2)}px) scale(${(1 + v * .38).toFixed(3)}, ${(1 - v * .18).toFixed(3)})`;\n      }\n    };\n    const tick = now => {\n      const dt = Math.min(.04, (now - last) / 1000 || .016); last = now; const n = Math.max(1, Math.round(dt * 240)), h = dt / n;\n      const all = [Y, H, ...T.map(t => t.s)];\n      for (const s of all) if (!(s === Y && drag)) for (let i = 0; i < n; i++) step(s, h);\n      if (reduce.matches) for (const s of all) { if (!(s === Y && drag)) { s.x = s.g; s.v = 0; } }\n      paint();\n      const busy = drag || all.some(s => !settled(s));\n      if (!busy) { if (hLock) { hLock = false; panel.style.height = ''; } if (!open && onGone) { const f = onGone; onGone = null; f(); } }\n      raf = busy ? requestAnimationFrame(tick) : 0;\n    };\n    const wake = () => { if (!raf) { last = performance.now(); raf = requestAnimationFrame(tick); } };\n\n    const consent = () => Object.fromEntries([['essential', true], ...T.slice(1).map(t => [t.sw.closest('[data-cat]').dataset.cat, t.sw.getAttribute('aria-checked') === 'true'])]);\n    const setSwitch = (t, on, kick = 0) => { t.sw.setAttribute('aria-checked', String(on)); t.s.g = on ? 1 : 0; t.s.v += kick; wake(); };\n    T.forEach(t => t.sw.addEventListener('click', () => { if (!t.sw.disabled) setSwitch(t, t.sw.getAttribute('aria-checked') !== 'true'); }));\n\n    const setLong = (on, focus) => {\n      if (el.classList.contains('is-long') === on) return;\n      const h0 = panel.offsetHeight; el.classList.toggle('is-long', on); const h1 = panel.scrollHeight;\n      more.setAttribute('aria-expanded', String(on));\n      if (reduce.matches) { if (focus) (on ? T[1].sw : more).focus({ preventScroll: true }); return; }\n      H.x = h0; H.g = h1; H.v = 0; hLock = true; panel.style.height = h0 + 'px';\n      if (on) [...longV.querySelectorAll('.vs-cookie-sheet-row, .vs-cookie-sheet-actions')].forEach((r, i) => r.animate(\n        [{ opacity: 0, transform: 'translateY(10px)', filter: 'blur(4px)' }, { opacity: 1, transform: 'none', filter: 'blur(0)' }],\n        { duration: 520, delay: 40 + i * 45, easing: 'cubic-bezier(.16,1,.3,1)', fill: 'backwards' }));\n      if (focus) (on ? T[1].sw : more).focus({ preventScroll: true });\n      wake();\n    };\n\n    const api = {\n      get consent() { return consent(); },\n      open({ expanded = false, focus = true } = {}) {\n        onGone = null; reopen.classList.remove('is-on');\n        if (!open) { el.classList.add('is-open'); Y.x = offY(); Y.v = 0; }\n        open = true; Y.g = 0; if (expanded) setLong(true); wake();\n        if (focus) setTimeout(() => el.querySelector(expanded ? '.vs-cookie-sheet-switch:not(:disabled)' : '[data-act=\"all\"]')?.focus({ preventScroll: true }), 60);\n        if (reduce.matches) el.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 250 });\n      },\n      close(v = 0) {\n        if (!open) return; open = false; Y.g = offY(); Y.v = Math.max(Y.v, v);\n        const hadFocus = el.contains(document.activeElement);\n        onGone = () => { el.classList.remove('is-open', 'is-long'); more.setAttribute('aria-expanded', 'false'); reopen.classList.add('is-on'); if (hadFocus) reopen.focus({ preventScroll: true }); };\n        if (reduce.matches) { Y.x = Y.g; } wake();\n      },\n    };\n    const commit = mode => {\n      if (mode === 'all') T.slice(1).forEach(t => setSwitch(t, true, 6));\n      if (mode === 'none') T.slice(1).forEach(t => setSwitch(t, false));\n      const c = consent(); if (key) store.set(key, c);\n      el.dispatchEvent(new CustomEvent('vs-cookie-sheet:change', { bubbles: true, detail: c }));\n      setTimeout(() => api.close(), mode === 'all' && el.classList.contains('is-long') ? 380 : 0);\n    };\n    el.addEventListener('click', e => {\n      const b = e.target.closest('[data-act]'); if (!b) return;\n      const a = b.dataset.act;\n      if (a === 'more') setLong(!el.classList.contains('is-long'), e.detail === 0); else commit(a === 'save' ? 'save' : a);\n    });\n    el.addEventListener('keydown', e => { if (e.key === 'Escape') { if (el.classList.contains('is-long')) setLong(false, true); } });\n    reopen.addEventListener('click', () => api.open({ expanded: true }));\n\n    // drag down to dismiss; up rubber-bands\n    el.addEventListener('pointerdown', e => {\n      if (reduce.matches || e.button > 0 || e.target.closest('button, a, input, label')) return;\n      drag = { id: e.pointerId, y0: e.clientY, s: Y.x, t: performance.now(), v: 0, last: e.clientY };\n      try { el.setPointerCapture(e.pointerId); } catch (_) {} wake();\n    });\n    el.addEventListener('pointermove', e => {\n      if (!drag || drag.id !== e.pointerId) return;\n      const now = performance.now(), dy = e.clientY - drag.y0;\n      Y.x = dy > 0 ? drag.s + dy : drag.s - 24 * Math.tanh(-dy / 90);\n      drag.v = drag.v * .7 + ((e.clientY - drag.last) / Math.max(1, now - drag.t) * 1000) * .3; drag.last = e.clientY; drag.t = now;\n    });\n    const end = e => {\n      if (!drag || drag.id !== e.pointerId) return; const d = drag; drag = null; Y.v = d.v;\n      if (Y.x > 90 || d.v > 700) commit('none'); else { Y.g = 0; wake(); }\n    };\n    el.addEventListener('pointerup', end); el.addEventListener('pointercancel', end);\n\n    new ResizeObserver(() => paint()).observe(el);\n    const saved = key && store.get(key);\n    if (saved) { T.slice(1).forEach(t => setSwitch(t, !!saved[t.sw.closest('[data-cat]').dataset.cat])); reopen.classList.add('is-on'); }\n    else setTimeout(() => api.open({ focus: false }), +el.dataset.delay || 500);\n    paint();\n    el.vsCookie = api; return api;\n  }\n  const init = () => document.querySelectorAll('.vs-cookie-sheet').forEach(mount);\n  document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init();\n  window.vsCookieSheet = { mount };\n})();\n</script>\n"
    }
  ],
  "meta": {
    "category": "ui",
    "group": "Marketing blocks",
    "tags": [
      "cookie banner",
      "consent",
      "gdpr",
      "sheet",
      "drawer",
      "toggle",
      "switch",
      "spring",
      "privacy"
    ],
    "tech": [
      "CSS",
      "JS"
    ],
    "deps": [],
    "source": null,
    "free": true,
    "accent": "#0f5f56",
    "added": "2026-10-01",
    "demo": "https://vividsites.app/api/component/cookie-sheet/index.html",
    "thumbnail": "https://vividsites.app/api/component/cookie-sheet/thumb.jpg",
    "video": "https://vividsites.app/api/component/cookie-sheet/thumb.mp4",
    "page": "https://vividsites.app/c/cookie-sheet"
  }
}