{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "testimonial-wall",
  "type": "registry:block",
  "title": "Testimonial Wall",
  "description": "Two or three rows of testimonial cards drifting in opposite directions under soft edge fades. Hover glides a row down to a crawl on a spring, and a drag scrubs it and keeps your flick's momentum before the cruise eases back in.",
  "docs": "Testimonial Wall was added to public/vividsites/testimonial-wall.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/testimonial-wall",
  "categories": [
    "ui",
    "cards-layout"
  ],
  "files": [
    {
      "path": "components/vividsites/testimonial-wall.html",
      "type": "registry:file",
      "target": "public/vividsites/testimonial-wall.html",
      "content": "<!--\n  vs-testimonial-wall: rows of testimonial cards drifting in opposite directions under soft fade edges.\n\n  - Each .vs-testimonial-wall-row runs on its own clock: data-dir=\"-1\" (left) or \"1\" (right) and\n    data-speed (px per second) per row.\n  - Speed is a spring, not a switch: hovering a row glides it down to a crawl, leaving glides it\n    back up. Nothing stops dead.\n  - Drag (mouse or touch) to scrub a row. Let go mid-flick and it keeps your velocity, bleeds it off,\n    and the cruise eases back in underneath. Touch only claims horizontal swipes, so the page still\n    scrolls. Clicks after a drag are swallowed so links in cards don't fire by accident.\n  - Hovering a card lifts it and swipes a highlighter under its italic phrase.\n  - Options (live): data-rows=\"2\" | \"3\" on .vs-testimonial-wall (three rows switch to a compact card).\n  - Cards are cloned to fill the width; clones are aria-hidden and inert.\n  - Pauses off-screen (IntersectionObserver) and in background tabs. The pause button glides all\n    rows to a stop (WCAG 2.2.2).\n  - prefers-reduced-motion: no drift; each row becomes a normal snap scroller.\n\n  CSS variables (on .vs-testimonial-wall, all live):\n    --vs-testimonial-wall-speed   speed multiplier for every row (1 = the data-speed values)\n    --vs-testimonial-wall-bg      card background\n    --vs-testimonial-wall-ink     text colour\n    --vs-testimonial-wall-accent  italic phrase, opening quote mark, highlighter\n    --vs-testimonial-wall-gap     gap between cards and rows\n    --vs-testimonial-wall-fade    width of each edge fade (% of the row)\n    --vs-testimonial-wall-radius  card radius\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=Instrument+Serif:ital@0;1&family=Mulish:wght@400;500&display=swap\" rel=\"stylesheet\">\n<style>\n.vs-testimonial-wall{\n  --vs-testimonial-wall-bg: #fffdf9;\n  --vs-testimonial-wall-ink: #2a2118;\n  --vs-testimonial-wall-accent: #b8482a;\n  --vs-testimonial-wall-gap: 16px;\n  --vs-testimonial-wall-fade: 12%;\n  --vs-testimonial-wall-radius: 18px;\n  --vs-testimonial-wall-speed: 1;\n  position: relative; display: grid; gap: var(--vs-testimonial-wall-gap); width: 100%; min-width: 0;\n  color: var(--vs-testimonial-wall-ink); font-family: \"Mulish\", system-ui, sans-serif;\n  -webkit-font-smoothing: antialiased;\n}\n.vs-testimonial-wall[data-rows=\"2\"] .vs-testimonial-wall-row:nth-of-type(n+3){ display: none; }\n.vs-testimonial-wall-row{\n  position: relative; overflow: hidden; min-width: 0; padding: 12px 0 30px; margin: -12px 0 -30px; cursor: grab; touch-action: pan-y;\n  user-select: none; -webkit-user-select: none;\n  -webkit-mask-image: linear-gradient(90deg, transparent, #000 var(--vs-testimonial-wall-fade), #000 calc(100% - var(--vs-testimonial-wall-fade)), transparent);\n          mask-image: linear-gradient(90deg, transparent, #000 var(--vs-testimonial-wall-fade), #000 calc(100% - var(--vs-testimonial-wall-fade)), transparent);\n}\n.vs-testimonial-wall-row.is-dragging{ cursor: grabbing; }\n.vs-testimonial-wall-track{ display: flex; gap: var(--vs-testimonial-wall-gap); width: max-content; will-change: transform; }\n\n.vs-testimonial-wall-card{\n  position: relative; flex: none; width: 360px; min-height: 246px; margin: 0; padding: 24px 24px 20px; border-radius: var(--vs-testimonial-wall-radius);\n  display: flex; flex-direction: column; justify-content: space-between; gap: 22px;\n  background: var(--vs-testimonial-wall-bg);\n  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--vs-testimonial-wall-ink) 8%, transparent), inset 0 1px 0 #fff,\n              0 1px 1px rgba(70,45,20,.04), 0 3px 6px -2px rgba(70,45,20,.05), 0 22px 40px -28px rgba(70,45,20,.32);\n  transition: transform .6s cubic-bezier(.16,1,.3,1), box-shadow .6s cubic-bezier(.16,1,.3,1);\n}\n.vs-testimonial-wall-card.is-wide{ width: 450px; }\n.vs-testimonial-wall-card:hover{ transform: translate3d(0,-5px,0) rotate(-.4deg);\n  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--vs-testimonial-wall-ink) 12%, transparent), inset 0 1px 0 #fff,\n              0 2px 2px rgba(70,45,20,.04), 0 8px 14px -6px rgba(70,45,20,.08), 0 34px 54px -30px rgba(70,45,20,.42); }\n.vs-testimonial-wall-card blockquote{ margin: 0; font: 400 23px/1.24 \"Instrument Serif\", Georgia, serif; letter-spacing: -.008em; text-wrap: pretty; }\n.vs-testimonial-wall-card.is-wide blockquote{ font-size: 26px; line-height: 1.2; }\n.vs-testimonial-wall-card blockquote::before{ content: \"\\201C\"; display: block; height: .42em; margin: -.06em 0 .06em -.02em;\n  font-size: 2.1em; line-height: 1; color: var(--vs-testimonial-wall-accent); opacity: .85; }\n/* the italic is the one spark; a highlighter swipes under it when you hover the card */\n.vs-testimonial-wall-card blockquote em{\n  font-style: italic; color: var(--vs-testimonial-wall-accent);\n  background: linear-gradient(color-mix(in srgb, var(--vs-testimonial-wall-accent) 13%, transparent), color-mix(in srgb, var(--vs-testimonial-wall-accent) 13%, transparent)) 0 90% / 0% 34% no-repeat;\n  -webkit-box-decoration-break: clone; box-decoration-break: clone;\n  transition: background-size .8s cubic-bezier(.16,1,.3,1);\n}\n.vs-testimonial-wall-card:hover blockquote em{ background-size: 100% 34%; }\n.vs-testimonial-wall-card figcaption{ display: flex; align-items: center; gap: 11px; }\n.vs-testimonial-wall-avatar{\n  flex: none; width: 36px; height: 36px; border-radius: 50%; display: grid; place-items: center;\n  font: 500 11.5px/1 \"Mulish\", system-ui, sans-serif; letter-spacing: .06em; color: color-mix(in srgb, var(--vs-testimonial-wall-ink) 78%, transparent);\n  background: radial-gradient(circle at 30% 25%, rgba(255,255,255,.65), transparent 58%), var(--vs-testimonial-wall-tone, #ecdcc9);\n  box-shadow: inset 0 0 0 1px rgba(70,45,20,.08), 0 1px 2px rgba(70,45,20,.08);\n}\n.vs-testimonial-wall-who{ display: grid; gap: 3px; min-width: 0; }\n.vs-testimonial-wall-who b{ font-weight: 500; font-size: 13.5px; letter-spacing: -.005em; }\n.vs-testimonial-wall-who span{ font-size: 12.5px; opacity: .56; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.vs-testimonial-wall-src{ margin-left: auto; align-self: center; padding: 5px 8px; border-radius: 999px; white-space: nowrap;\n  font-size: 10px; font-weight: 500; letter-spacing: .12em; text-transform: uppercase; opacity: .5;\n  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--vs-testimonial-wall-ink) 14%, transparent); }\n\n/* three rows: a compact card so the wall keeps its height */\n.vs-testimonial-wall[data-rows=\"3\"] .vs-testimonial-wall-card{ width: 300px; min-height: 0; padding: 18px 20px 16px; gap: 16px; }\n.vs-testimonial-wall[data-rows=\"3\"] .vs-testimonial-wall-card.is-wide{ width: 380px; }\n.vs-testimonial-wall[data-rows=\"3\"] .vs-testimonial-wall-card blockquote{ font-size: 18.5px; }\n.vs-testimonial-wall[data-rows=\"3\"] .vs-testimonial-wall-card blockquote::before{ display: none; }\n.vs-testimonial-wall[data-rows=\"3\"] .vs-testimonial-wall-avatar{ width: 30px; height: 30px; font-size: 10px; }\n\n.vs-testimonial-wall-pause{\n  justify-self: center; margin: 4px 0 0; appearance: none; border: 0; cursor: pointer; color: inherit;\n  display: inline-flex; align-items: center; gap: 8px; padding: 8px 13px 8px 11px; border-radius: 999px;\n  font: 500 10.5px/1 \"Mulish\", system-ui, sans-serif; letter-spacing: .14em; text-transform: uppercase;\n  background: color-mix(in srgb, var(--vs-testimonial-wall-bg) 70%, transparent);\n  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--vs-testimonial-wall-ink) 12%, transparent); opacity: .62;\n  transition: opacity .2s cubic-bezier(.2,0,0,1), transform .2s cubic-bezier(.2,0,0,1);\n  -webkit-tap-highlight-color: transparent;\n}\n.vs-testimonial-wall-pause:hover{ opacity: 1; }\n.vs-testimonial-wall-pause:active{ transform: scale(.95); }\n.vs-testimonial-wall-pause:focus-visible{ outline: 2px solid var(--vs-testimonial-wall-accent); outline-offset: 2px; opacity: 1; }\n.vs-testimonial-wall-pause svg{ width: 9px; height: 9px; }\n.vs-testimonial-wall-pause .vs-testimonial-wall-play{ display: none; }\n.vs-testimonial-wall.is-paused .vs-testimonial-wall-pause .vs-testimonial-wall-play{ display: block; }\n.vs-testimonial-wall.is-paused .vs-testimonial-wall-pause .vs-testimonial-wall-bars{ display: none; }\n\n@media (max-width: 640px){\n  .vs-testimonial-wall{ --vs-testimonial-wall-fade: 6%; --vs-testimonial-wall-gap: 12px; }\n  .vs-testimonial-wall-card, .vs-testimonial-wall[data-rows=\"3\"] .vs-testimonial-wall-card{ width: 272px; min-height: 0; padding: 18px 18px 16px; gap: 16px; }\n  .vs-testimonial-wall-card.is-wide, .vs-testimonial-wall[data-rows=\"3\"] .vs-testimonial-wall-card.is-wide{ width: 300px; }\n  .vs-testimonial-wall-card blockquote, .vs-testimonial-wall-card.is-wide blockquote{ font-size: 19px; }\n  .vs-testimonial-wall-src{ display: none; }\n}\n@media (prefers-reduced-motion: reduce){\n  .vs-testimonial-wall-row{ overflow-x: auto; cursor: auto; scroll-snap-type: x mandatory; scrollbar-width: none; -webkit-mask-image: none; mask-image: none; padding: 10px 16px 14px; }\n  .vs-testimonial-wall-card{ scroll-snap-align: start; transition: none; }\n  .vs-testimonial-wall-card blockquote em{ transition: none; }\n  .vs-testimonial-wall-pause{ display: none; }\n}\n</style>\n\n<section class=\"vs-testimonial-wall\" data-rows=\"2\" aria-label=\"What studios say\">\n  <div class=\"vs-testimonial-wall-row\" data-dir=\"-1\" data-speed=\"34\">\n    <div class=\"vs-testimonial-wall-track\">\n      <figure class=\"vs-testimonial-wall-card is-wide\">\n        <blockquote>We used to lose a day per project to PDF round-trips. Now the client <em>circles the thing on the actual page</em> and we fix it before lunch.</blockquote>\n        <figcaption><span class=\"vs-testimonial-wall-avatar\" style=\"--vs-testimonial-wall-tone:#ecd6c3\" aria-hidden=\"true\">MH</span><span class=\"vs-testimonial-wall-who\"><b>Maren Holt</b><span>Creative director, Northfold</span></span><span class=\"vs-testimonial-wall-src\">Email</span></figcaption>\n      </figure>\n      <figure class=\"vs-testimonial-wall-card\">\n        <blockquote>The first review tool our clients <em>didn't need a tutorial</em> for.</blockquote>\n        <figcaption><span class=\"vs-testimonial-wall-avatar\" style=\"--vs-testimonial-wall-tone:#d6e0d2\" aria-hidden=\"true\">TW</span><span class=\"vs-testimonial-wall-who\"><b>Tobias Wren</b><span>Founder, Studio Kettle</span></span><span class=\"vs-testimonial-wall-src\">Review</span></figcaption>\n      </figure>\n      <figure class=\"vs-testimonial-wall-card\">\n        <blockquote>It killed the final_final_v7 folder. <em>I didn't know that was possible.</em></blockquote>\n        <figcaption><span class=\"vs-testimonial-wall-avatar\" style=\"--vs-testimonial-wall-tone:#dcdde9\" aria-hidden=\"true\">IV</span><span class=\"vs-testimonial-wall-who\"><b>Ines Varga</b><span>Art director, Pellucid</span></span><span class=\"vs-testimonial-wall-src\">Review</span></figcaption>\n      </figure>\n      <figure class=\"vs-testimonial-wall-card\">\n        <blockquote>Version compare is the feature. Two frames, one slider, and <em>the argument is over.</em></blockquote>\n        <figcaption><span class=\"vs-testimonial-wall-avatar\" style=\"--vs-testimonial-wall-tone:#efd8d2\" aria-hidden=\"true\">KM</span><span class=\"vs-testimonial-wall-who\"><b>Kofi Mensah</b><span>Producer, Lowtide Films</span></span><span class=\"vs-testimonial-wall-src\">Call notes</span></figcaption>\n      </figure>\n      <figure class=\"vs-testimonial-wall-card is-wide\">\n        <blockquote>Our producers stopped forwarding emails. <em>Everything that matters is pinned to the frame,</em> and the rest can wait.</blockquote>\n        <figcaption><span class=\"vs-testimonial-wall-avatar\" style=\"--vs-testimonial-wall-tone:#e9e0c4\" aria-hidden=\"true\">SI</span><span class=\"vs-testimonial-wall-who\"><b>Sana Ikeda</b><span>Design lead, Arbor &amp; Co</span></span><span class=\"vs-testimonial-wall-src\">Survey</span></figcaption>\n      </figure>\n    </div>\n  </div>\n  <div class=\"vs-testimonial-wall-row\" data-dir=\"1\" data-speed=\"28\">\n    <div class=\"vs-testimonial-wall-track\">\n      <figure class=\"vs-testimonial-wall-card\">\n        <blockquote>Honestly it's just calm. Comments land where they belong and <em>nothing pings me after seven.</em></blockquote>\n        <figcaption><span class=\"vs-testimonial-wall-avatar\" style=\"--vs-testimonial-wall-tone:#efd8d2\" aria-hidden=\"true\">LF</span><span class=\"vs-testimonial-wall-who\"><b>Luca Ferri</b><span>Partner, Ferri Brandt</span></span><span class=\"vs-testimonial-wall-src\">Review</span></figcaption>\n      </figure>\n      <figure class=\"vs-testimonial-wall-card is-wide\">\n        <blockquote>We onboarded a forty-person agency in an afternoon. <em>Nobody asked for a training call,</em> which has never happened before.</blockquote>\n        <figcaption><span class=\"vs-testimonial-wall-avatar\" style=\"--vs-testimonial-wall-tone:#dcdde9\" aria-hidden=\"true\">AL</span><span class=\"vs-testimonial-wall-who\"><b>Ada Lindqvist</b><span>Head of brand, Hollow Oak</span></span><span class=\"vs-testimonial-wall-src\">Email</span></figcaption>\n      </figure>\n      <figure class=\"vs-testimonial-wall-card\">\n        <blockquote>The approvals log saved us in a billing dispute. <em>Timestamped, per frame,</em> no debate.</blockquote>\n        <figcaption><span class=\"vs-testimonial-wall-avatar\" style=\"--vs-testimonial-wall-tone:#e9e0c4\" aria-hidden=\"true\">RM</span><span class=\"vs-testimonial-wall-who\"><b>Ravi Menon</b><span>Illustrator, independent</span></span><span class=\"vs-testimonial-wall-src\">Review</span></figcaption>\n      </figure>\n      <figure class=\"vs-testimonial-wall-card\">\n        <blockquote>Feels like it was designed by people who have <em>actually sat through a client review.</em></blockquote>\n        <figcaption><span class=\"vs-testimonial-wall-avatar\" style=\"--vs-testimonial-wall-tone:#ecd6c3\" aria-hidden=\"true\">CM</span><span class=\"vs-testimonial-wall-who\"><b>Clara Moss</b><span>Studio manager, Fieldwork</span></span><span class=\"vs-testimonial-wall-src\">Survey</span></figcaption>\n      </figure>\n      <figure class=\"vs-testimonial-wall-card\">\n        <blockquote>Our turnaround on motion notes went from <em>three days to one.</em></blockquote>\n        <figcaption><span class=\"vs-testimonial-wall-avatar\" style=\"--vs-testimonial-wall-tone:#d6e0d2\" aria-hidden=\"true\">JP</span><span class=\"vs-testimonial-wall-who\"><b>Jonah Pike</b><span>Motion designer, Tallow</span></span><span class=\"vs-testimonial-wall-src\">Call notes</span></figcaption>\n      </figure>\n    </div>\n  </div>\n  <div class=\"vs-testimonial-wall-row\" data-dir=\"-1\" data-speed=\"22\">\n    <div class=\"vs-testimonial-wall-track\">\n      <figure class=\"vs-testimonial-wall-card\">\n        <blockquote>The comment pins stay put when the layout changes. <em>That alone paid for the year.</em></blockquote>\n        <figcaption><span class=\"vs-testimonial-wall-avatar\" style=\"--vs-testimonial-wall-tone:#d6e0d2\" aria-hidden=\"true\">NH</span><span class=\"vs-testimonial-wall-who\"><b>Noor Haddad</b><span>UX lead, Glasshouse</span></span><span class=\"vs-testimonial-wall-src\">Review</span></figcaption>\n      </figure>\n      <figure class=\"vs-testimonial-wall-card is-wide\">\n        <blockquote>I send one link. <em>Clients reply on the frame,</em> not in a thread I have to decode at midnight.</blockquote>\n        <figcaption><span class=\"vs-testimonial-wall-avatar\" style=\"--vs-testimonial-wall-tone:#ecd6c3\" aria-hidden=\"true\">ES</span><span class=\"vs-testimonial-wall-who\"><b>Emil Sorensen</b><span>Freelance designer</span></span><span class=\"vs-testimonial-wall-src\">Email</span></figcaption>\n      </figure>\n      <figure class=\"vs-testimonial-wall-card\">\n        <blockquote>Three tools in a quarter. This is the one <em>the account team actually opens.</em></blockquote>\n        <figcaption><span class=\"vs-testimonial-wall-avatar\" style=\"--vs-testimonial-wall-tone:#dcdde9\" aria-hidden=\"true\">PR</span><span class=\"vs-testimonial-wall-who\"><b>Priya Raman</b><span>Ops director, Kiln Studio</span></span><span class=\"vs-testimonial-wall-src\">Survey</span></figcaption>\n      </figure>\n      <figure class=\"vs-testimonial-wall-card\">\n        <blockquote>Our Friday review got <em>forty minutes shorter</em> and nobody misses the slides.</blockquote>\n        <figcaption><span class=\"vs-testimonial-wall-avatar\" style=\"--vs-testimonial-wall-tone:#efd8d2\" aria-hidden=\"true\">TB</span><span class=\"vs-testimonial-wall-who\"><b>Theo Brandt</b><span>Creative lead, Halfmoon</span></span><span class=\"vs-testimonial-wall-src\">Call notes</span></figcaption>\n      </figure>\n    </div>\n  </div>\n  <button class=\"vs-testimonial-wall-pause\" type=\"button\" aria-pressed=\"false\"><svg class=\"vs-testimonial-wall-bars\" viewBox=\"0 0 10 10\" aria-hidden=\"true\"><rect x=\"1.5\" y=\"1\" width=\"2.2\" height=\"8\" rx=\".6\" fill=\"currentColor\"/><rect x=\"6.3\" y=\"1\" width=\"2.2\" height=\"8\" rx=\".6\" fill=\"currentColor\"/></svg><svg class=\"vs-testimonial-wall-play\" viewBox=\"0 0 10 10\" aria-hidden=\"true\"><path d=\"M2.5 1.2v7.6L8.8 5z\" fill=\"currentColor\"/></svg><span class=\"vs-testimonial-wall-label\">Pause</span></button>\n</section>\n\n<script>\n(() => {\n  const reduce = matchMedia('(prefers-reduced-motion: reduce)');\n  function init(root) {\n    if (root.dataset.vsReady) return; root.dataset.vsReady = '1';\n    if (reduce.matches) return;       // CSS turns each row into a plain scroller\n    const pauseBtn = root.querySelector('.vs-testimonial-wall-pause');\n    let paused = false, visible = true, raf = 0, last = 0, speedMul = 1;\n    const readSpeed = () => { const v = parseFloat(getComputedStyle(root).getPropertyValue('--vs-testimonial-wall-speed')); speedMul = Number.isFinite(v) ? v : 1; };\n    const HK = 28, HC = 2 * Math.sqrt(HK);   // hover spring: critically damped, ~0.4 s to settle\n    const rows = [...root.querySelectorAll('.vs-testimonial-wall-row')].map(row => {\n      const track = row.querySelector('.vs-testimonial-wall-track');\n      const originals = [...track.children];\n      // f = cruise factor on a spring (1 cruising, .12 hovered, 0 paused); boost = flick velocity that bleeds off\n      const r = { row, track, originals, x: 0, set: 0, dir: +row.dataset.dir || -1, base: +row.dataset.speed || 34,\n        f: 1, fv: 0, boost: 0, hover: false, drag: null };\n      r.fill = () => {\n        track.querySelectorAll('[data-vs-clone]').forEach(n => n.remove());\n        const gap = parseFloat(getComputedStyle(track).columnGap) || 0;\n        r.set = originals.reduce((a, n) => a + n.getBoundingClientRect().width + gap, 0);\n        if (!r.set) return;\n        const need = Math.ceil((row.clientWidth + r.set) / r.set);\n        for (let k = 0; k < need; k++) originals.forEach(n => {\n          const c = n.cloneNode(true); c.setAttribute('data-vs-clone', ''); c.setAttribute('aria-hidden', 'true'); c.inert = true; track.append(c);\n        });\n      };\n      row.addEventListener('pointerenter', e => { if (e.pointerType === 'mouse') { r.hover = true; kick(); } });\n      row.addEventListener('pointerleave', () => { r.hover = false; kick(); });\n      row.addEventListener('pointerdown', e => {\n        if (e.button !== 0) return;\n        r.drag = { id: e.pointerId, x: e.clientX, y: e.clientY, t: performance.now(), vx: 0, on: e.pointerType !== 'touch' };\n        if (r.drag.on) { row.setPointerCapture(e.pointerId); row.classList.add('is-dragging'); r.boost = 0; }\n      });\n      row.addEventListener('pointermove', e => {\n        const d = r.drag; if (!d || d.id !== e.pointerId) return;\n        const dx = e.clientX - d.x;\n        if (!d.on) {   // touch: only claim horizontal gestures, let vertical ones scroll the page\n          if (Math.abs(dx) < 6 && Math.abs(e.clientY - d.y) < 6) return;\n          if (Math.abs(dx) <= Math.abs(e.clientY - d.y)) { r.drag = null; return; }\n          d.on = true; r.boost = 0; row.setPointerCapture(e.pointerId); row.classList.add('is-dragging');\n        }\n        const now = performance.now(), dt = Math.max(1, now - d.t);\n        r.x += dx; d.vx = d.vx * 0.55 + (dx / dt * 1000) * 0.45; d.x = e.clientX; d.t = now;\n        kick();\n      });\n      const up = e => {\n        const d = r.drag; if (!d || d.id !== e.pointerId) return;\n        if (d.on) {\n          if (performance.now() - d.t > 90) d.vx = 0;          // held still before letting go: no flick\n          r.boost = Math.max(-3200, Math.min(3200, d.vx));     // keep the flick; cruise eases back in underneath it\n          r.f = 0; r.fv = 0;\n          row.classList.add('was-dragged'); setTimeout(() => row.classList.remove('was-dragged'), 0);\n        }\n        r.drag = null; row.classList.remove('is-dragging'); kick();\n      };\n      row.addEventListener('pointerup', up); row.addEventListener('pointercancel', up);\n      // stop clicks after a drag from reaching links inside cards\n      row.addEventListener('click', e => { if (row.classList.contains('was-dragged')) { e.preventDefault(); e.stopPropagation(); } }, true);\n      return r;\n    });\n    const tick = now => {\n      const dt = Math.min(0.05, (now - last) / 1000); last = now;\n      const n = Math.max(1, Math.ceil(dt * 240)), h = dt / n;\n      for (const r of rows) {\n        if (!r.set) continue;\n        if (!r.drag || !r.drag.on) {\n          const ft = paused ? 0 : r.hover ? 0.12 : 1;\n          for (let i = 0; i < n; i++) { r.fv += (-HK * (r.f - ft) - HC * r.fv) * h; r.f += r.fv * h; }\n          r.boost *= Math.exp(-2.4 * dt);\n          if (Math.abs(r.boost) < 0.5) r.boost = 0;\n          r.x += (r.base * r.dir * speedMul * r.f + r.boost) * dt;\n        }\n        r.x = ((r.x % r.set) - r.set) % r.set;              // wrap into (−set, 0]\n        r.track.style.transform = `translate3d(${r.x.toFixed(2)}px,0,0)`;\n      }\n      raf = visible && !document.hidden ? requestAnimationFrame(tick) : 0;\n    };\n    const kick = () => { if (!raf && visible && !document.hidden) { last = performance.now(); raf = requestAnimationFrame(tick); } };\n    const layout = () => { rows.forEach(r => r.fill()); kick(); };\n    readSpeed();\n    new MutationObserver(readSpeed).observe(root, { attributes: true, attributeFilter: ['style', 'class'] });\n    new ResizeObserver(() => layout()).observe(root);\n    if (document.fonts) document.fonts.ready.then(layout);\n    new IntersectionObserver(([e]) => { visible = e.isIntersecting; if (visible) kick(); else { cancelAnimationFrame(raf); raf = 0; } }).observe(root);\n    document.addEventListener('visibilitychange', kick);\n    if (pauseBtn) pauseBtn.addEventListener('click', () => {\n      paused = !paused; root.classList.toggle('is-paused', paused);\n      pauseBtn.setAttribute('aria-pressed', String(paused));\n      pauseBtn.querySelector('.vs-testimonial-wall-label').textContent = paused ? 'Play' : 'Pause';\n      kick();\n    });\n    root.vsWall = { layout };\n    layout();\n  }\n  const boot = () => document.querySelectorAll('.vs-testimonial-wall').forEach(init);\n  document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', boot) : boot();\n})();\n</script>\n"
    }
  ],
  "meta": {
    "category": "ui",
    "group": "Cards & layout",
    "tags": [
      "testimonials",
      "marquee",
      "social proof",
      "reviews",
      "drag",
      "infinite scroll",
      "cards",
      "momentum"
    ],
    "tech": [
      "CSS",
      "JS"
    ],
    "deps": [
      "https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Mulish:wght@400;500&display=swap"
    ],
    "source": null,
    "free": true,
    "accent": "#b8482a",
    "added": "2026-09-30",
    "demo": "https://vividsites.app/api/component/testimonial-wall/index.html",
    "thumbnail": "https://vividsites.app/api/component/testimonial-wall/thumb.jpg",
    "video": "https://vividsites.app/api/component/testimonial-wall/thumb.mp4",
    "page": "https://vividsites.app/c/testimonial-wall"
  }
}