# Tilt Card With a Moving Glare Layer

A shadcn/ui block from the [@weinc](https://we.inc/components?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/weinc/tilt-card-with-a-moving-glare-layer).

- Page, with a live preview: https://shadcn.exchange/r/weinc/tilt-card-with-a-moving-glare-layer
- Screenshot: https://shadcn.exchange/shot/weinc/tilt-card-with-a-moving-glare-layer/light.png
- Registry item: https://we.inc/r/tilt-card-with-a-moving-glare-layer.json
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/weinc/tilt-card-with-a-moving-glare-layer.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Files

### `components/we-inc/tilt-card-with-a-moving-glare-layer.tsx`

```tsx
import { useEffect, useRef } from "react";

const MAX_TILT = 10; // degrees at the card's edge
const GLARE = 0.55; // brightest glare opacity

export default function TiltCardWithAMovingGlareLayer() {
  const root = useRef<HTMLElement>(null);
  const card = useRef<HTMLAnchorElement>(null);
  const glare = useRef<HTMLSpanElement>(null);

  useEffect(() => {
    const el = root.current, c = card.current, g = glare.current;
    if (!el || !c || !g) return;
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    const fine = window.matchMedia("(hover: hover) and (pointer: fine)").matches;
    let tx = 0, ty = 0, x = 0, y = 0, live = false, frame = 0, visible = true;
    const move = (e: PointerEvent) => {
      if (e.pointerType !== "mouse") return;
      const r = c.getBoundingClientRect();
      // -1..1 across the card, clamped so the tilt eases off outside it
      tx = Math.max(-1, Math.min(1, ((e.clientX - r.left) / r.width) * 2 - 1));
      ty = Math.max(-1, Math.min(1, ((e.clientY - r.top) / r.height) * 2 - 1));
      live = true;
    };
    const leave = () => { live = false; };
    const tick = (now: number) => {
      if (!live) { tx = Math.sin(now / 2100) * 0.8; ty = Math.sin(now / 1500 + 1) * 0.6; }
      x += (tx - x) * 0.08;
      y += (ty - y) * 0.08;
      c.style.transform = `rotateX(${(-y * MAX_TILT).toFixed(2)}deg) rotateY(${(x * MAX_TILT).toFixed(2)}deg)`;
      g.style.transform = `translate(${(x * 30).toFixed(1)}%, ${(y * 30).toFixed(1)}%)`;
      g.style.opacity = (GLARE * (0.35 + 0.65 * Math.min(1, Math.hypot(x, y)))).toFixed(3);
      frame = requestAnimationFrame(tick);
    };
    const play = () => { if (!frame && visible) frame = requestAnimationFrame(tick); };
    const stop = () => { cancelAnimationFrame(frame); frame = 0; };
    const io = new IntersectionObserver(([en]) => { visible = en.isIntersecting; if (visible) play(); else stop(); });
    io.observe(el);
    if (fine) { el.addEventListener("pointermove", move); el.addEventListener("pointerleave", leave); }
    play();
    return () => {
      stop();
      io.disconnect();
      el.removeEventListener("pointermove", move);
      el.removeEventListener("pointerleave", leave);
    };
  }, []);

  return (
    <section ref={root} className="bg-[#26282b] px-5 py-16 sm:py-20">
      <style>{`
        .w3tlt-stage { perspective: 900px; }
        .w3tlt-card { transform-style: preserve-3d; will-change: transform; }
        .w3tlt-glare { opacity: .25; background: radial-gradient(circle at 50% 50%, rgba(255,255,255,.55), rgba(255,255,255,0) 45%); }
        .w3tlt-card:focus-visible { outline: 2px solid #f1efe9; outline-offset: 6px; }
        @media (prefers-reduced-motion: reduce) { .w3tlt-card { transform: rotateX(4deg) rotateY(-6deg) !important; will-change: auto; } .w3tlt-glare { transform: translate(-15%, -15%) !important; } }
      `}</style>
      <div className="mx-auto grid max-w-5xl items-center gap-12 md:grid-cols-2">
        <div className="text-[#f1efe9]">
          <p className="text-xs font-semibold uppercase tracking-[0.25em] text-[#b5b0a5]">Ashgrove Bouldering</p>
          <h2 className="mt-4 text-3xl font-semibold leading-tight tracking-tight sm:text-4xl">One card for every wall in the city.</h2>
          <p className="mt-5 max-w-md text-base leading-relaxed text-[#c9c5bc]">Unlimited visits to our three gyms, two guest passes a month and shoe hire included.</p>
        </div>
        <div className="w3tlt-stage flex justify-center">
          <a
            ref={card}
            href="#"
            aria-label="Ashgrove all-gyms membership, 58 dollars a month, view details"
            className="w3tlt-card relative block aspect-[1.586] w-full max-w-sm overflow-hidden rounded-2xl bg-gradient-to-br from-[#4a4d52] via-[#2f3135] to-[#1b1c1f] p-6 text-[#f1efe9] shadow-[0_30px_60px_-20px_rgba(0,0,0,0.6)] ring-1 ring-white/10"
          >
            <span aria-hidden="true" className="absolute inset-0 opacity-30" style={{ backgroundImage: "repeating-linear-gradient(115deg, rgba(255,255,255,0.05) 0 2px, transparent 2px 9px)" }} />
            <span aria-hidden="true" className="relative flex h-full flex-col justify-between">
              <span className="flex items-start justify-between">
                <span className="text-sm font-semibold uppercase tracking-[0.2em]">Ashgrove</span>
                <svg viewBox="0 0 32 24" className="h-6 w-8" fill="none" stroke="#d6c49a" strokeWidth="1.6"><path d="M2 22 L11 6 L17 15 L21 9 L30 22 Z" /></svg>
              </span>
              <span>
                <span className="block text-xs uppercase tracking-[0.2em] text-[#c9c5bc]">All gyms · monthly</span>
                <span className="mt-1 flex items-end justify-between">
                  <span className="text-2xl font-semibold">$58</span>
                  <span className="font-mono text-sm tracking-widest text-[#d6c49a]">No. 0412</span>
                </span>
              </span>
            </span>
            <span ref={glare} aria-hidden="true" className="w3tlt-glare pointer-events-none absolute -inset-1/2" />
          </a>
        </div>
      </div>
    </section>
  );
}
```
