File size: 1,754 Bytes
38bc0dc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | import { useRef, type ReactNode, type MouseEvent } from "react";
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
interface TiltPanelProps {
children: ReactNode;
className?: string;
/** max tilt in degrees */
maxTilt?: number;
}
/**
* Volumetric glass panel: tilts toward the cursor in 3D and carries a
* light "sheen" reflection that pans across the surface as you move.
*/
export default function TiltPanel({ children, className = "", maxTilt = 7 }: TiltPanelProps) {
const ref = useRef<HTMLDivElement>(null);
const mx = useMotionValue(0.5);
const my = useMotionValue(0.5);
const springCfg = { stiffness: 120, damping: 16, mass: 0.6 };
const sx = useSpring(mx, springCfg);
const sy = useSpring(my, springCfg);
const rotateY = useTransform(sx, [0, 1], [-maxTilt, maxTilt]);
const rotateX = useTransform(sy, [0, 1], [maxTilt, -maxTilt]);
const sheenX = useTransform(sx, [0, 1], ["-30%", "130%"]);
const sheenY = useTransform(sy, [0, 1], ["-30%", "130%"]);
function handleMouseMove(e: MouseEvent<HTMLDivElement>) {
const el = ref.current;
if (!el) return;
const rect = el.getBoundingClientRect();
mx.set((e.clientX - rect.left) / rect.width);
my.set((e.clientY - rect.top) / rect.height);
}
function handleMouseLeave() {
mx.set(0.5);
my.set(0.5);
}
return (
<motion.div
ref={ref}
className={`tilt-panel ${className}`}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
style={{ rotateX, rotateY, transformStyle: "preserve-3d" }}
>
<motion.div
className="tilt-sheen"
style={{ left: sheenX, top: sheenY }}
aria-hidden
/>
{children}
</motion.div>
);
}
|