effects#3d#perspective

3D Card Tilt

Cards that tilt in 3D space following cursor position with realistic glare effects

Implementation Guide
# 3D Card Tilt Effect

> Cards that tilt in 3D space following cursor position, with subtle lighting/glare effects that shift. Creates depth perception without WebGL.

## Quick Start

```tsx
import ThreeDCardTilt from '@/components/effects/3d-card-tilt';

export default function Features() {
  return (
    <div className="h-screen">
      <ThreeDCardTilt maxTilt={15} glare={true} />
    </div>
  );
}
```

## Props

| Prop         | Type    | Default | Description                                  |
| ------------ | ------- | ------- | -------------------------------------------- |
| maxTilt      | number  | 15      | Maximum tilt angle in degrees (5 - 25)       |
| perspective  | number  | 1000    | CSS perspective value in pixels (500 - 2000) |
| scale        | number  | 1.05    | Scale multiplier on hover (1.0 - 1.2)        |
| glare        | boolean | true    | Enable glare/shine overlay effect            |
| glareOpacity | number  | 0.3     | Maximum opacity of glare effect (0.1 - 0.5)  |

## Full Implementation

```tsx
'use client';

import { useCallback, useEffect, useRef, useState } from 'react';

interface TiltCardProps {
  maxTilt?: number;
  perspective?: number;
  scale?: number;
  glare?: boolean;
  glareOpacity?: number;
  children: React.ReactNode;
  className?: string;
}

interface TiltState {
  rotateX: number;
  rotateY: number;
  glareX: number;
  glareY: number;
}

function TiltCard({
  maxTilt = 15,
  perspective = 1000,
  scale = 1.05,
  glare = true,
  glareOpacity = 0.3,
  children,
  className = '',
}: TiltCardProps) {
  const cardRef = useRef<HTMLDivElement>(null);
  const [tilt, setTilt] = useState<TiltState>({
    rotateX: 0,
    rotateY: 0,
    glareX: 50,
    glareY: 50,
  });
  const [isHovering, setIsHovering] = useState(false);
  const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);

  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
    setPrefersReducedMotion(mediaQuery.matches);

    const handleChange = (event: MediaQueryListEvent) => {
      setPrefersReducedMotion(event.matches);
    };

    mediaQuery.addEventListener('change', handleChange);
    return () => mediaQuery.removeEventListener('change', handleChange);
  }, []);

  const handleMouseMove = useCallback(
    (event: React.MouseEvent<HTMLDivElement>) => {
      if (!cardRef.current || prefersReducedMotion) return;

      const rect = cardRef.current.getBoundingClientRect();
      const centerX = rect.left + rect.width / 2;
      const centerY = rect.top + rect.height / 2;

      // Calculate mouse position relative to card center
      const mouseX = event.clientX - centerX;
      const mouseY = event.clientY - centerY;

      // Map to rotation values
      const rotateY = (mouseX / (rect.width / 2)) * maxTilt;
      const rotateX = -(mouseY / (rect.height / 2)) * maxTilt;

      // Calculate glare position (follows cursor)
      const glareX = ((event.clientX - rect.left) / rect.width) * 100;
      const glareY = ((event.clientY - rect.top) / rect.height) * 100;

      setTilt({ rotateX, rotateY, glareX, glareY });
    },
    [maxTilt, prefersReducedMotion]
  );

  const handleMouseEnter = useCallback(() => {
    setIsHovering(true);
  }, []);

  const handleMouseLeave = useCallback(() => {
    setIsHovering(false);
    setTilt({ rotateX: 0, rotateY: 0, glareX: 50, glareY: 50 });
  }, []);

  const cardStyle: React.CSSProperties = {
    perspective: `${perspective}px`,
  };

  const innerStyle: React.CSSProperties = {
    transform: prefersReducedMotion
      ? `scale(${isHovering ? scale : 1})`
      : `rotateX(${tilt.rotateX}deg) rotateY(${tilt.rotateY}deg) scale(${isHovering ? scale : 1})`,
    transformStyle: 'preserve-3d',
    transition: 'transform 0.15s ease-out',
  };

  const glareStyle: React.CSSProperties = {
    position: 'absolute',
    inset: 0,
    background: `radial-gradient(circle at ${tilt.glareX}% ${tilt.glareY}%, rgba(255, 255, 255, ${isHovering ? glareOpacity : 0}) 0%, transparent 60%)`,
    pointerEvents: 'none',
    transition: 'opacity 0.3s ease-out',
    opacity: isHovering ? 1 : 0,
  };

  return (
    <div style={cardStyle} className={className}>
      <div
        ref={cardRef}
        style={innerStyle}
        onMouseMove={handleMouseMove}
        onMouseEnter={handleMouseEnter}
        onMouseLeave={handleMouseLeave}
        className="relative"
      >
        {children}
        {glare && !prefersReducedMotion && <div style={glareStyle} />}
      </div>
    </div>
  );
}

// Example usage with demo cards
export default function ThreeDCardTilt({
  maxTilt = 15,
  perspective = 1000,
  scale = 1.05,
  glare = true,
  glareOpacity = 0.3,
}: ThreeDCardTiltProps) {
  return (
    <div className="flex h-full w-full items-center justify-center">
      <TiltCard
        maxTilt={maxTilt}
        perspective={perspective}
        scale={scale}
        glare={glare}
        glareOpacity={glareOpacity}
      >
        <div className="border border-zinc-700 bg-zinc-800 p-6">
          <h3 className="font-semibold text-white">Your Card Content</h3>
          <p className="text-zinc-400">Hover to see the 3D tilt effect</p>
        </div>
      </TiltCard>
    </div>
  );
}
```

