readysite / website / frontend / components / TiltCard.jsx
2.5 KB
TiltCard.jsx
import * as React from 'react';
import { useRef, useCallback } from 'react';

// Balatro-style 3D tilt card with glossy shine effect.
// Wraps children and adds mouse-tracking perspective tilt + moving gloss overlay.
export function TiltCard({ children, className = '', style = {}, as: Tag = 'div', ...props }) {
  const cardRef = useRef(null);
  const glowRef = useRef(null);
  const rafRef = useRef(null);

  const handleMouseMove = useCallback((e) => {
    const card = cardRef.current;
    const glow = glowRef.current;
    if (!card || !glow) return;

    if (rafRef.current) cancelAnimationFrame(rafRef.current);
    rafRef.current = requestAnimationFrame(() => {
      const rect = card.getBoundingClientRect();
      const x = e.clientX - rect.left;
      const y = e.clientY - rect.top;
      const centerX = rect.width / 2;
      const centerY = rect.height / 2;

      // Tilt: max 8 degrees
      const rotateY = ((x - centerX) / centerX) * 8;
      const rotateX = ((centerY - y) / centerY) * 8;

      card.style.transform = `perspective(600px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale(1.02)`;

      // Shine position follows cursor
      const percentX = (x / rect.width) * 100;
      const percentY = (y / rect.height) * 100;
      glow.style.background = `radial-gradient(circle at ${percentX}% ${percentY}%, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0.05) 40%, transparent 70%)`;
      glow.style.opacity = '1';
    });
  }, []);

  const handleMouseLeave = useCallback(() => {
    const card = cardRef.current;
    const glow = glowRef.current;
    if (rafRef.current) cancelAnimationFrame(rafRef.current);
    if (card) card.style.transform = 'perspective(600px) rotateX(0deg) rotateY(0deg) scale(1)';
    if (glow) glow.style.opacity = '0';
  }, []);

  return (
    <Tag
      ref={cardRef}
      className={className}
      style={{
        ...style,
        transition: 'transform 0.25s cubic-bezier(0.03, 0.98, 0.52, 0.99)',
        transformStyle: 'preserve-3d',
        willChange: 'transform',
        position: 'relative',
        overflow: 'hidden',
      }}
      onMouseMove={handleMouseMove}
      onMouseLeave={handleMouseLeave}
      {...props}
    >
      {children}
      <div
        ref={glowRef}
        style={{
          position: 'absolute',
          inset: 0,
          pointerEvents: 'none',
          opacity: 0,
          transition: 'opacity 0.3s ease',
          borderRadius: 'inherit',
          zIndex: 1,
        }}
      />
    </Tag>
  );
}
← Back