{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "spotlight-grid",
  "type": "registry:block",
  "title": "Spotlight Grid",
  "description": "A feature grid lit by one shared cursor spotlight that trails the pointer on a spring, lighting the hairline border of every card it crosses and a dot lattice in the gutters. Keyboard focus pulls the light to the focused card, and touch can press and drag it.",
  "docs": "Spotlight Grid was added to public/vividsites/spotlight-grid.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/spotlight-grid",
  "categories": [
    "ui",
    "marketing-blocks"
  ],
  "files": [
    {
      "path": "components/vividsites/spotlight-grid.html",
      "type": "registry:file",
      "target": "public/vividsites/spotlight-grid.html",
      "content": "<!--\n  Spotlight Grid — vividsites.app\n  A feature grid lit by ONE shared cursor spotlight. The light rides a spring behind the pointer,\n  so it glides across the gaps and lights the hairline border of every card it passes, washes\n  the card it is over, and picks out a dot lattice in the gutters between them. Keyboard focus\n  pulls the light to the focused card; on touch, press and drag to steer it.\n\n  Markup: .vs-spotlight-grid > a.vs-spotlight-grid-card > .vs-spotlight-grid-icon + h3 + p\n  CSS variables:\n    --vs-spotlight-grid-accent     colour of the light\n    --vs-spotlight-grid-radius     spotlight radius (px)\n    --vs-spotlight-grid-strength   0–1, how hard the border lights\n    --vs-spotlight-grid-shade      darkens the light for pale cards (0% on dark, ~30% on white)\n    --vs-spotlight-grid-card       card fill\n    --vs-spotlight-grid-line       resting hairline\n    --vs-spotlight-grid-ink        title colour\n    --vs-spotlight-grid-muted      body colour\n    --vs-spotlight-grid-gap        gutter between cards\n    --vs-spotlight-grid-rounded    card radius\n    --vs-spotlight-grid-cols       columns on wide screens\n  Reduced motion: the light jumps straight to the pointer instead of gliding.\n-->\n<style>\n.vs-spotlight-grid {\n  --vs-spotlight-grid-accent: #5cf2c2;\n  --vs-spotlight-grid-radius: 280;\n  --vs-spotlight-grid-strength: .9;\n  --vs-spotlight-grid-shade: 0%;\n  --vs-spotlight-grid-card: #101318;\n  --vs-spotlight-grid-line: rgba(255,255,255,.07);\n  --vs-spotlight-grid-ink: #eef1f4;\n  --vs-spotlight-grid-muted: rgba(226,232,240,.52);\n  --vs-spotlight-grid-gap: 12px;\n  --vs-spotlight-grid-rounded: 18px;\n  --vs-spotlight-grid-cols: 3;\n  --vs-spotlight-grid-font: inherit;\n\n  --r: calc(var(--vs-spotlight-grid-radius) * 1px);\n  --glow: color-mix(in srgb, var(--vs-spotlight-grid-accent), #000 var(--vs-spotlight-grid-shade));\n  position: relative; display: grid; gap: var(--vs-spotlight-grid-gap);\n  grid-template-columns: repeat(var(--vs-spotlight-grid-cols), minmax(0, 1fr));\n  font-family: var(--vs-spotlight-grid-font); touch-action: pan-y;\n}\n.vs-spotlight-grid, .vs-spotlight-grid * { box-sizing: border-box; }\n/* dot lattice in the gutters, only visible under the light */\n.vs-spotlight-grid::before {\n  content: \"\"; position: absolute; inset: 0; pointer-events: none; opacity: var(--on, 0);\n  background-image: radial-gradient(var(--glow) 1px, transparent 1.4px);\n  background-size: 8px 8px; background-position: 50% 50%;\n  -webkit-mask-image: radial-gradient(calc(var(--r) * .7) circle at var(--gx, -999px) var(--gy, -999px), rgba(0,0,0,.8), transparent 70%);\n          mask-image: radial-gradient(calc(var(--r) * .7) circle at var(--gx, -999px) var(--gy, -999px), rgba(0,0,0,.8), transparent 70%);\n}\n.vs-spotlight-grid-card {\n  position: relative; isolation: isolate; display: flex; flex-direction: column; gap: .5rem;\n  min-height: 12.5rem; padding: 1.5rem 1.5rem 1.4rem; border-radius: var(--vs-spotlight-grid-rounded);\n  background: var(--vs-spotlight-grid-card); color: var(--vs-spotlight-grid-ink); text-decoration: none; outline: none;\n  box-shadow: inset 0 0 0 1px var(--vs-spotlight-grid-line), inset 0 1px 0 rgba(255,255,255,.04), 0 1px 2px rgba(0,0,0,.2);\n  transition: transform .5s cubic-bezier(.3,1.4,.5,1); -webkit-tap-highlight-color: transparent;\n}\n/* border light: a radial gradient clipped to a 1px ring */\n.vs-spotlight-grid-card::before, .vs-spotlight-grid-card::after {\n  content: \"\"; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; opacity: var(--on, 0);\n}\n.vs-spotlight-grid-card::before {\n  padding: 1px; z-index: 2;\n  background: radial-gradient(var(--r) circle at var(--x, -999px) var(--y, -999px),\n    color-mix(in srgb, var(--glow) calc(var(--vs-spotlight-grid-strength) * 100%), transparent),\n    color-mix(in srgb, var(--glow) calc(var(--vs-spotlight-grid-strength) * 45%), transparent) 38%, transparent 72%);\n  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor;\n          mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);\n}\n/* fill wash, softer and wider */\n.vs-spotlight-grid-card::after {\n  z-index: -1;\n  background: radial-gradient(calc(var(--r) * 1.15) circle at var(--x, -999px) var(--y, -999px),\n    color-mix(in srgb, var(--glow) calc(var(--vs-spotlight-grid-strength) * 15%), transparent), transparent 70%);\n}\n.vs-spotlight-grid-icon {\n  display: grid; place-items: center; width: 2.5rem; height: 2.5rem; border-radius: 11px;\n  color: var(--vs-spotlight-grid-muted);\n  background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.015));\n  box-shadow: inset 0 0 0 1px var(--vs-spotlight-grid-line), inset 0 1px 0 rgba(255,255,255,.06);\n  transition: color .4s cubic-bezier(.2,0,0,1), box-shadow .4s cubic-bezier(.2,0,0,1);\n}\n.vs-spotlight-grid-icon svg { width: 1.25rem; height: 1.25rem; }\n.vs-spotlight-grid-card h3 {\n  display: flex; align-items: center; gap: .4em; margin: 2.75rem 0 0; font-size: 1.0625rem; line-height: 1.25; font-weight: 500; letter-spacing: -.012em;\n}\n.vs-spotlight-grid-card h3::after {\n  content: \"\"; width: .5em; height: .5em; margin-left: .1em; opacity: 0; transform: translate(-4px, 4px);\n  border-top: 1.5px solid currentColor; border-right: 1.5px solid currentColor;\n  transition: opacity .3s cubic-bezier(.2,0,0,1), transform .45s cubic-bezier(.3,1.5,.5,1);\n}\n.vs-spotlight-grid-card p { margin: 0; max-width: 34ch; font-size: .875rem; line-height: 1.5; color: var(--vs-spotlight-grid-muted); text-wrap: pretty; }\n.vs-spotlight-grid-card:hover .vs-spotlight-grid-icon, .vs-spotlight-grid-card:focus-visible .vs-spotlight-grid-icon {\n  color: var(--glow);\n  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--glow) 35%, transparent), inset 0 1px 0 rgba(255,255,255,.06), 0 0 18px -4px color-mix(in srgb, var(--glow) 45%, transparent);\n}\n.vs-spotlight-grid-card:hover h3::after, .vs-spotlight-grid-card:focus-visible h3::after { opacity: .7; transform: translate(0, 0) rotate(45deg); }\n.vs-spotlight-grid-card:active { transform: scale(.985); transition-duration: .15s; }\n.vs-spotlight-grid-card:focus-visible { box-shadow: inset 0 0 0 1px var(--vs-spotlight-grid-line), 0 0 0 2px color-mix(in srgb, var(--glow) 70%, transparent); }\n.vs-spotlight-grid-card[aria-disabled=\"true\"] { opacity: .45; pointer-events: none; }\n@media (max-width: 720px) {\n  .vs-spotlight-grid { --vs-spotlight-grid-cols: 1; }\n  .vs-spotlight-grid-card { min-height: 0; display: grid; grid-template-columns: auto 1fr; column-gap: 1rem; row-gap: .2rem; padding: 1rem 1.1rem; align-items: start; }\n  .vs-spotlight-grid-icon { grid-row: span 2; margin: 0; width: 2.25rem; height: 2.25rem; }\n  .vs-spotlight-grid-card h3 { margin: .1rem 0 0; font-size: 1rem; }\n  .vs-spotlight-grid-card p { font-size: .8125rem; line-height: 1.45; }\n}\n</style>\n\n<div class=\"vs-spotlight-grid\">\n  <a class=\"vs-spotlight-grid-card\" href=\"#\">\n    <span class=\"vs-spotlight-grid-icon\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M12 3 4 7v5c0 4.4 3.4 8.2 8 9 4.6-.8 8-4.6 8-9V7z\"/><path d=\"m9 12 2 2 4-4\"/></svg></span>\n    <h3>Signed every step</h3>\n    <p>Each build is hashed and signed at the source, so what ships is exactly what you reviewed.</p>\n  </a>\n  <a class=\"vs-spotlight-grid-card\" href=\"#\">\n    <span class=\"vs-spotlight-grid-icon\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"6\" cy=\"6\" r=\"2.2\"/><circle cx=\"6\" cy=\"18\" r=\"2.2\"/><circle cx=\"18\" cy=\"9\" r=\"2.2\"/><path d=\"M6 8.2v7.6M18 11.2c0 3.8-5 3-10.6 5.4\"/></svg></span>\n    <h3>A preview per branch</h3>\n    <p>Push a branch and get a live URL in under forty seconds, torn down the moment it merges.</p>\n  </a>\n  <a class=\"vs-spotlight-grid-card\" href=\"#\">\n    <span class=\"vs-spotlight-grid-icon\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3 12h4l3-8 4 16 3-8h4\"/></svg></span>\n    <h3>Traces, not guesses</h3>\n    <p>Follow one request through every service it touched, with the slow hop already circled.</p>\n  </a>\n  <a class=\"vs-spotlight-grid-card\" href=\"#\">\n    <span class=\"vs-spotlight-grid-icon\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M3 12h18M12 3c2.6 2.6 3.8 5.6 3.8 9s-1.2 6.4-3.8 9c-2.6-2.6-3.8-5.6-3.8-9S9.4 5.6 12 3z\"/></svg></span>\n    <h3>Pinned to a region</h3>\n    <p>Keep data in Frankfurt, Sydney or São Paulo with one line of config and a signed guarantee.</p>\n  </a>\n  <a class=\"vs-spotlight-grid-card\" href=\"#\">\n    <span class=\"vs-spotlight-grid-icon\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M4 17V7M9 17v-6M14 17V4M19 17v-9\"/><path d=\"M3 20.5h18\"/></svg></span>\n    <h3>Budgets that page you</h3>\n    <p>Set a monthly ceiling per project and hear about the spike before the invoice does.</p>\n  </a>\n  <a class=\"vs-spotlight-grid-card\" href=\"#\">\n    <span class=\"vs-spotlight-grid-icon\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M20 12a8 8 0 1 1-2.3-5.7\"/><path d=\"M20 4v4.5h-4.5\"/></svg></span>\n    <h3>Instant rollback</h3>\n    <p>Every deploy stays warm for thirty days. Going back is one click and zero cold starts.</p>\n  </a>\n</div>\n\n<script>\n(() => {\n  const reduce = matchMedia('(prefers-reduced-motion: reduce)');\n  function mount(grid) {\n    if (grid.__vsSpot) return grid.__vsSpot;\n    const cards = [...grid.querySelectorAll('.vs-spotlight-grid-card')];\n    // spring state (grid-local px): position follows the pointer, `on` fades the light in and out\n    const S = { x: 0, y: 0, vx: 0, vy: 0, tx: 0, ty: 0, on: 0, von: 0, ton: 0, placed: false };\n    let raf = 0, last = 0, visible = true, box = null, rects = [];\n    const measure = () => {\n      box = grid.getBoundingClientRect();\n      rects = cards.map(c => [c.offsetLeft, c.offsetTop, c.offsetWidth, c.offsetHeight]);\n    };\n    const aim = (cx, cy, on = 1) => {\n      if (!box) measure();\n      S.tx = cx - box.left; S.ty = cy - box.top; S.ton = on;\n      if (!S.placed || reduce.matches) { S.x = S.tx; S.y = S.ty; S.vx = S.vy = 0; S.placed = true; }\n      wake();\n    };\n    const paint = () => {\n      grid.style.setProperty('--gx', S.x.toFixed(1) + 'px'); grid.style.setProperty('--gy', S.y.toFixed(1) + 'px');\n      grid.style.setProperty('--on', Math.max(0, Math.min(1, S.on)).toFixed(3));\n      for (let i = 0; i < cards.length; i++) {\n        const [l, t] = rects[i];\n        cards[i].style.setProperty('--x', (S.x - l).toFixed(1) + 'px'); cards[i].style.setProperty('--y', (S.y - t).toFixed(1) + 'px');\n      }\n    };\n    const tick = now => {\n      const dt = Math.min(.05, (now - last) / 1000 || .016); last = now;\n      const n = Math.max(1, Math.round(dt * 240)), h = dt / n;\n      for (let i = 0; i < n; i++) {\n        // tension 190 / friction 22: a light that trails the pointer and overshoots a hair\n        S.vx += (190 * (S.tx - S.x) - 22 * S.vx) * h; S.x += S.vx * h;\n        S.vy += (190 * (S.ty - S.y) - 22 * S.vy) * h; S.y += S.vy * h;\n        S.von += (90 * (S.ton - S.on) - 19 * S.von) * h; S.on += S.von * h;\n      }\n      if (reduce.matches) { S.x = S.tx; S.y = S.ty; S.on = S.ton; S.vx = S.vy = S.von = 0; }\n      paint();\n      const moving = Math.abs(S.vx) + Math.abs(S.vy) > .5 || Math.abs(S.tx - S.x) + Math.abs(S.ty - S.y) > .3 || Math.abs(S.ton - S.on) > .002 || Math.abs(S.von) > .002;\n      raf = moving && visible ? requestAnimationFrame(tick) : 0;\n    };\n    const wake = () => { if (!raf && visible) { last = performance.now(); raf = requestAnimationFrame(tick); } };\n\n    grid.addEventListener('pointerenter', e => { measure(); aim(e.clientX, e.clientY); });\n    grid.addEventListener('pointermove', e => { if (e.pointerType === 'mouse' || e.buttons) aim(e.clientX, e.clientY); }, { passive: true });\n    grid.addEventListener('pointerdown', e => { measure(); aim(e.clientX, e.clientY); });\n    grid.addEventListener('pointerleave', e => { S.ton = 0; wake(); });\n    grid.addEventListener('pointerup', e => { if (e.pointerType !== 'mouse') setTimeout(() => { S.ton = 0; wake(); }, 900); });\n    grid.addEventListener('pointercancel', () => { S.ton = 0; wake(); });\n    // keyboard: the light glides to the focused card\n    grid.addEventListener('focusin', e => {\n      const c = e.target.closest('.vs-spotlight-grid-card'); if (!c || !c.matches(':focus-visible')) return;\n      measure(); const r = c.getBoundingClientRect(); aim(r.left + r.width * .3, r.top + r.height * .25);\n    });\n    grid.addEventListener('focusout', () => { if (!grid.matches(':hover')) { S.ton = 0; wake(); } });\n    new ResizeObserver(measure).observe(grid);\n    addEventListener('scroll', () => { box = null; }, { passive: true });\n    new IntersectionObserver(e => { visible = e[0].isIntersecting; if (visible) wake(); }).observe(grid);\n    measure(); paint();\n    const api = { aim, off: () => { S.ton = 0; wake(); } };\n    grid.__vsSpot = api; return api;\n  }\n  const init = () => document.querySelectorAll('.vs-spotlight-grid').forEach(mount);\n  document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init();\n  window.vsSpotlightGrid = { mount };\n})();\n</script>\n"
    }
  ],
  "meta": {
    "category": "ui",
    "group": "Marketing blocks",
    "tags": [
      "feature grid",
      "spotlight",
      "border glow",
      "cursor",
      "hover",
      "cards",
      "features",
      "spring"
    ],
    "tech": [
      "CSS",
      "JS"
    ],
    "deps": [],
    "source": null,
    "free": true,
    "accent": "#5cf2c2",
    "added": "2026-10-01",
    "demo": "https://vividsites.app/api/component/spotlight-grid/index.html",
    "thumbnail": "https://vividsites.app/api/component/spotlight-grid/thumb.jpg",
    "video": "https://vividsites.app/api/component/spotlight-grid/thumb.mp4",
    "page": "https://vividsites.app/c/spotlight-grid"
  }
}