A magnetic effect for elements that allows them to be attracted to the mouse cursor.
1'use client';
2
3import React, { useState, useEffect, useRef } from 'react';
4import {
5 motion,
6 useMotionValue,
7 useSpring,
8 type SpringOptions,
9} from 'motion/react';
10
11const SPRING_CONFIG = { stiffness: 26.7, damping: 4.1, mass: 0.2 };
12
13export type MagneticProps = {
14 children: React.ReactNode;
15 intensity?: number;
16 range?: number;
17 actionArea?: 'self' | 'parent' | 'global';
18 springOptions?: SpringOptions;
19};
20
21export function Magnetic({
22 children,
23 intensity = 0.6,
24 range = 100,
25 actionArea = 'self',
26 springOptions = SPRING_CONFIG,
27}: MagneticProps) {
28 const [isHovered, setIsHovered] = useState(false);
29 const ref = useRef<HTMLDivElement>(null);
30
31 const x = useMotionValue(0);
32 const y = useMotionValue(0);
33
34 const springX = useSpring(x, springOptions);
35 const springY = useSpring(y, springOptions);
36
37 useEffect(() => {
38 const calculateDistance = (e: MouseEvent) => {
39 if (ref.current) {
40 const rect = ref.current.getBoundingClientRect();
41 const centerX = rect.left + rect.width / 2;
42 const centerY = rect.top + rect.height / 2;
43 const distanceX = e.clientX - centerX;
44 const distanceY = e.clientY - centerY;
45
46 const absoluteDistance = Math.sqrt(distanceX ** 2 + distanceY ** 2);
47
48 if (isHovered && absoluteDistance <= range) {
49 const scale = 1 - absoluteDistance / range;
50 x.set(distanceX * intensity * scale);
51 y.set(distanceY * intensity * scale);
52 } else {
53 x.set(0);
54 y.set(0);
55 }
56 }
57 };
58
59 document.addEventListener('mousemove', calculateDistance);
60
61 return () => {
62 document.removeEventListener('mousemove', calculateDistance);
63 };
64 }, [ref, isHovered, intensity, range]);
65
66 useEffect(() => {
67 if (actionArea === 'parent' && ref.current?.parentElement) {
68 const parent = ref.current.parentElement;
69
70 const handleParentEnter = () => setIsHovered(true);
71 const handleParentLeave = () => setIsHovered(false);
72
73 parent.addEventListener('mouseenter', handleParentEnter);
74 parent.addEventListener('mouseleave', handleParentLeave);
75
76 return () => {
77 parent.removeEventListener('mouseenter', handleParentEnter);
78 parent.removeEventListener('mouseleave', handleParentLeave);
79 };
80 } else if (actionArea === 'global') {
81 setIsHovered(true);
82 }
83 }, [actionArea]);
84
85 const handleMouseEnter = () => {
86 if (actionArea === 'self') {
87 setIsHovered(true);
88 }
89 };
90
91 const handleMouseLeave = () => {
92 if (actionArea === 'self') {
93 setIsHovered(false);
94 x.set(0);
95 y.set(0);
96 }
97 };
98
99 return (
100 <motion.div
101 ref={ref}
102 onMouseEnter={actionArea === 'self' ? handleMouseEnter : undefined}
103 onMouseLeave={actionArea === 'self' ? handleMouseLeave : undefined}
104 style={{
105 x: springX,
106 y: springY,
107 }}
108 >
109 {children}
110 </motion.div>
111 );
112}
113