{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "momentum-carousel",
  "type": "registry:block",
  "title": "Momentum Carousel",
  "description": "A card carousel that throws like a native scroll view: velocity-projected flicks land on the nearest card with a spring, cards dim and drift in parallax with distance, and a progress bar and rolling counter track where you are.",
  "docs": "Momentum Carousel was added to public/vividsites/momentum-carousel.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/momentum-carousel",
  "categories": [
    "ui",
    "media"
  ],
  "files": [
    {
      "path": "components/vividsites/momentum-carousel.html",
      "type": "registry:file",
      "target": "public/vividsites/momentum-carousel.html",
      "content": "<!--\nvs-momentum-carousel: a card carousel that throws like a native scroll view. Drag follows the\n  finger 1:1 (rubber-banded past the ends); on release the throw is projected from its velocity\n  and lands on the nearest card with a spring that keeps that velocity, so a flick can travel\n  several cards and a nudge settles back. Cards shrink and dim with distance from the anchor and\n  their photos drift in parallax; the rest grips slightly while you hold. Below: a progress bar\n  sized to the visible fraction, a rolling counter, and prev/next buttons. Trackpad horizontal\n  scrolling works and snaps when you stop. Vertical page scrolling on touch is left alone.\n  Keys (viewport focused): ←/→, PageUp/PageDown (3 cards), Home/End.\n\n  Markup:  <div class=\"vs-momentum-carousel\" data-align=\"center\">\n             <div class=\"vs-momentum-carousel-viewport\" aria-label=\"...\">\n               <div class=\"vs-momentum-carousel-track\">\n                 <figure class=\"vs-momentum-carousel-card\"><img src=\"...\" alt=\"...\">\n                   <figcaption><small>No. 01</small><b>Title</b><span>Meta</span></figcaption></figure> ...\n  The foot (progress, counter, buttons) is added by the script.\n  Attributes on .vs-momentum-carousel:\n    data-align   \"center\" (default) or \"start\" (cards snap to the left edge, --vs-momentum-carousel-inset)\n    data-snap    \"free\" lets a throw coast and stop anywhere instead of on a card\n  CSS variables: --vs-momentum-carousel-card (width), -gap, -radius, -ink (foot controls), -accent,\n    -caption (text over photos), -on-ink, -font, -mono, -inset\n  Events: \"change\" with detail { index } when a snap target is chosen.\n  JS: el.__vsMomentumCarousel.goTo(i) / next() / prev() / fling(pxPerSecond) / refresh();\n      vsMomentumCarousel.mount(el) for carousels added later.\n  Reduced motion: no parallax drift or spring; it jumps to the target.\n-->\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\"><link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Archivo:wdth,wght@100,400;125,400&display=swap\" rel=\"stylesheet\">\n<div class=\"vs-momentum-carousel\" data-align=\"center\">\n  <div class=\"vs-momentum-carousel-viewport\" aria-label=\"Field notes\">\n    <div class=\"vs-momentum-carousel-track\">\n      <figure class=\"vs-momentum-carousel-card\"><img src=\"assets/scour.webp\" alt=\"A basket of scoured fleece in a mill hall\" draggable=\"false\">\n        <figcaption><small>No. 01</small><b>Raw fleece, scoured</b><span>Wool mill · batch 14</span></figcaption></figure>\n      <figure class=\"vs-momentum-carousel-card\"><img src=\"assets/attar.webp\" alt=\"An amber perfume bottle on a travertine plinth\" draggable=\"false\">\n        <figcaption><small>No. 02</small><b>Attar No. 9</b><span>Oud, smoke, fig leaf</span></figcaption></figure>\n      <figure class=\"vs-momentum-carousel-card\"><img src=\"assets/pointe.webp\" alt=\"A dancer on pointe on a grey studio floor\" draggable=\"false\">\n        <figcaption><small>No. 03</small><b>Rehearsal, 7 a.m.</b><span>Studio B · pointe work</span></figcaption></figure>\n      <figure class=\"vs-momentum-carousel-card\"><img src=\"assets/wrench.webp\" alt=\"A hand holding a lamp over a motorcycle engine\" draggable=\"false\">\n        <figcaption><small>No. 04</small><b>Engine rebuild</b><span>Twin 1200 · week three</span></figcaption></figure>\n      <figure class=\"vs-momentum-carousel-card\"><img src=\"assets/monolith.webp\" alt=\"A concrete house half hidden by mist on a hillside\" draggable=\"false\" loading=\"lazy\">\n        <figcaption><small>No. 05</small><b>House in the fog</b><span>Hillside retreat</span></figcaption></figure>\n      <figure class=\"vs-momentum-carousel-card\"><img src=\"assets/shed.webp\" alt=\"Hive boxes and buckets inside a wooden shed\" draggable=\"false\" loading=\"lazy\">\n        <figcaption><small>No. 06</small><b>Bee shed</b><span>Heather honey · autumn</span></figcaption></figure>\n      <figure class=\"vs-momentum-carousel-card\"><img src=\"assets/mend.webp\" alt=\"An old loom beside tall factory windows\" draggable=\"false\" loading=\"lazy\">\n        <figcaption><small>No. 07</small><b>Loom room</b><span>Hand repair, mill floor</span></figcaption></figure>\n      <figure class=\"vs-momentum-carousel-card\"><img src=\"assets/dish.webp\" alt=\"A radio telescope dish lit from below at night\" draggable=\"false\" loading=\"lazy\">\n        <figcaption><small>No. 08</small><b>Listening station</b><span>Radio dish after dark</span></figcaption></figure>\n      <figure class=\"vs-momentum-carousel-card\"><img src=\"assets/carrots.webp\" alt=\"A hand lifting carrots from dark soil\" draggable=\"false\" loading=\"lazy\">\n        <figcaption><small>No. 09</small><b>First pull</b><span>Loam farm · October</span></figcaption></figure>\n    </div>\n  </div>\n</div>\n\n<style>\n.vs-momentum-carousel {\n  --vs-momentum-carousel-card: 340px;            /* card width (height follows a 4:5 ratio) */\n  --vs-momentum-carousel-gap: 20px;\n  --vs-momentum-carousel-radius: 22px;\n  --vs-momentum-carousel-ink: #17191a;           /* counter, progress track, buttons */\n  --vs-momentum-carousel-accent: #2f6b3a;        /* progress fill, focus ring */\n  --vs-momentum-carousel-caption: #f6f2e9;       /* text over the photos */\n  --vs-momentum-carousel-on-ink: #ffffff;        /* arrow colour on a hovered button */\n  --vs-momentum-carousel-font: \"Archivo\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif;\n  --vs-momentum-carousel-mono: ui-monospace, \"SF Mono\", Menlo, Consolas, monospace;\n\n  position: relative; width: 100%; min-width: 0; max-width: 100%; color: var(--vs-momentum-carousel-ink); font-family: var(--vs-momentum-carousel-font);\n  -webkit-font-smoothing: antialiased;\n}\n.vs-momentum-carousel *, .vs-momentum-carousel *::before, .vs-momentum-carousel *::after { box-sizing: border-box; }\n.vs-momentum-carousel-viewport {\n  position: relative; overflow: hidden; touch-action: pan-y; cursor: grab; outline: none; padding: 24px 0 44px; margin: -10px 0 -26px;\n  -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent;\n}\n.vs-momentum-carousel.is-dragging .vs-momentum-carousel-viewport { cursor: grabbing; }\n.vs-momentum-carousel-viewport:focus-visible { box-shadow: 0 0 0 2px var(--vs-momentum-carousel-accent) inset; border-radius: 8px; }\n.vs-momentum-carousel-track { display: flex; gap: var(--vs-momentum-carousel-gap); width: max-content; will-change: transform; }\n.vs-momentum-carousel-card {\n  position: relative; flex: none; width: var(--vs-momentum-carousel-card); aspect-ratio: 4 / 5; margin: 0; overflow: hidden;\n  border-radius: var(--vs-momentum-carousel-radius); background: #1d2621; isolation: isolate; will-change: transform;\n  box-shadow: 0 0 0 .5px rgba(255,255,255,.08) inset, 0 30px 60px -30px rgba(0,0,0,.7), 0 8px 20px -10px rgba(0,0,0,.45);\n}\n.vs-momentum-carousel-card img {\n  position: absolute; top: 0; left: -10%; width: 120%; height: 100%; object-fit: cover; display: block; pointer-events: none;\n  will-change: transform; -webkit-user-drag: none;\n}\n.vs-momentum-carousel-card::before { /* read-gradient */\n  content: \"\"; position: absolute; inset: 0; z-index: 1; pointer-events: none;\n  background: linear-gradient(to top, rgba(8,12,10,.78), rgba(8,12,10,.28) 34%, transparent 58%);\n}\n.vs-momentum-carousel-card::after { /* distance dim, driven by --dim */\n  content: \"\"; position: absolute; inset: 0; z-index: 2; pointer-events: none; background: #070a08; opacity: var(--dim, 0);\n}\n.vs-momentum-carousel-card figcaption { color: var(--vs-momentum-carousel-caption); position: absolute; left: 22px; right: 22px; bottom: 20px; z-index: 3; display: grid; gap: 6px; }\n.vs-momentum-carousel-card figcaption small { font: 500 11px/1 var(--vs-momentum-carousel-mono); letter-spacing: .14em; text-transform: uppercase; opacity: .7; }\n.vs-momentum-carousel-card figcaption b { font-weight: 400; font-stretch: 125%; font-size: 24px; line-height: 1.02; letter-spacing: -.02em; }\n.vs-momentum-carousel-card figcaption span { font-size: 13.5px; opacity: .62; }\n\n/* foot: progress + counter + arrows */\n.vs-momentum-carousel-foot { display: flex; align-items: center; gap: 18px; width: min(100%, 720px); margin: 22px auto 0; padding: 0 4px; }\n.vs-momentum-carousel-count { font: 500 12px/1 var(--vs-momentum-carousel-mono); font-variant-numeric: tabular-nums; letter-spacing: .06em; display: flex; gap: 6px; white-space: nowrap; }\n.vs-momentum-carousel-count .n { position: relative; display: inline-block; overflow: hidden; height: 1em; }\n.vs-momentum-carousel-count .n i { display: block; font-style: normal; will-change: transform; }\n.vs-momentum-carousel-count .of { opacity: .45; }\n.vs-momentum-carousel-bar { position: relative; flex: 1; height: 2px; border-radius: 2px; background: color-mix(in srgb, var(--vs-momentum-carousel-ink) 16%, transparent); overflow: hidden; }\n.vs-momentum-carousel-bar i { position: absolute; left: 0; top: 0; bottom: 0; border-radius: 2px; background: var(--vs-momentum-carousel-accent); will-change: transform; }\n.vs-momentum-carousel-btns { display: flex; gap: 8px; }\n.vs-momentum-carousel-btn {\n  all: unset; box-sizing: border-box; width: 44px; height: 44px; border-radius: 50%; display: grid; place-items: center; cursor: pointer;\n  color: var(--vs-momentum-carousel-ink); box-shadow: 0 0 0 1px color-mix(in srgb, var(--vs-momentum-carousel-ink) 22%, transparent) inset;\n  transition: background-color .15s cubic-bezier(.2,0,0,1), color .15s, box-shadow .15s, transform .35s cubic-bezier(.3,1.6,.5,1), opacity .2s;\n}\n.vs-momentum-carousel-btn:hover { background: var(--vs-momentum-carousel-ink); color: var(--vs-momentum-carousel-on-ink); box-shadow: none; }\n.vs-momentum-carousel-btn:active { transform: scale(.9); }\n.vs-momentum-carousel-btn:focus-visible { box-shadow: 0 0 0 2px var(--vs-momentum-carousel-accent); }\n.vs-momentum-carousel-btn[disabled] { opacity: .28; pointer-events: none; }\n.vs-momentum-carousel-btn svg { width: 18px; height: 18px; transition: transform .35s cubic-bezier(.3,1.6,.5,1); }\n.vs-momentum-carousel-btn:hover svg { transform: translateX(var(--nudge, 2px)); }\n\n@media (max-width: 600px) {\n  .vs-momentum-carousel { --vs-momentum-carousel-card: 72vw; --vs-momentum-carousel-gap: 12px; }\n  .vs-momentum-carousel-card figcaption b { font-size: 20px; }\n  .vs-momentum-carousel-foot { gap: 12px; padding: 0 16px; }\n}\n@media (prefers-reduced-motion: reduce) {\n  .vs-momentum-carousel-btn, .vs-momentum-carousel-btn svg { transition: none; }\n}\n</style>\n\n<script>\n(() => {\n  const RM = matchMedia('(prefers-reduced-motion: reduce)');\n  const clamp = (v, a, b) => Math.min(b, Math.max(a, v));\n  const ARROW = d => `<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"${d > 0 ? 'M4 10h11M11 5.5 15.5 10 11 14.5' : 'M16 10H5M9 5.5 4.5 10 9 14.5'}\"/></svg>`;\n\n  function mount(root) {\n    if (root.__vsMomentumCarousel) return root.__vsMomentumCarousel;\n    const $ = (t, c, h) => { const e = document.createElement(t); if (c) e.className = c; if (h != null) e.innerHTML = h; return e; };\n    const vp = root.querySelector('.vs-momentum-carousel-viewport'), track = root.querySelector('.vs-momentum-carousel-track');\n    const cards = [...track.children]; const n = cards.length;\n    vp.tabIndex = 0; root.setAttribute('role', 'region'); root.setAttribute('aria-roledescription', 'carousel');\n    cards.forEach((c, i) => { c.setAttribute('role', 'group'); c.setAttribute('aria-roledescription', 'slide'); c.setAttribute('aria-label', `${i + 1} of ${n}`); });\n\n    // foot\n    const foot = $('div', 'vs-momentum-carousel-foot');\n    const count = $('div', 'vs-momentum-carousel-count', `<span class=\"n\"><i>01</i></span><span class=\"of\">/ ${String(n).padStart(2, '0')}</span>`); count.setAttribute('aria-live', 'polite');\n    const bar = $('div', 'vs-momentum-carousel-bar', '<i></i>'); const fill = bar.firstChild;\n    const btns = $('div', 'vs-momentum-carousel-btns');\n    const prev = $('button', 'vs-momentum-carousel-btn', ARROW(-1)), next = $('button', 'vs-momentum-carousel-btn', ARROW(1));\n    prev.type = next.type = 'button'; prev.setAttribute('aria-label', 'Previous'); next.setAttribute('aria-label', 'Next'); prev.style.setProperty('--nudge', '-2px');\n    btns.append(prev, next); foot.append(count, bar, btns); root.append(foot);\n\n    // geometry\n    let VW = 0, CW = 0, GAP = 0, snaps = [], minX = 0, maxX = 0;\n    const align = () => root.dataset.align === 'start' ? 'start' : 'center';\n    function measure() {\n      VW = vp.clientWidth; CW = cards[0].offsetWidth; GAP = parseFloat(getComputedStyle(track).columnGap) || 0;\n      const step = CW + GAP, total = n * CW + (n - 1) * GAP;\n      if (align() === 'center') { snaps = cards.map((_, i) => VW / 2 - (i * step + CW / 2)); }\n      else {\n        const pad = Math.max(0, parseFloat(getComputedStyle(root).getPropertyValue('--vs-momentum-carousel-inset')) || Math.min(24, VW * .04));\n        snaps = cards.map((_, i) => Math.max(pad - i * step, Math.min(pad, VW - pad - total)));\n      }\n      maxX = snaps[0]; minX = snaps[n - 1];\n      x = snaps[index]; target = x; vel = 0; render();\n    }\n\n    // physics\n    let x = 0, vel = 0, target = 0, index = 0, raf = 0, last = 0, drag = null, samples = [], wheelT = 0, shownIndex = -1;\n    const free = () => root.dataset.snap === 'free';\n    const rubber = v => v > maxX ? maxX + (v - maxX) * .32 : v < minX ? minX - (minX - v) * .32 : v;\n    const nearest = v => { let b = 0; snaps.forEach((s, i) => { if (Math.abs(s - v) < Math.abs(snaps[b] - v)) b = i; }); return b; };\n    function render() {\n      track.style.transform = `translate3d(${x.toFixed(2)}px,0,0)`;\n      const step = CW + GAP, mid = align() === 'center' ? VW / 2 : (snaps[0] + CW / 2);\n      const grip = drag && drag.moved ? .975 : 1;\n      cards.forEach((c, i) => {\n        const cx = x + i * step + CW / 2, d = (cx - mid) / step, a = Math.min(Math.abs(d), 2);\n        c.style.transform = `scale(${((1 - a * .06) * grip).toFixed(4)})`;\n        c.style.setProperty('--dim', (Math.min(a, 1.4) * .38).toFixed(3));\n        const img = c.querySelector('img'); if (img) img.style.transform = `translate3d(${(clamp(-d, -1.5, 1.5) * 7).toFixed(2)}%,0,0)`;\n      });\n      const prog = clamp((maxX - x) / ((maxX - minX) || 1), 0, 1), frac = clamp(VW / (n * step), .08, 1);\n      fill.style.width = (frac * 100) + '%'; fill.style.transform = `translateX(${(prog * (1 / frac - 1) * 100).toFixed(2)}%)`;\n      const cur = nearest(x);\n      if (cur !== shownIndex) {\n        const dir = cur > shownIndex ? 1 : -1, old = count.querySelector('.n i'), nu = $('i', '', String(cur + 1).padStart(2, '0'));\n        const box = count.querySelector('.n');\n        if (shownIndex >= 0 && !RM.matches && old) {\n          [...box.children].filter(e => e !== old).forEach(e => e.remove());\n          nu.style.cssText = 'position:absolute;left:0;top:0'; box.append(nu);\n          const o = { duration: 380, easing: 'cubic-bezier(.3,1.3,.5,1)' };\n          old.animate([{ transform: 'translateY(0)' }, { transform: `translateY(${-dir * 100}%)` }], o);\n          nu.animate([{ transform: `translateY(${dir * 100}%)` }, { transform: 'translateY(0)' }], o).onfinish = () => { if (nu.isConnected) { nu.style.cssText = ''; [...box.children].filter(e => e !== nu).forEach(e => e.remove()); } };\n        } else box.replaceChildren(nu);\n        shownIndex = cur; prev.disabled = cur === 0 && x >= maxX - 1; next.disabled = cur === n - 1 && x <= minX + 1;\n        cards.forEach((c, i) => c.setAttribute('aria-hidden', String(Math.abs(i - cur) > 1)));\n      }\n    }\n    function frame(ts) {\n      raf = 0; const dt = Math.min(.032, (ts - (last || ts)) / 1000); last = ts;\n      if (!drag) {\n        if (RM.matches) { x = target; vel = 0; }\n        else { const k = 170, c = 2 * Math.sqrt(k) * .92; vel += (-(x - target) * k - vel * c) * dt; x += vel * dt; }\n        if (Math.abs(x - target) < .05 && Math.abs(vel) < 2) { x = target; vel = 0; }\n      }\n      render();\n      if (drag || x !== target) raf = requestAnimationFrame(frame); else last = 0;\n    }\n    const kick = () => { if (!raf) raf = requestAnimationFrame(frame); };\n    function goTo(i, v) {\n      index = clamp(i, 0, n - 1); target = snaps[index]; if (v != null) vel = v; kick();\n      root.dispatchEvent(new CustomEvent('change', { detail: { index } }));\n    }\n    function release(v) {\n      // project the throw like a scroll view decelerating, then land on the nearest card\n      const proj = x + v * (free() ? .45 : .28);\n      if (free()) { target = clamp(proj, minX, maxX); vel = v; index = nearest(target); kick(); return; }\n      let i = nearest(proj);\n      if (i === index && Math.abs(v) > 350) i = clamp(index + (v < 0 ? 1 : -1), 0, n - 1);\n      goTo(i, v);\n    }\n\n    // pointer\n    vp.addEventListener('pointerdown', e => {\n      if (e.button > 0) return;\n      drag = { id: e.pointerId, sx: e.clientX, sy: e.clientY, x0: x, moved: false, axis: null }; samples = [{ t: performance.now(), x: e.clientX }];\n      vel = 0; if (e.pointerType === 'mouse') vp.setPointerCapture(e.pointerId);\n    });\n    vp.addEventListener('pointermove', e => {\n      if (!drag || e.pointerId !== drag.id) return;\n      const dx = e.clientX - drag.sx, dy = e.clientY - drag.sy;\n      if (!drag.axis) { if (Math.hypot(dx, dy) < 5) return; drag.axis = Math.abs(dx) >= Math.abs(dy) ? 'x' : 'y'; if (drag.axis === 'x') { vp.setPointerCapture(e.pointerId); root.classList.add('is-dragging'); drag.moved = true; } }\n      if (drag.axis !== 'x') return;\n      x = rubber(drag.x0 + dx); const t = performance.now(); samples.push({ t, x: e.clientX }); while (samples.length > 2 && t - samples[0].t > 100) samples.shift();\n      kick();\n    });\n    const end = e => {\n      if (!drag || e.pointerId !== drag.id) return; const d = drag; drag = null; root.classList.remove('is-dragging');\n      if (d.axis !== 'x') { kick(); return; }\n      const a = samples[0], b = samples[samples.length - 1], dt = (b.t - a.t) / 1000, idle = performance.now() - b.t > 80;\n      const v = dt > .005 && !idle ? clamp((b.x - a.x) / dt, -6000, 6000) : 0;\n      release(v);\n      const eat = ev => { ev.preventDefault(); ev.stopPropagation(); }; vp.addEventListener('click', eat, true); setTimeout(() => vp.removeEventListener('click', eat, true), 0);\n    };\n    vp.addEventListener('pointerup', end); vp.addEventListener('pointercancel', end);\n    vp.addEventListener('lostpointercapture', e => { if (drag && e.pointerId === drag.id) end(e); });\n    vp.addEventListener('dragstart', e => e.preventDefault());\n\n    // trackpad / horizontal wheel\n    vp.addEventListener('wheel', e => {\n      const dx = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.shiftKey ? e.deltaY : 0; if (!dx) return;\n      e.preventDefault(); x = rubber(x - dx); target = x; vel = 0; kick();\n      clearTimeout(wheelT); wheelT = setTimeout(() => { if (free()) { target = clamp(x, minX, maxX); index = nearest(target); kick(); } else goTo(nearest(x)); }, 120);\n    }, { passive: false });\n\n    prev.addEventListener('click', () => goTo(nearest(x) - 1));\n    next.addEventListener('click', () => goTo(nearest(x) + 1));\n    vp.addEventListener('keydown', e => {\n      const map = { ArrowRight: 1, ArrowLeft: -1, PageDown: 3, PageUp: -3 };\n      if (e.key in map) { e.preventDefault(); goTo(nearest(x) + map[e.key]); }\n      else if (e.key === 'Home') { e.preventDefault(); goTo(0); } else if (e.key === 'End') { e.preventDefault(); goTo(n - 1); }\n    });\n\n    new ResizeObserver(() => measure()).observe(vp);\n    new MutationObserver(() => { measure(); }).observe(root, { attributes: true, attributeFilter: ['data-align', 'style'] });\n    measure();\n    const api = { goTo: i => goTo(i), next: () => goTo(nearest(x) + 1), prev: () => goTo(nearest(x) - 1), fling: v => release(v), get index() { return nearest(x); }, refresh: measure };\n    root.__vsMomentumCarousel = api; return api;\n  }\n  document.querySelectorAll('.vs-momentum-carousel').forEach(mount);\n  window.vsMomentumCarousel = { mount };\n})();\n</script>\n"
    }
  ],
  "meta": {
    "category": "ui",
    "group": "Media",
    "tags": [
      "carousel",
      "slider",
      "drag",
      "momentum",
      "snap",
      "gallery",
      "cards",
      "touch",
      "parallax"
    ],
    "tech": [
      "CSS",
      "JS"
    ],
    "deps": [],
    "source": null,
    "free": true,
    "accent": "#d9ff5b",
    "added": "2026-10-01",
    "demo": "https://vividsites.app/api/component/momentum-carousel/index.html",
    "thumbnail": "https://vividsites.app/api/component/momentum-carousel/thumb.jpg",
    "video": "https://vividsites.app/api/component/momentum-carousel/thumb.mp4",
    "page": "https://vividsites.app/c/momentum-carousel"
  }
}