## Customization

### Subtle Professional Effect

For a more subtle, professional feel:

```tsx
<ThreeDCardTilt maxTilt={8} scale={1.02} glareOpacity={0.15} />
```

### Dramatic 3D Effect

For a more dramatic, playful effect:

```tsx
<ThreeDCardTilt maxTilt={25} perspective={800} scale={1.1} glareOpacity={0.4} />
```

### No Glare (Clean Look)

For a clean tilt without the glare overlay:

```tsx
<ThreeDCardTilt glare={false} maxTilt={12} />
```

### Custom Card Content

Use the `TiltCard` component directly for custom content:

```tsx
import { TiltCard } from '@/components/effects/3d-card-tilt';

function ProductCard({ product }) {
  return (
    <TiltCard maxTilt={10} glare={true}>
      <div className="rounded-lg bg-white p-4 shadow-lg">
        <img src={product.image} alt={product.name} />
        <h3>{product.name}</h3>
        <p>{product.price}</p>
      </div>
    </TiltCard>
  );
}
```

### Adjusting Transition Speed

Modify the transition timing for different feels:

```tsx
// In the innerStyle object:
const innerStyle: React.CSSProperties = {
  transform: `rotateX(${tilt.rotateX}deg) rotateY(${tilt.rotateY}deg)`,
  transformStyle: 'preserve-3d',
  transition: 'transform 0.3s cubic-bezier(0.03, 0.98, 0.52, 0.99)', // Smoother, slower
};
```

## Performance Considerations

- **GPU Acceleration**: The effect uses CSS transforms which are GPU-accelerated
- **No requestAnimationFrame**: Uses React state with mouse events for simplicity
- **Throttling**: Consider throttling `handleMouseMove` if experiencing performance issues on many cards
- **Mobile**: Touch events are not handled by default - the effect is primarily for desktop hover interactions
- **Many Cards**: For grids with 10+ cards, consider reducing `maxTilt` and disabling `glare`

### Throttled Version for Performance

```tsx
import { useCallback, useRef } from 'react';

function useThrottledCallback<T extends (...args: any[]) => void>(callback: T, delay: number): T {
  const lastRun = useRef(Date.now());

  return useCallback(
    ((...args) => {
      if (Date.now() - lastRun.current >= delay) {
        callback(...args);
        lastRun.current = Date.now();
      }
    }) as T,
    [callback, delay]
  );
}

// Use in component:
const throttledMouseMove = useThrottledCallback(handleMouseMove, 16); // ~60fps
```

## Accessibility

- **Reduced Motion**: Fully respects `prefers-reduced-motion` media query
  - When enabled: Tilt and glare effects are disabled
  - Scale effect remains as a subtle hover indicator
- **Keyboard Navigation**: Cards remain fully interactive via keyboard
- **Screen Readers**: Effect is purely visual; ensure card content is properly structured
- **Focus States**: Add visible focus indicators for keyboard users:

```tsx
<TiltCard className="focus-within:ring-2 focus-within:ring-blue-500">
  <button>Interactive content</button>
</TiltCard>
```

## Browser Support

- **Modern Browsers**: Full support in Chrome, Firefox, Safari, Edge
- **CSS Transform 3D**: Required for the perspective effect
- **CSS Custom Properties**: Used for dynamic styling
- **Pointer Events**: Required for glare positioning

| Browser | Version | Support |
| ------- | ------- | ------- |
| Chrome  | 36+     | Full    |
| Firefox | 16+     | Full    |
| Safari  | 9+      | Full    |
| Edge    | 12+     | Full    |
| IE      | -       | None    |