{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "holo-card",
  "type": "registry:block",
  "title": "Holo Card",
  "description": "A trading card with a real holographic foil shader under a crisp HTML face: prism, galaxy or etched foil that shifts with a spring tilt, plus glare and a click-to-flip back.",
  "docs": "Holo Card was added to public/vividsites/holo-card.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/holo-card",
  "categories": [
    "3d",
    "3d-objects"
  ],
  "files": [
    {
      "path": "components/vividsites/holo-card.html",
      "type": "registry:file",
      "target": "public/vividsites/holo-card.html",
      "content": "<!--\n  vividsites · holo-card\n  A trading card with a real holographic foil: a WebGL shader under the crisp HTML\n  face, whose rainbow, sparkle and glare move with the card's tilt. The card follows\n  the pointer on springs (velocity-aware, it overshoots and settles), sways a little\n  when idle, and flips to its back on click, Enter or Space. Touch: drag to tilt, tap to flip.\n\n  Customise (attributes, read at mount; all live through the JS API too):\n    data-foil       prism | galaxy | etched                  (default prism)\n    data-coverage   art | reverse | full                     (where the foil sits; default art)\n  CSS variables:\n    --vs-holo-card-accent     the frame metal                (#d8b25c)\n    --vs-holo-card-intensity  foil strength, 0 – 1.5         (1)\n    --vs-holo-card-tilt       max tilt in degrees            (16)\n    --vs-holo-card-width      card width                     (min(340px, 78vw))\n  Edit the card's text in the markup below; the art is drawn in the shader (swap drawArt()).\n  JS:\n    el.vsHoloCard.set({ foil: 'galaxy', coverage: 'full', accent: '#b9c3cf', intensity: 1.2 })\n    el.vsHoloCard.flip()      el.vsHoloCard.destroy()\n-->\n<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;600&family=DM+Mono:wght@400&display=swap\">\n<div class=\"vs-holo-card\" data-foil=\"prism\" data-coverage=\"art\">\n  <div class=\"vs-holo-card-shadow\" aria-hidden=\"true\"></div>\n  <div class=\"vs-holo-card-tilt\" tabindex=\"0\" role=\"button\" aria-pressed=\"false\" aria-label=\"Aurelian Drift, holo rare card. Press to flip.\">\n    <canvas class=\"vs-holo-card-gl\" aria-hidden=\"true\"></canvas>\n    <div class=\"vs-holo-card-face\">\n      <header class=\"vs-holo-card-name\"><b>Aurelian Drift</b><span>Nº 047</span></header>\n      <p class=\"vs-holo-card-type\">Celestial · Tidebound</p>\n      <div class=\"vs-holo-card-text\">\n        <p><b>Low-tide light.</b> Once per turn, tilt this card toward any light: look at the top three cards of your deck and keep one.</p>\n        <p class=\"vs-holo-card-flavor\">It rises twice a night over the salt dunes, and only the patient see both.</p>\n      </div>\n      <footer class=\"vs-holo-card-foot\"><span>Illus. Vale Oyelaran</span><span>047/210 ✦</span></footer>\n    </div>\n  </div>\n</div>\n\n<style>\n  .vs-holo-card {\n    --vs-holo-card-accent: #d8b25c;\n    --vs-holo-card-intensity: 1;\n    --vs-holo-card-tilt: 16;\n    --vs-holo-card-width: min(340px, 78vw);\n    position: relative; display: grid; place-items: center; width: 100%; height: 100%; min-height: 540px; perspective: 1200px;\n  }\n  .vs-holo-card-tilt {\n    position: relative; width: var(--vs-holo-card-width); aspect-ratio: 63 / 88; border-radius: calc(var(--vs-holo-card-width) * .045);\n    overflow: hidden; cursor: pointer; outline: none; touch-action: none; -webkit-tap-highlight-color: transparent; user-select: none; -webkit-user-select: none;\n    transform-origin: 50% 50%; background: #1a1a1a;\n    box-shadow: inset 0 0 0 .5px rgba(255,255,255,.25), 0 1px 1px rgba(0,0,0,.25);\n    container-type: inline-size;\n  }\n  .vs-holo-card-tilt:focus-visible { box-shadow: 0 0 0 2px #121214, 0 0 0 4px var(--vs-holo-card-accent); }\n  .vs-holo-card-gl { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }\n  .vs-holo-card-shadow { position: absolute; left: 50%; top: 50%; width: var(--vs-holo-card-width); aspect-ratio: 63 / 88; margin-left: calc(var(--vs-holo-card-width) / -2);\n    transform: translateY(-50%); border-radius: 6%; pointer-events: none; will-change: transform;\n    background: rgba(0,0,0,.55); filter: blur(28px); opacity: .7; }\n  /* the printed face: sits on the shader, sized in container units so it scales with the card */\n  .vs-holo-card-face { position: absolute; inset: 0; pointer-events: none; color: #17140f; transition: opacity .12s linear;\n    font: 400 3.3cqw/1.38 \"Chakra Petch\", ui-sans-serif, system-ui, sans-serif; }\n  .vs-holo-card.is-back .vs-holo-card-face { opacity: 0; }\n  .vs-holo-card-name { position: absolute; left: 7%; right: 7%; top: 4.6%; height: 5.6%; display: flex; align-items: center; justify-content: space-between; color: #fbf6ea; }\n  .vs-holo-card-name b { font-weight: 600; font-size: 5.6cqw; letter-spacing: -.01em; text-shadow: 0 1px 0 rgba(0,0,0,.35); }\n  .vs-holo-card-name span { font: 400 3cqw/1 \"DM Mono\", ui-monospace, Menlo, monospace; letter-spacing: .08em; opacity: .85; }\n  .vs-holo-card-type { position: absolute; left: 7%; right: 7%; top: 56.2%; margin: 0; font: 400 2.7cqw/1 \"DM Mono\", ui-monospace, Menlo, monospace; letter-spacing: .14em; text-transform: uppercase; color: #fbf6ea; opacity: .9; }\n  .vs-holo-card-text { position: absolute; left: 10%; right: 10%; top: 63%; bottom: 11%; display: flex; flex-direction: column; justify-content: center; gap: 2.6cqw; }\n  .vs-holo-card-text p { margin: 0; }\n  .vs-holo-card-text b { font-weight: 600; }\n  .vs-holo-card-flavor { font-size: 3cqw; opacity: .62; padding-top: 2.4cqw; border-top: .5px solid rgba(23,20,15,.25); }\n  .vs-holo-card-foot { position: absolute; left: 7%; right: 7%; bottom: 3.6%; display: flex; justify-content: space-between; font: 400 2.4cqw/1 \"DM Mono\", ui-monospace, Menlo, monospace; letter-spacing: .06em; color: #fbf6ea; opacity: .85; }\n  /* no WebGL: a printed card with a CSS foil */\n  .vs-holo-card[data-vs-state=\"nogl\"] .vs-holo-card-gl { display: none; }\n  .vs-holo-card[data-vs-state=\"nogl\"] .vs-holo-card-tilt { background:\n    linear-gradient(115deg, transparent 20%, rgba(255,120,200,.35), rgba(120,220,255,.35), rgba(255,240,140,.35), transparent 80%) 0 0 / 100% 100%,\n    linear-gradient(#0e2a3a, #2c5d6b 30%, #f2b872 52%, #f1ece2 52%, #f1ece2 92%, var(--vs-holo-card-accent) 92%); }\n  @media (prefers-reduced-motion: reduce) { .vs-holo-card-face { transition: none; } }\n</style>\n\n<script type=\"module\">\nconst VERT = 'attribute vec2 p; varying vec2 vUv; void main(){ vUv = p * 0.5 + 0.5; gl_Position = vec4(p, 0.0, 1.0); }';\nconst FRAG = /* glsl */`\nprecision highp float;\nvarying vec2 vUv;\nuniform vec2 uRes, uTilt, uPtr; uniform float uTime, uFoil, uCov, uInt, uHover, uBack; uniform vec3 uAccent;\nconst float AR = 63.0 / 88.0;\nfloat hash(vec2 p){ p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); }\nfloat pw(float x, float e){ return pow(max(x, 0.0), e); }\nfloat box(vec2 p, vec2 a, vec2 b, float aa){ vec2 d = max(a - p, p - b); return 1.0 - smoothstep(-aa, aa, max(d.x, d.y)); }\nvec3 spectrum(float h){ return 0.5 + 0.5 * cos(6.28318 * (h + vec3(0.0, 0.33, 0.67))); }\nfloat lum(vec3 c){ return dot(c, vec3(0.299, 0.587, 0.114)); }\nvec3 screen(vec3 a, vec3 b){ return 1.0 - (1.0 - a) * (1.0 - b); }\n\n// the art: a ringed moon rising over salt dunes at dusk\nvec3 drawArt(vec2 a, float aa){\n  vec3 sky = mix(vec3(0.94, 0.66, 0.40), vec3(0.16, 0.36, 0.42), smoothstep(0.78, 0.18, a.y));\n  sky = mix(sky, vec3(0.05, 0.13, 0.19), smoothstep(0.35, 0.0, a.y));\n  vec2 c = vec2(0.56, 0.5); vec2 d = (a - c) * vec2(1.415, 1.0);\n  // stars\n  vec2 g = floor(a * vec2(70.0, 52.0)); vec2 f = fract(a * vec2(70.0, 52.0)) - 0.5;\n  float st = step(0.93, hash(g)) * smoothstep(0.12, 0.0, length(f)) * smoothstep(0.62, 0.1, a.y);\n  sky += st * 0.8;\n  // moon body with soft terminator and bands\n  float r = length(d); float R = 0.3;\n  float body = 1.0 - smoothstep(R - aa, R + aa, r);\n  vec3 moon = mix(vec3(0.99, 0.86, 0.62), vec3(0.93, 0.52, 0.36), smoothstep(-0.2, 0.25, d.y + d.x * 0.4));\n  moon *= 0.9 + 0.1 * sin(d.y * 38.0 + sin(d.x * 7.0) * 1.2);\n  moon *= 0.6 + 0.4 * smoothstep(0.3, -0.2, d.x + d.y * 0.3);\n  vec3 col = mix(sky + vec3(0.9, 0.55, 0.3) * 0.35 * exp(-r * r * 9.0), moon, body);\n  // the ring: an ellipse that passes in front below, behind above\n  vec2 rd = d * mat2(0.95, -0.31, 0.31, 0.95); float re = length(rd * vec2(1.0, 4.2));\n  float ring = smoothstep(0.022, 0.0, abs(re - 0.45)) * 0.9 + smoothstep(0.012, 0.0, abs(re - 0.51)) * 0.6;\n  float front = step(0.0, rd.y) + (1.0 - body);\n  col = mix(col, vec3(1.0, 0.9, 0.72), clamp(ring * min(front, 1.0), 0.0, 1.0));\n  // dunes, three ridges\n  for (int i = 0; i < 3; i++) {\n    float fi = float(i);\n    float h = 0.66 + fi * 0.09 + 0.035 * sin(a.x * (5.0 + fi * 3.0) + fi * 2.0) + 0.02 * sin(a.x * 13.0 + fi);\n    float m = smoothstep(h - aa, h + aa, a.y);\n    vec3 dc = mix(vec3(0.72, 0.44, 0.30), vec3(0.18, 0.2, 0.22), fi / 2.0);\n    dc *= 0.8 + 0.25 * smoothstep(h + 0.12, h, a.y);\n    col = mix(col, dc, m);\n  }\n  return col;\n}\n\nvec3 drawBack(vec2 p, float aa){\n  vec2 q = (p - 0.5) * vec2(AR, 1.0);\n  float r = length(q), an = atan(q.y, q.x);\n  vec3 base = mix(uAccent * 0.26, uAccent * 0.1, r * 1.4);\n  float guil = 0.5 + 0.5 * sin(r * 150.0 + 2.2 * sin(an * 10.0 + r * 24.0));\n  base += uAccent * 0.1 * smoothstep(0.7, 1.0, guil);\n  float em = smoothstep(0.012, 0.0, abs(r - 0.2)) + smoothstep(0.006, 0.0, abs(r - 0.225)) + (1.0 - smoothstep(-0.004, 0.004, sqrt(abs(q.x) + 1e-5) + sqrt(abs(q.y) + 1e-5) - 0.36));\n  base = mix(base, uAccent * 1.15, clamp(em, 0.0, 1.0));\n  float frame = 1.0 - box(p, vec2(0.05, 0.05 * AR), vec2(0.95, 1.0 - 0.05 * AR), aa);\n  return mix(base, uAccent * 0.8, frame);\n}\n\nvoid main(){\n  vec2 p = vec2(vUv.x, 1.0 - vUv.y);\n  if (uBack > 0.5) p.x = 1.0 - p.x; // the canvas is seen from behind: un-mirror it\n  float aa = 1.5 / uRes.y;\n  vec2 ptr = uPtr; if (uBack > 0.5) ptr.x = 1.0 - ptr.x;\n  float bx = 0.035, by = 0.035 * AR;\n  // regions\n  float art = box(p, vec2(0.07, 0.115), vec2(0.93, 0.55), aa);\n  float tbox = box(p, vec2(0.07, 0.605), vec2(0.93, 0.905), aa);\n  // the metal frame: brushed accent with a soft vertical falloff\n  float brush = hash(vec2(floor(p.y * 900.0), 3.0)) * 0.06;\n  vec3 metal = uAccent * (0.62 + 0.3 * smoothstep(1.0, 0.0, p.y) + brush);\n  metal = mix(metal * 0.55, metal, box(p, vec2(bx, by), vec2(1.0 - bx, 1.0 - by), aa) * 0.6 + 0.4);\n  vec3 col = metal;\n  // name bar and type strip, darker inlays\n  float bar = box(p, vec2(0.05, 0.04), vec2(0.95, 0.104), aa) + box(p, vec2(0.05, 0.553), vec2(0.95, 0.597), aa);\n  col = mix(col, mix(uAccent * 0.22, vec3(0.07), 0.5), clamp(bar, 0.0, 1.0) * 0.85);\n  // bevel line around the art\n  float bev = box(p, vec2(0.065, 0.111), vec2(0.935, 0.554), aa) - art;\n  col = mix(col, vec3(1.0, 0.96, 0.88), bev * 0.7);\n  vec2 ap = (p - vec2(0.07, 0.115)) / vec2(0.86, 0.435);\n  col = mix(col, drawArt(ap, aa * 2.2), art);\n  vec3 paper = vec3(0.95, 0.93, 0.88) * (0.96 + 0.04 * hash(floor(p * 400.0)));\n  col = mix(col, paper, tbox);\n  if (uBack > 0.5) col = drawBack(p, aa);\n\n  // ---- the foil\n  float tl = length(uTilt);\n  float amt = uInt * (0.3 + 0.7 * smoothstep(0.0, 0.7, tl));\n  float mask = uBack > 0.5 ? 0.55 : (uCov < 0.5 ? art : uCov < 1.5 ? (1.0 - art) * (1.0 - tbox * 0.65) : 1.0 - tbox * 0.5);\n  float hue = p.x * 0.55 + p.y * 0.9 + uTilt.x * 0.8 - uTilt.y * 0.6;\n  vec3 rb = spectrum(hue);\n  float pat;\n  if (uFoil < 0.5) {            // prism: fine diagonal lines under a sweeping band\n    float lines = 0.72 + 0.28 * sin((p.x - p.y * AR) * 520.0);\n    float band = exp(-pw(abs(fract(p.x * 0.6 + p.y * 0.8 - uTilt.x * 0.9 + uTilt.y * 0.7) - 0.5) * 4.0, 2.0));\n    pat = lines * (0.25 + 0.85 * band);\n  } else if (uFoil < 1.5) {     // galaxy: star cells that catch the light at their own angle\n    vec2 cell = p * vec2(46.0, 46.0 / AR); vec2 id = floor(cell), fc = fract(cell) - 0.5;\n    float h = hash(id); vec2 dir = vec2(cos(h * 6.283), sin(h * 6.283));\n    float catchL = pw(max(dot(normalize(uTilt + 1e-4), dir), 0.0), 6.0) * smoothstep(0.05, 0.4, tl);\n    float star = smoothstep(0.24, 0.0, length(fc)) + smoothstep(0.05, 0.0, abs(fc.x)) * smoothstep(0.4, 0.0, abs(fc.y)) * 0.5 + smoothstep(0.05, 0.0, abs(fc.y)) * smoothstep(0.4, 0.0, abs(fc.x)) * 0.5;\n    float sz = mix(0.6, 1.3, fract(h * 7.1));\n    pat = 0.3 + step(0.55, h) * star * sz * (0.7 + 3.2 * catchL);\n    rb = mix(spectrum(h * 3.0 + uTilt.x * 0.5), vec3(1.0), 0.25) * mix(0.8, 1.0, step(0.55, h));\n  } else {                      // etched: rings spreading from the light\n    float rr = length((p - (0.5 + uTilt * vec2(-0.6, 0.6))) * vec2(AR, 1.0));\n    pat = 0.35 + 0.65 * smoothstep(0.3, 1.0, sin(rr * 170.0));\n    hue = rr * 2.2 + uTilt.x * 0.6; rb = spectrum(hue);\n  }\n  vec3 foil = rb * pat;\n  vec3 holo = screen(col * mix(1.0, 0.72, mask * amt), foil * (0.45 + lum(col) * 0.55));\n  col = mix(col, holo, clamp(mask * amt, 0.0, 1.0));\n  // glare: a soft specular that follows the light, plus a wide sheen\n  float gd = length((p - ptr) * vec2(AR, 1.0));\n  col += vec3(1.0, 0.98, 0.94) * (exp(-gd * gd * 16.0) * 0.28 + exp(-gd * gd * 2.0) * 0.06) * (0.35 + 0.65 * uHover) * (0.5 + 0.5 * uInt);\n  // a hairline of light on the edge nearest the light\n  float edge = 1.0 - box(p, vec2(0.006, 0.006 * AR), vec2(0.994, 1.0 - 0.006 * AR), aa);\n  col += edge * 0.25 * smoothstep(0.5, 0.0, gd);\n  gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);\n}`;\n\nclass Spring {\n  constructor(x, k = 170, c = 16) { this.x = x; this.v = 0; this.t = x; this.k = k; this.c = c; }\n  step(dt) { const n = Math.ceil(dt / (1 / 240)); const h = dt / n; for (let i = 0; i < n; i++) { this.v += (-this.k * (this.x - this.t) - this.c * this.v) * h; this.x += this.v * h; } return this.x; }\n}\nconst FOILS = { prism: 0, galaxy: 1, etched: 2 }, COVS = { art: 0, reverse: 1, full: 2 };\nconst hex = (h) => { const m = /^#?([\\da-f]{2})([\\da-f]{2})([\\da-f]{2})$/i.exec(h.trim()); return m ? [1, 2, 3].map((i) => parseInt(m[i], 16) / 255) : [0.85, 0.7, 0.36]; };\n\nfunction mount(el) {\n  const tilt = el.querySelector('.vs-holo-card-tilt'), canvas = el.querySelector('.vs-holo-card-gl'), shadow = el.querySelector('.vs-holo-card-shadow');\n  const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;\n  const cs = () => getComputedStyle(el);\n  const S = { vis: true, raf: 0, last: 0, t: 0, hover: false, drag: null, back: false, maxT: parseFloat(cs().getPropertyValue('--vs-holo-card-tilt')) || 16,\n    int: parseFloat(cs().getPropertyValue('--vs-holo-card-intensity')) || 1, accent: hex(cs().getPropertyValue('--vs-holo-card-accent') || '#d8b25c'), accentTo: null };\n  S.accentTo = S.accent.slice();\n  const rx = new Spring(0, 170, 14), ry = new Spring(0, 170, 14), sc = new Spring(1, 300, 20), flip = new Spring(0, 90, 13), hov = new Spring(0, 120, 18);\n  const px = new Spring(0.5, 200, 24), py = new Spring(0.3, 200, 24);\n\n  // ---- GL\n  let gl = null, prog = null, U = {};\n  function initGL() {\n    gl = canvas.getContext('webgl', { antialias: false, alpha: false, premultipliedAlpha: false, preserveDrawingBuffer: false });\n    if (!gl) return false;\n    const sh = (t, src) => { const s = gl.createShader(t); gl.shaderSource(s, src); gl.compileShader(s); if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) { console.warn(gl.getShaderInfoLog(s)); return null; } return s; };\n    const v = sh(gl.VERTEX_SHADER, VERT), f = sh(gl.FRAGMENT_SHADER, FRAG); if (!v || !f) return false;\n    prog = gl.createProgram(); gl.attachShader(prog, v); gl.attachShader(prog, f); gl.linkProgram(prog); gl.useProgram(prog);\n    const b = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, b); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n    const loc = gl.getAttribLocation(prog, 'p'); gl.enableVertexAttribArray(loc); gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);\n    U = {}; ['uRes', 'uTilt', 'uPtr', 'uTime', 'uFoil', 'uCov', 'uInt', 'uHover', 'uBack', 'uAccent'].forEach((n) => U[n] = gl.getUniformLocation(prog, n));\n    return true;\n  }\n  if (!initGL()) { el.dataset.vsState = 'nogl'; }\n\n  function resize() {\n    const r = tilt.getBoundingClientRect(), w = tilt.offsetWidth, h = tilt.offsetHeight; if (!w) return;\n    const dpr = Math.min(devicePixelRatio, 2);\n    canvas.width = Math.round(w * dpr); canvas.height = Math.round(h * dpr);\n    S.w = w; S.h = h;\n  }\n\n  // ---- pointer\n  const local = (e) => { const r = el.getBoundingClientRect(), cx = r.left + r.width / 2, cy = r.top + r.height / 2; return { x: (e.clientX - cx) / (S.w / 2), y: (e.clientY - cy) / (S.h / 2) }; };\n  const aim = (e) => { const l = local(e); const nx = Math.max(-1, Math.min(1, l.x)), ny = Math.max(-1, Math.min(1, l.y)); ry.t = nx * S.maxT; rx.t = -ny * S.maxT; px.t = 0.5 + nx * 0.5; py.t = 0.5 + ny * 0.5; };\n  const onEnter = (e) => { if (e.pointerType === 'mouse') { S.hover = true; hov.t = 1; } };\n  const onMove = (e) => { if (e.pointerType === 'mouse' || S.drag) { S.hover = true; hov.t = 1; aim(e); } };\n  const onLeave = (e) => { if (e.pointerType === 'mouse' && !S.drag) { S.hover = false; hov.t = 0; rx.t = ry.t = 0; } };\n  const onDown = (e) => { S.drag = { x: e.clientX, y: e.clientY, m: 0 }; sc.t = 0.965; try { tilt.setPointerCapture(e.pointerId); } catch (_) {} if (e.pointerType !== 'mouse') { S.hover = true; hov.t = 1; aim(e); } };\n  const onDrag = (e) => { if (!S.drag) return; S.drag.m += Math.abs(e.clientX - S.drag.x) + Math.abs(e.clientY - S.drag.y); S.drag.x = e.clientX; S.drag.y = e.clientY; aim(e); };\n  const onUp = (e) => {\n    if (!S.drag) return; const tap = S.drag.m < 8; S.drag = null; sc.t = 1;\n    if (tap) doFlip();\n    if (e.pointerType !== 'mouse') { S.hover = false; hov.t = 0; rx.t = ry.t = 0; }\n  };\n  const onKey = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); sc.v -= 3; doFlip(); } };\n  function doFlip() { S.back = !S.back; flip.t = S.back ? 180 : 0; ry.v += (S.back ? 1 : -1) * 60; tilt.setAttribute('aria-pressed', S.back); }\n  el.addEventListener('pointerenter', onEnter); el.addEventListener('pointermove', onMove); el.addEventListener('pointerleave', onLeave);\n  tilt.addEventListener('pointerdown', onDown); tilt.addEventListener('pointermove', onDrag); tilt.addEventListener('pointerup', onUp); tilt.addEventListener('pointercancel', onUp);\n  tilt.addEventListener('keydown', onKey);\n\n  function frame(now) {\n    S.raf = 0; if (!S.vis) return;\n    const dt = Math.min(0.05, S.last ? (now - S.last) / 1000 : 0.016); S.last = now; S.t += dt;\n    // idle: a slow sway so the foil is never still\n    if (!S.hover && !S.drag && !reduced) { ry.t = Math.sin(S.t * 0.7) * S.maxT * 0.45; rx.t = Math.sin(S.t * 0.53 + 1) * S.maxT * 0.3; px.t = 0.5 + Math.sin(S.t * 0.7) * 0.35; py.t = 0.35 - Math.sin(S.t * 0.53 + 1) * 0.25; }\n    const X = rx.step(dt), Y = ry.step(dt), F = flip.step(dt), K = sc.step(dt), H = hov.step(dt);\n    const PX = px.step(dt), PY = py.step(dt);\n    const lift = 1 + H * 0.03;\n    tilt.style.transform = `rotateX(${X.toFixed(2)}deg) rotateY(${(Y + F).toFixed(2)}deg) scale(${(K * lift).toFixed(4)})`;\n    shadow.style.transform = `translate(${(-Y * 1.6).toFixed(1)}px, calc(-50% + ${(22 + X * 1.4 + H * 14).toFixed(1)}px)) scale(${(0.9 - H * 0.02).toFixed(3)})`;\n    shadow.style.opacity = (0.62 - H * 0.12).toFixed(3);\n    const yy = ((Y + F) % 360 + 360) % 360, isBack = yy > 90 && yy < 270;\n    el.classList.toggle('is-back', isBack);\n    for (let i = 0; i < 3; i++) S.accent[i] += (S.accentTo[i] - S.accent[i]) * (1 - Math.exp(-dt * 8));\n    if (gl && !gl.isContextLost()) {\n      gl.viewport(0, 0, canvas.width, canvas.height);\n      gl.uniform2f(U.uRes, canvas.width, canvas.height);\n      gl.uniform2f(U.uTilt, Y / S.maxT, X / S.maxT);\n      gl.uniform2f(U.uPtr, PX, PY);\n      gl.uniform1f(U.uTime, S.t); gl.uniform1f(U.uFoil, FOILS[el.dataset.foil] ?? 0); gl.uniform1f(U.uCov, COVS[el.dataset.coverage] ?? 0);\n      gl.uniform1f(U.uInt, S.int); gl.uniform1f(U.uHover, H); gl.uniform1f(U.uBack, isBack ? 1 : 0); gl.uniform3fv(U.uAccent, S.accent);\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n    }\n    loop();\n  }\n  const loop = () => { if (!S.raf && S.vis) S.raf = requestAnimationFrame(frame); };\n  const ro = new ResizeObserver(resize); ro.observe(tilt); resize();\n  const io = new IntersectionObserver(([e]) => { S.vis = e.isIntersecting; if (S.vis) { S.last = 0; loop(); } }); io.observe(el);\n  const onVis = () => { S.vis = !document.hidden; if (S.vis) { S.last = 0; loop(); } };\n  document.addEventListener('visibilitychange', onVis);\n  canvas.addEventListener('webglcontextlost', (e) => { e.preventDefault(); });\n  canvas.addEventListener('webglcontextrestored', () => { initGL(); });\n  el.dataset.vsState = gl ? 'ready' : 'nogl';\n  loop();\n\n  function set(o = {}) {\n    if (o.foil in FOILS) el.dataset.foil = o.foil;\n    if (o.coverage in COVS) el.dataset.coverage = o.coverage;\n    if (o.accent) { el.style.setProperty('--vs-holo-card-accent', o.accent); S.accentTo = hex(o.accent); }\n    if (o.intensity != null) { S.int = +o.intensity; el.style.setProperty('--vs-holo-card-intensity', o.intensity); }\n    if (o.tilt != null) { S.maxT = +o.tilt; el.style.setProperty('--vs-holo-card-tilt', o.tilt); }\n    sc.v -= 1.5; // a small breath on change\n  }\n  function destroy() {\n    cancelAnimationFrame(S.raf); S.vis = false; ro.disconnect(); io.disconnect(); document.removeEventListener('visibilitychange', onVis);\n    el.removeEventListener('pointerenter', onEnter); el.removeEventListener('pointermove', onMove); el.removeEventListener('pointerleave', onLeave);\n    tilt.removeEventListener('pointerdown', onDown); tilt.removeEventListener('pointermove', onDrag); tilt.removeEventListener('pointerup', onUp); tilt.removeEventListener('pointercancel', onUp); tilt.removeEventListener('keydown', onKey);\n    if (gl) gl.getExtension('WEBGL_lose_context')?.loseContext(); delete el.vsHoloCard;\n  }\n  return { set, flip: doFlip, destroy };\n}\n\ndocument.querySelectorAll('.vs-holo-card').forEach((el) => { if (!el.vsHoloCard) el.vsHoloCard = mount(el); });\n</script>\n"
    }
  ],
  "meta": {
    "category": "3d",
    "group": "3D objects",
    "tags": [
      "card",
      "holographic",
      "foil",
      "tilt",
      "flip",
      "shader",
      "trading card"
    ],
    "tech": [
      "GLSL",
      "JS",
      "CSS"
    ],
    "deps": [],
    "source": null,
    "free": true,
    "accent": "#d8b25c",
    "added": "2026-10-01",
    "demo": "https://vividsites.app/api/component/holo-card/index.html",
    "thumbnail": "https://vividsites.app/api/component/holo-card/thumb.jpg",
    "video": "https://vividsites.app/api/component/holo-card/thumb.mp4",
    "page": "https://vividsites.app/c/holo-card"
  }
}