{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "logo-cloud",
  "type": "registry:block",
  "title": "Logo Cloud",
  "description": "Rows of made-up logos drifting on a rAF loop that glides to a stop under the pointer, drags and flicks with momentum, and fades at the edges. Logos rest in one ink and bloom into their own colour on hover while the rest of the row steps back.",
  "docs": "Logo Cloud was added to public/vividsites/logo-cloud.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/logo-cloud",
  "categories": [
    "ui",
    "marketing-blocks"
  ],
  "files": [
    {
      "path": "components/vividsites/logo-cloud.html",
      "type": "registry:file",
      "target": "public/vividsites/logo-cloud.html",
      "content": "<!--\n  Logo Cloud — vividsites.app\n  Rows of logos that drift on a rAF loop (not a CSS keyframe), so they can do what a CSS\n  marquee can't: glide to a stop when you hover (and glide back when you leave), be dragged or\n  flicked with momentum, and pause off-screen. Logos rest in one ink and bloom into their own\n  colour under the pointer while the rest of the row dims.\n\n  Markup: .vs-logo-cloud > p.vs-logo-cloud-label (optional) + .vs-logo-cloud-row[data-dir=\"-1|1\"] > ul.vs-logo-cloud-track > li > a|span.vs-logo-cloud-logo\n          each logo can carry style=\"--c:#hex\" for its hover colour. Rows are cloned to fill the width.\n  Options (data attributes on .vs-logo-cloud):\n    data-variant=\"plain|tiles\"   bare logos, or each in a hairline tile\n  Row options: data-dir=\"-1\" (left) or \"1\" (right), data-speed=\"1\" (multiplier)\n  CSS variables:\n    --vs-logo-cloud-ink        resting ink of every logo\n    --vs-logo-cloud-rest       resting opacity (0–1)\n    --vs-logo-cloud-speed      cruise speed in px/s\n    --vs-logo-cloud-gap        space between logos\n    --vs-logo-cloud-fade       width of the soft edge mask\n    --vs-logo-cloud-size       logo height / type size\n    --vs-logo-cloud-tile       tile fill (tiles variant)\n    --vs-logo-cloud-line       tile hairline (tiles variant)\n    --vs-logo-cloud-hi         tile top inner highlight (tiles variant)\n    --vs-logo-cloud-radius     tile radius\n    --vs-logo-cloud-lighten    mixes each logo's hover colour toward white (use ~45% on dark stages)\n  Reduced motion: the rows stop and wrap into a centred grid.\n-->\n<style>\n.vs-logo-cloud {\n  --vs-logo-cloud-ink: #1d1c1a;\n  --vs-logo-cloud-rest: .5;\n  --vs-logo-cloud-speed: 42;\n  --vs-logo-cloud-gap: 3.25rem;\n  --vs-logo-cloud-fade: 12%;\n  --vs-logo-cloud-size: 1.6rem;\n  --vs-logo-cloud-tile: rgba(255,255,255,.55);\n  --vs-logo-cloud-line: rgba(29,28,26,.09);\n  --vs-logo-cloud-radius: 14px;\n  --vs-logo-cloud-hi: rgba(255,255,255,.6);\n  --vs-logo-cloud-lighten: 0%;\n  --vs-logo-cloud-font: inherit;\n\n  display: grid; gap: calc(var(--vs-logo-cloud-gap) * .55);\n  width: 100%; color: var(--vs-logo-cloud-ink); font-family: var(--vs-logo-cloud-font);\n  -webkit-user-select: none; user-select: none;\n}\n.vs-logo-cloud, .vs-logo-cloud * { box-sizing: border-box; }\n.vs-logo-cloud-label {\n  margin: 0 0 .4rem; text-align: center; font-size: .8125rem; font-weight: 500; letter-spacing: .01em;\n  color: var(--vs-logo-cloud-ink); opacity: .55; padding: 0 1rem; text-wrap: balance;\n}\n.vs-logo-cloud-row {\n  position: relative; overflow: hidden; touch-action: pan-y; cursor: grab;\n  -webkit-mask-image: linear-gradient(90deg, transparent, #000 var(--vs-logo-cloud-fade), #000 calc(100% - var(--vs-logo-cloud-fade)), transparent);\n          mask-image: linear-gradient(90deg, transparent, #000 var(--vs-logo-cloud-fade), #000 calc(100% - var(--vs-logo-cloud-fade)), transparent);\n}\n.vs-logo-cloud-row.is-drag { cursor: grabbing; }\n.vs-logo-cloud-track {\n  display: flex; align-items: center; width: max-content; margin: 0; padding: .6rem 0; list-style: none;\n  will-change: transform;\n}\n.vs-logo-cloud-track > li { flex: none; padding-right: var(--vs-logo-cloud-gap); }\n.vs-logo-cloud-logo {\n  --c: var(--vs-logo-cloud-ink);\n  --hc: color-mix(in srgb, var(--c), #fff var(--vs-logo-cloud-lighten));\n  position: relative; display: inline-flex; align-items: center; gap: .42em;\n  height: calc(var(--vs-logo-cloud-size) * 1.6); font-size: var(--vs-logo-cloud-size); line-height: 1;\n  color: inherit; text-decoration: none; white-space: nowrap; outline: none; border-radius: 8px;\n  opacity: var(--vs-logo-cloud-rest);\n  transition: opacity .45s cubic-bezier(.2,0,0,1), color .45s cubic-bezier(.2,0,0,1), transform .5s cubic-bezier(.3,1.5,.5,1), filter .45s;\n  -webkit-tap-highlight-color: transparent;\n}\n.vs-logo-cloud-logo svg { height: 1.05em; width: auto; flex: none; overflow: visible; }\n.vs-logo-cloud-logo svg [data-c] { transition: fill .45s cubic-bezier(.2,0,0,1), stroke .45s cubic-bezier(.2,0,0,1); }\n/* one logo in focus: it blooms into its colour, the rest of its row step back */\n.vs-logo-cloud-row:hover .vs-logo-cloud-logo { opacity: calc(var(--vs-logo-cloud-rest) * .55); filter: blur(.4px); }\n.vs-logo-cloud-row .vs-logo-cloud-logo:hover, .vs-logo-cloud-logo:focus-visible {\n  opacity: 1; filter: none; color: var(--hc); transform: translateY(-2px) scale(1.04);\n}\n.vs-logo-cloud-logo:hover svg [data-c=\"fill\"], .vs-logo-cloud-logo:focus-visible svg [data-c=\"fill\"] { fill: var(--hc); }\n.vs-logo-cloud-logo:hover svg [data-c=\"stroke\"], .vs-logo-cloud-logo:focus-visible svg [data-c=\"stroke\"] { stroke: var(--hc); }\n.vs-logo-cloud-logo:active { transform: translateY(0) scale(.97); transition-duration: .12s; }\n.vs-logo-cloud-logo:focus-visible { box-shadow: 0 0 0 2px currentColor; }\n.vs-logo-cloud-row.is-drag .vs-logo-cloud-logo { filter: none; opacity: var(--vs-logo-cloud-rest); transform: none; color: inherit; }\n/* tiles variant */\n.vs-logo-cloud[data-variant=\"tiles\"] { --vs-logo-cloud-gap: .75rem; }\n.vs-logo-cloud[data-variant=\"tiles\"] .vs-logo-cloud-track > li { padding-right: var(--vs-logo-cloud-gap); }\n.vs-logo-cloud[data-variant=\"tiles\"] .vs-logo-cloud-logo {\n  height: calc(var(--vs-logo-cloud-size) * 2.9); min-width: calc(var(--vs-logo-cloud-size) * 7.4); justify-content: center;\n  padding: 0 1.1em; border-radius: var(--vs-logo-cloud-radius); background: var(--vs-logo-cloud-tile);\n  box-shadow: inset 0 0 0 1px var(--vs-logo-cloud-line), inset 0 1px 0 var(--vs-logo-cloud-hi), 0 1px 2px rgba(0,0,0,.04);\n  opacity: 1; font-size: calc(var(--vs-logo-cloud-size) * .82);\n}\n.vs-logo-cloud[data-variant=\"tiles\"] .vs-logo-cloud-logo > * { opacity: var(--vs-logo-cloud-rest); transition: opacity .45s cubic-bezier(.2,0,0,1); }\n.vs-logo-cloud[data-variant=\"tiles\"] .vs-logo-cloud-row:hover .vs-logo-cloud-logo { opacity: .7; }\n.vs-logo-cloud[data-variant=\"tiles\"] .vs-logo-cloud-row .vs-logo-cloud-logo:hover,\n.vs-logo-cloud[data-variant=\"tiles\"] .vs-logo-cloud-logo:focus-visible { opacity: 1; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--hc) 45%, transparent), inset 0 1px 0 var(--vs-logo-cloud-hi), 0 10px 24px -12px color-mix(in srgb, var(--hc) 55%, transparent); }\n.vs-logo-cloud[data-variant=\"tiles\"] .vs-logo-cloud-logo:hover > *, .vs-logo-cloud[data-variant=\"tiles\"] .vs-logo-cloud-logo:focus-visible > * { opacity: 1; }\n/* wordmark styles, so a cloud of made-up brands reads as many companies */\n.vs-logo-cloud-serif { font-family: Georgia, \"Times New Roman\", serif; letter-spacing: -.01em; }\n.vs-logo-cloud-mono { font-family: ui-monospace, \"SF Mono\", Menlo, monospace; font-size: .86em; letter-spacing: -.02em; }\n.vs-logo-cloud-caps { text-transform: uppercase; letter-spacing: .16em; font-size: .72em; font-weight: 600; }\n@media (prefers-reduced-motion: reduce) {\n  .vs-logo-cloud-row { -webkit-mask-image: none; mask-image: none; cursor: default; }\n  .vs-logo-cloud-track { width: auto; flex-wrap: wrap; justify-content: center; row-gap: .6rem; transform: none !important; }\n  .vs-logo-cloud-track > li[aria-hidden] { display: none; }\n  .vs-logo-cloud-track > li { padding: 0 calc(var(--vs-logo-cloud-gap) * .5); }\n}\n</style>\n\n<div class=\"vs-logo-cloud\" data-variant=\"plain\" aria-labelledby=\"vs-logo-cloud-label\">\n  <p class=\"vs-logo-cloud-label\" id=\"vs-logo-cloud-label\">Trusted by 4,200 teams, from two-person studios to public companies</p>\n  <div class=\"vs-logo-cloud-row\" data-dir=\"-1\">\n    <ul class=\"vs-logo-cloud-track\">\n      <li><a class=\"vs-logo-cloud-logo\" href=\"#\" style=\"--c:#1f7a5c\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path data-c=\"stroke\" d=\"M4 4v7a8 8 0 0 0 16 0V4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"3.2\" stroke-linecap=\"round\"/></svg><span style=\"font-weight:600;letter-spacing:-.03em\">oxbow</span></a></li>\n      <li><a class=\"vs-logo-cloud-logo\" href=\"#\" style=\"--c:#c2410c\"><svg viewBox=\"0 0 28 24\" aria-hidden=\"true\"><path data-c=\"fill\" d=\"M1 20 14 3l4 6-8 11z\" fill=\"currentColor\"/><path data-c=\"fill\" d=\"m12 20 8-11 7 11z\" fill=\"currentColor\" opacity=\".55\"/></svg><span class=\"vs-logo-cloud-caps\">Kestrel</span></a></li>\n      <li><a class=\"vs-logo-cloud-logo\" href=\"#\" style=\"--c:#8b2f4c\"><span class=\"vs-logo-cloud-serif\" style=\"font-style:italic;font-size:1.12em\">Velda</span></a></li>\n      <li><a class=\"vs-logo-cloud-logo\" href=\"#\" style=\"--c:#2447d6\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path data-c=\"fill\" d=\"M3 3h12l6 6v12H3z\" fill=\"currentColor\"/><path d=\"M15 3v6h6\" fill=\"none\" stroke=\"#fff\" stroke-opacity=\".7\" stroke-width=\"1.6\"/></svg><span style=\"font-weight:500;letter-spacing:-.02em\">Northfold</span></a></li>\n      <li><a class=\"vs-logo-cloud-logo\" href=\"#\" style=\"--c:#0e7490\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><circle data-c=\"stroke\" cx=\"12\" cy=\"12\" r=\"8.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\"/><circle data-c=\"fill\" cx=\"18.2\" cy=\"6\" r=\"3\" fill=\"currentColor\"/></svg><span class=\"vs-logo-cloud-mono\">quanta</span></a></li>\n      <li><a class=\"vs-logo-cloud-logo\" href=\"#\" style=\"--c:#b45309\"><svg viewBox=\"0 0 30 24\" aria-hidden=\"true\"><circle data-c=\"fill\" cx=\"10\" cy=\"12\" r=\"8\" fill=\"currentColor\" opacity=\".5\"/><circle data-c=\"fill\" cx=\"19\" cy=\"12\" r=\"8\" fill=\"currentColor\"/></svg><span class=\"vs-logo-cloud-serif\">Mirelle</span></a></li>\n    </ul>\n  </div>\n  <div class=\"vs-logo-cloud-row\" data-dir=\"1\" data-speed=\".8\">\n    <ul class=\"vs-logo-cloud-track\">\n      <li><a class=\"vs-logo-cloud-logo\" href=\"#\" style=\"--c:#15803d\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path data-c=\"fill\" d=\"M3 5h11v3.4H3zM3 10.3h15v3.4H3zM3 15.6h18V19H3z\" fill=\"currentColor\"/></svg><span style=\"font-weight:700;letter-spacing:-.045em\">Fieldnote</span></a></li>\n      <li><a class=\"vs-logo-cloud-logo\" href=\"#\" style=\"--c:#9f1239\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path data-c=\"stroke\" d=\"M4 21V11a8 8 0 0 1 16 0v10\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"3.4\"/></svg><span class=\"vs-logo-cloud-caps\" style=\"font-weight:700;letter-spacing:.08em;font-size:.8em\">Arcway</span></a></li>\n      <li><a class=\"vs-logo-cloud-logo\" href=\"#\" style=\"--c:#d97706\"><svg viewBox=\"0 0 26 24\" aria-hidden=\"true\"><path data-c=\"fill\" d=\"M4 18a9 9 0 0 1 18 0z\" fill=\"currentColor\"/><path data-c=\"stroke\" d=\"M13 2.5v3.2M4.6 6l2.2 2.3M21.4 6l-2.2 2.3M1 21h24\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\"/></svg><span style=\"font-weight:600;letter-spacing:-.02em\">lumora</span></a></li>\n      <li><a class=\"vs-logo-cloud-logo\" href=\"#\" style=\"--c:#4338ca\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><g data-c=\"fill\" fill=\"currentColor\" transform=\"rotate(45 12 12)\"><rect x=\"4.5\" y=\"4.5\" width=\"6.5\" height=\"6.5\" rx=\"1\"/><rect x=\"13\" y=\"4.5\" width=\"6.5\" height=\"6.5\" rx=\"1\" opacity=\".55\"/><rect x=\"4.5\" y=\"13\" width=\"6.5\" height=\"6.5\" rx=\"1\" opacity=\".55\"/><rect x=\"13\" y=\"13\" width=\"6.5\" height=\"6.5\" rx=\"1\"/></g></svg><span style=\"font-weight:500\">Tessel</span></a></li>\n      <li><a class=\"vs-logo-cloud-logo\" href=\"#\" style=\"--c:#334155\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><circle data-c=\"stroke\" cx=\"12\" cy=\"12\" r=\"10\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\"/><path data-c=\"stroke\" d=\"M8.5 7v10M15.5 7v10M8.5 12h7\" stroke=\"currentColor\" stroke-width=\"2\"/></svg><span class=\"vs-logo-cloud-serif\" style=\"text-transform:uppercase;letter-spacing:.24em;font-size:.74em\">Halden</span></a></li>\n      <li><a class=\"vs-logo-cloud-logo\" href=\"#\" style=\"--c:#be185d\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path data-c=\"stroke\" d=\"M12 2.5v19M2.5 12h19M5.3 5.3l13.4 13.4M18.7 5.3 5.3 18.7\" stroke=\"currentColor\" stroke-width=\"2.6\" stroke-linecap=\"round\"/></svg><span class=\"vs-logo-cloud-mono\" style=\"font-weight:600\">pallas</span></a></li>\n    </ul>\n  </div>\n  <div class=\"vs-logo-cloud-row\" data-dir=\"-1\" data-speed=\"1.25\">\n    <ul class=\"vs-logo-cloud-track\">\n      <li><a class=\"vs-logo-cloud-logo\" href=\"#\" style=\"--c:#0f766e\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path data-c=\"fill\" d=\"M12 2c3.5 4.2 6 7.6 6 11a6 6 0 0 1-12 0c0-3.4 2.5-6.8 6-11z\" fill=\"currentColor\"/></svg><span style=\"font-weight:600;letter-spacing:-.03em\">brisa</span></a></li>\n      <li><a class=\"vs-logo-cloud-logo\" href=\"#\" style=\"--c:#92400e\"><span class=\"vs-logo-cloud-serif\" style=\"font-size:1.04em\">Umber <i style=\"font-size:.9em\">&amp;</i> Oak</span></a></li>\n      <li><a class=\"vs-logo-cloud-logo\" href=\"#\" style=\"--c:#1d4ed8\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><circle data-c=\"fill\" cx=\"8\" cy=\"10\" r=\"4.2\" fill=\"currentColor\"/><circle data-c=\"fill\" cx=\"16\" cy=\"10\" r=\"4.2\" fill=\"currentColor\"/><path data-c=\"stroke\" d=\"M9 19c2 1.4 4 1.4 6 0\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\"/></svg><span class=\"vs-logo-cloud-mono\" style=\"font-weight:600\">noctua</span></a></li>\n      <li><a class=\"vs-logo-cloud-logo\" href=\"#\" style=\"--c:#57534e\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><g data-c=\"fill\" fill=\"currentColor\"><rect x=\"7\" y=\"2.5\" width=\"10\" height=\"5\" rx=\"2.5\"/><rect x=\"4.5\" y=\"9.5\" width=\"15\" height=\"5\" rx=\"2.5\" opacity=\".7\"/><rect x=\"2.5\" y=\"16.5\" width=\"19\" height=\"5\" rx=\"2.5\" opacity=\".45\"/></g></svg><span class=\"vs-logo-cloud-caps\" style=\"letter-spacing:.3em\">Cairn</span></a></li>\n      <li><a class=\"vs-logo-cloud-logo\" href=\"#\" style=\"--c:#b91c1c\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path data-c=\"fill\" d=\"M3 3h18L3 21z\" fill=\"currentColor\"/><path data-c=\"fill\" d=\"M21 7v14H7z\" fill=\"currentColor\" opacity=\".45\"/></svg><span style=\"font-weight:700;letter-spacing:-.02em\">Ferrous</span></a></li>\n      <li><a class=\"vs-logo-cloud-logo\" href=\"#\" style=\"--c:#a16207\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path data-c=\"stroke\" d=\"M12 22V11\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\"/><path data-c=\"fill\" d=\"M12 2.5c2.2 2.6 3.2 4.4 3.2 5.8a3.2 3.2 0 0 1-6.4 0c0-1.4 1-3.2 3.2-5.8z\" fill=\"currentColor\"/></svg><span class=\"vs-logo-cloud-serif\" style=\"font-style:italic\">wick</span></a></li>\n    </ul>\n  </div>\n</div>\n\n<script>\n(() => {\n  const reduce = matchMedia('(prefers-reduced-motion: reduce)');\n  const fine = matchMedia('(hover: hover) and (pointer: fine)');\n  function mount(root) {\n    if (root.__vsLogoCloud) return root.__vsLogoCloud;\n    const rows = [...root.querySelectorAll('.vs-logo-cloud-row')].map(row => {\n      const track = row.querySelector('.vs-logo-cloud-track');\n      const originals = [...track.children];\n      const R = { row, track, originals, x: 0, set: 0, v: 0, cruise: 0, extra: 0, hover: false, focus: false, drag: null,\n        dir: +(row.dataset.dir || -1) < 0 ? -1 : 1, mult: +(row.dataset.speed || 1) };\n      // clone the set until two sets overflow the row, so the wrap is invisible\n      R.fill = () => {\n        track.querySelectorAll('[data-vs-clone]').forEach(n => n.remove());\n        R.set = track.scrollWidth; if (!R.set) return;\n        const need = Math.ceil((row.clientWidth * 2) / R.set) + 1;\n        for (let i = 1; i < need; i++) for (const li of originals) {\n          const c = li.cloneNode(true); c.setAttribute('aria-hidden', 'true'); c.dataset.vsClone = '';\n          c.querySelectorAll('a,button,[tabindex]').forEach(a => a.tabIndex = -1); track.appendChild(c);\n        }\n      };\n      row.addEventListener('pointerenter', e => { if (e.pointerType === 'mouse' && fine.matches) { R.hover = true; wake(); } });\n      row.addEventListener('pointerleave', e => { if (e.pointerType === 'mouse') { R.hover = false; wake(); } });\n      row.addEventListener('focusin', () => { R.focus = true; wake(); });\n      row.addEventListener('focusout', () => { R.focus = false; wake(); });\n      // drag / flick with momentum; a tap still follows the link\n      row.addEventListener('pointerdown', e => {\n        if (reduce.matches || e.button > 0) return;\n        R.drag = { id: e.pointerId, x0: e.clientX, last: e.clientX, t: performance.now(), vx: 0, moved: false };\n        R.extra = 0; wake();\n      });\n      row.addEventListener('pointermove', e => {\n        const d = R.drag; if (!d || d.id !== e.pointerId) return;\n        const now = performance.now(), dx = e.clientX - d.last;\n        if (!d.moved && Math.abs(e.clientX - d.x0) > 5) { d.moved = true; row.classList.add('is-drag'); try { row.setPointerCapture(e.pointerId); } catch (_) {} }\n        if (!d.moved) return;\n        R.x += dx; d.vx = d.vx * .6 + (dx / Math.max(1, now - d.t) * 1000) * .4; d.last = e.clientX; d.t = now; wake();\n      });\n      const end = e => {\n        const d = R.drag; if (!d || d.id !== e.pointerId) return; R.drag = null; row.classList.remove('is-drag');\n        if (d.moved) { R.extra = Math.max(-2400, Math.min(2400, d.vx)) - R.cruise; R.justDragged = performance.now(); }\n        wake();\n      };\n      row.addEventListener('pointerup', end); row.addEventListener('pointercancel', end);\n      row.addEventListener('click', e => { if (R.justDragged && performance.now() - R.justDragged < 60) e.preventDefault(); }, true);\n      row.addEventListener('dragstart', e => e.preventDefault());\n      return R;\n    });\n    let speed = 42, visible = true, raf = 0, last = 0;\n    const readVars = () => { const v = parseFloat(getComputedStyle(root).getPropertyValue('--vs-logo-cloud-speed')); speed = isNaN(v) ? 42 : v; };\n    const layout = () => { rows.forEach(R => { R.fill(); if (R.set) R.x = ((R.x % R.set) - R.set) % R.set; }); wake(); };\n    const tick = now => {\n      const dt = Math.min(.05, (now - last) / 1000 || .016); last = now; let busy = false;\n      for (const R of rows) {\n        if (!R.set) continue;\n        const target = (R.hover || R.focus || R.drag || reduce.matches) ? 0 : speed * R.mult * R.dir;\n        // glide toward cruise (or stop) — a critically damped approach, so hover-pause eases out\n        R.cruise += (target - R.cruise) * (1 - Math.exp(-dt * (target === 0 ? 5.5 : 2.2)));\n        R.extra *= Math.exp(-dt * 2.6);\n        if (!R.drag) R.x += (R.cruise + R.extra) * dt;\n        R.x = ((R.x % R.set) + R.set) % R.set - R.set;     // keep x in [-set, 0)\n        R.track.style.transform = `translate3d(${R.x.toFixed(2)}px,0,0)`;\n        if (Math.abs(R.cruise - target) > .05 || Math.abs(R.extra) > .5 || target !== 0 || R.drag) busy = true;\n      }\n      raf = busy && visible ? requestAnimationFrame(tick) : 0;\n    };\n    const wake = () => { if (!raf && visible) { last = performance.now(); raf = requestAnimationFrame(tick); } };\n    new IntersectionObserver(e => { visible = e[0].isIntersecting; if (visible) wake(); }).observe(root);\n    new MutationObserver(() => { readVars(); layout(); }).observe(root, { attributes: true, attributeFilter: ['style', 'data-variant', 'class'] });\n    new ResizeObserver(() => layout()).observe(root);\n    reduce.addEventListener?.('change', wake);\n    readVars();\n    (document.fonts ? document.fonts.ready : Promise.resolve()).then(layout);\n    const api = { refresh: () => { readVars(); layout(); } };\n    root.__vsLogoCloud = api; return api;\n  }\n  const init = () => document.querySelectorAll('.vs-logo-cloud').forEach(mount);\n  document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init();\n  window.vsLogoCloud = { mount };\n})();\n</script>\n"
    }
  ],
  "meta": {
    "category": "ui",
    "group": "Marketing blocks",
    "tags": [
      "logo cloud",
      "marquee",
      "social proof",
      "logos",
      "drag",
      "momentum",
      "hover",
      "trusted by"
    ],
    "tech": [
      "CSS",
      "JS"
    ],
    "deps": [],
    "source": null,
    "free": true,
    "accent": "#2447d6",
    "added": "2026-10-01",
    "demo": "https://vividsites.app/api/component/logo-cloud/index.html",
    "thumbnail": "https://vividsites.app/api/component/logo-cloud/thumb.jpg",
    "video": "https://vividsites.app/api/component/logo-cloud/thumb.mp4",
    "page": "https://vividsites.app/c/logo-cloud"
  }
}