Logo

Tilt Card

Hover me

Code

"use client";

import { useState, MouseEvent, useCallback, useRef } from "react";

function throttle<T extends (...args: any[]) => any>(
  func: T,
  delay: number,
): (...args: Parameters<T>) => void {
  let lastCall = 0;
  return (...args: Parameters<T>) => {
    const now = new Date().getTime();
    if (now - lastCall < delay) {
      return;
    }
    lastCall = now;
    return func(...args);
  };
}

const TiltCard = () => {
  const [rotate, setRotate] = useState({ x: 0, y: 0 });
  const [opacity, setOpacity] = useState(0);
  const [position, setPosition] = useState({ x: 0, y: 0 });

  const divRef = useRef<HTMLDivElement>(null);

  const onMouseMove = useCallback(
    throttle((e: MouseEvent<HTMLDivElement>) => {
      const card = e.currentTarget;
      const box = card.getBoundingClientRect();
      const x = e.clientX - box.left;
      const y = e.clientY - box.top;
      const centerX = box.width / 2;
      const centerY = box.height / 2;
      const rotateX = (y - centerY) / 4;
      const rotateY = (centerX - x) / 4;

      setRotate({ x: rotateX, y: rotateY });
      setPosition({ x, y });
    }, 100),
    [],
  );

  const onMouseLeave = useCallback(() => {
    setRotate({ x: 0, y: 0 });
    setOpacity(0);
  }, []);

  const handleMouseEnter = useCallback(() => {
    setOpacity(0.1);
  }, []);

  return (
    <div
      ref={divRef}
      className="card relative aspect-square h-auto w-1/2 max-w-36 min-w-28 transition-all duration-200 ease-in-out will-change-transform"
      onMouseMove={onMouseMove}
      onMouseLeave={onMouseLeave}
      onMouseEnter={handleMouseEnter}
      style={{
        transform: `perspective(1000px) rotateX(${-rotate.x}deg) rotateY(${-rotate.y}deg) scale3d(1, 1, 1)`,
      }}
    >
      <div className="group relative flex h-full w-full select-none items-center justify-center rounded-lg border bg-gradient-to-tr from-popover to-accent">
        <div
          className="pointer-events-none absolute -inset-px opacity-0 transition duration-200"
          style={{
            opacity,
            background: `radial-gradient(360px circle at ${position.x}px ${position.y}px, hsla(var(--primary)), transparent 40%)`,
          }}
        />
        <code className="text-md inline-block bg-gradient-to-br from-foreground to-primary bg-clip-text font-medium text-transparent">
          Hover me
        </code>
      </div>
    </div>
  );
};

export default TiltCard;