113 lines
2.9 KiB
TypeScript
113 lines
2.9 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
import {
|
|
motion,
|
|
useMotionValue,
|
|
useSpring,
|
|
type SpringOptions,
|
|
} from 'motion/react';
|
|
|
|
const SPRING_CONFIG = { stiffness: 26.7, damping: 4.1, mass: 0.2 };
|
|
|
|
export type MagneticProps = {
|
|
children: React.ReactNode;
|
|
intensity?: number;
|
|
range?: number;
|
|
actionArea?: 'self' | 'parent' | 'global';
|
|
springOptions?: SpringOptions;
|
|
};
|
|
|
|
export function Magnetic({
|
|
children,
|
|
intensity = 0.6,
|
|
range = 100,
|
|
actionArea = 'self',
|
|
springOptions = SPRING_CONFIG,
|
|
}: MagneticProps) {
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
const x = useMotionValue(0);
|
|
const y = useMotionValue(0);
|
|
|
|
const springX = useSpring(x, springOptions);
|
|
const springY = useSpring(y, springOptions);
|
|
|
|
useEffect(() => {
|
|
const calculateDistance = (e: MouseEvent) => {
|
|
if (ref.current) {
|
|
const rect = ref.current.getBoundingClientRect();
|
|
const centerX = rect.left + rect.width / 2;
|
|
const centerY = rect.top + rect.height / 2;
|
|
const distanceX = e.clientX - centerX;
|
|
const distanceY = e.clientY - centerY;
|
|
|
|
const absoluteDistance = Math.sqrt(distanceX ** 2 + distanceY ** 2);
|
|
|
|
if (isHovered && absoluteDistance <= range) {
|
|
const scale = 1 - absoluteDistance / range;
|
|
x.set(distanceX * intensity * scale);
|
|
y.set(distanceY * intensity * scale);
|
|
} else {
|
|
x.set(0);
|
|
y.set(0);
|
|
}
|
|
}
|
|
};
|
|
|
|
document.addEventListener('mousemove', calculateDistance);
|
|
|
|
return () => {
|
|
document.removeEventListener('mousemove', calculateDistance);
|
|
};
|
|
}, [ref, isHovered, intensity, range]);
|
|
|
|
useEffect(() => {
|
|
if (actionArea === 'parent' && ref.current?.parentElement) {
|
|
const parent = ref.current.parentElement;
|
|
|
|
const handleParentEnter = () => setIsHovered(true);
|
|
const handleParentLeave = () => setIsHovered(false);
|
|
|
|
parent.addEventListener('mouseenter', handleParentEnter);
|
|
parent.addEventListener('mouseleave', handleParentLeave);
|
|
|
|
return () => {
|
|
parent.removeEventListener('mouseenter', handleParentEnter);
|
|
parent.removeEventListener('mouseleave', handleParentLeave);
|
|
};
|
|
} else if (actionArea === 'global') {
|
|
setIsHovered(true);
|
|
}
|
|
}, [actionArea]);
|
|
|
|
const handleMouseEnter = () => {
|
|
if (actionArea === 'self') {
|
|
setIsHovered(true);
|
|
}
|
|
};
|
|
|
|
const handleMouseLeave = () => {
|
|
if (actionArea === 'self') {
|
|
setIsHovered(false);
|
|
x.set(0);
|
|
y.set(0);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<motion.div
|
|
ref={ref}
|
|
onMouseEnter={actionArea === 'self' ? handleMouseEnter : undefined}
|
|
onMouseLeave={actionArea === 'self' ? handleMouseLeave : undefined}
|
|
style={{
|
|
x: springX,
|
|
y: springY,
|
|
}}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
);
|
|
}
|