1import { Box } from '@mui/system';
2import { animate, motion, useSpring, useTransform } from 'framer-motion';
3import { useRef, useState, useEffect } from 'react';
4
5export interface XRayProps {
6 children: React.ReactNode;
7}
8
9const XRay = ({ children }: XRayProps) => {
10 const containerRef = useRef<HTMLElement>(null);
11 const mouseStartY = useRef(0);
12 const [scannerOpacity, setScannerOpacity] = useState(0);
13 const animationRef = useRef<any>(null);
14
15 const initialScannerPosition = 174;
16 const scanLineOffset = 2;
17
18 const y = useSpring(0, {
19 damping: 32,
20 stiffness: 500,
21 mass: 0.8,
22 });
23
24 const clipPath = useTransform(y, latest => `inset(${latest}px 0 0 0)`);
25
26 useEffect(() => {
27 if (animationRef.current) {
28 animationRef.current.stop();
29 }
30
31 const startAnimation = async () => {
32 if (containerRef.current) {
33 y.set(containerRef.current.clientHeight);
34
35 await new Promise(resolve => setTimeout(resolve, 300));
36 setScannerOpacity(1);
37 animationRef.current = animate(y, initialScannerPosition, {
38 type: 'spring',
39 stiffness: 350,
40 damping: 50,
41 mass: 0.3,
42 });
43 }
44 };
45
46 startAnimation();
47
48 return () => {
49 if (animationRef.current) {
50 animationRef.current.stop();
51 }
52 };
53 }, []);
54
55 const constrainValue = (value: number, [min, max]: any, multiplier = 2) => {
56 if (value > max) {
57 const diff = value - max;
58 return (
59 max + (diff > 0 ? Math.sqrt(diff) : -Math.sqrt(-diff)) * multiplier
60 );
61 }
62 if (value < min) {
63 const diff = value - min;
64 return (
65 min + (diff > 0 ? Math.sqrt(diff) : -Math.sqrt(-diff)) * multiplier
66 );
67 }
68 return value;
69 };
70
71 const handleMouseEnter = () => {
72 const { y: rectY } = containerRef.current!.getBoundingClientRect();
73 mouseStartY.current = rectY;
74 };
75
76 const handleMouseLeave = () => {
77 animate(y, initialScannerPosition, {
78 type: 'spring',
79 stiffness: 350,
80 damping: 50,
81 });
82 };
83
84 const handleMouseMove = (event: any) => {
85 let offset = event.clientY - mouseStartY.current;
86 offset = constrainValue(
87 offset,
88 [100, containerRef.current!.offsetHeight - 100],
89 6
90 );
91 y.set(offset);
92 };
93
94 return (
95 <Box
96 onMouseEnter={handleMouseEnter}
97 onMouseLeave={handleMouseLeave}
98 onMouseMove={handleMouseMove}
99 ref={containerRef}
100 sx={{
101 padding: '90px 48px',
102 position: 'relative',
103 border: '1px solid rgba(255, 255, 255, 0.1)',
104 }}
105 >
106 <Corner position="top-left" />
107 <Corner position="top-right" />
108 <Corner position="bottom-left" />
109 <Corner position="bottom-right" />
110
111 <Box sx={{ color: '#fff' }}>{children}</Box>
112
113 <Box
114 component={motion.div}
115 sx={{
116 position: 'absolute',
117 left: 0,
118 top: 0,
119 background: '#009e86',
120 height: '2px',
121 width: '100%',
122 }}
123 style={{
124 y: useTransform(y, v => v - scanLineOffset),
125 opacity: scannerOpacity,
126 }}
127 />
128
129 <Box
130 component={motion.div}
131 sx={{
132 padding: '48px',
133 position: 'absolute',
134 inset: 0,
135 pointerEvents: 'none',
136 userSelect: 'none',
137 display: 'flex',
138 alignItems: 'center',
139 zIndex: 2,
140 }}
141 style={{ clipPath, opacity: scannerOpacity }}
142 >
143 <Box
144 sx={{
145 position: 'absolute',
146 width: '100%',
147 height: '100%',
148 inset: 0,
149 background: 'linear-gradient(180deg, #006354 0,transparent 100%)',
150 }}
151 />
152 <Box
153 sx={{
154 color: 'hsl(0 0% 8.5%)',
155 textShadow:
156 '-1px -1px 0 #00aa95,1px -1px 0 #00aa95,-1px 1px 0 #00aa95,1px 1px 0 #00aa95',
157 }}
158 >
159 {children}
160 </Box>
161 </Box>
162 </Box>
163 );
164};
165
166interface CornerProps {
167 position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
168}
169
170const Corner = ({ position }: CornerProps) => {
171 const baseStyles = {
172 position: 'absolute',
173 width: 15,
174 height: 15,
175 '&:before': {
176 content: '""',
177 position: 'absolute',
178 background: '#009e86',
179 width: 15,
180 height: '1px',
181 },
182 '&:after': {
183 content: '""',
184 position: 'absolute',
185 background: '#009e86',
186 width: '1px',
187 height: 15,
188 },
189 } as const;
190
191 const positionStyles = {
192 'top-left': {
193 top: '-1px',
194 left: '-1px',
195 '&:before': {
196 ...baseStyles['&:before'],
197 left: 0,
198 top: 0,
199 },
200 '&:after': {
201 ...baseStyles['&:after'],
202 left: 0,
203 top: 0,
204 },
205 },
206 'top-right': {
207 top: '-1px',
208 right: '-1px',
209 '&:before': {
210 ...baseStyles['&:before'],
211 right: 0,
212 top: 0,
213 },
214 '&:after': {
215 ...baseStyles['&:after'],
216 right: 0,
217 top: 0,
218 },
219 },
220 'bottom-left': {
221 bottom: '-1px',
222 left: '-1px',
223 '&:before': {
224 ...baseStyles['&:before'],
225 left: 0,
226 bottom: 0,
227 },
228 '&:after': {
229 ...baseStyles['&:after'],
230 left: 0,
231 bottom: 0,
232 },
233 },
234 'bottom-right': {
235 bottom: '-1px',
236 right: '-1px',
237 '&:before': {
238 ...baseStyles['&:before'],
239 right: 0,
240 bottom: 0,
241 },
242 '&:after': {
243 ...baseStyles['&:after'],
244 right: 0,
245 bottom: 0,
246 },
247 },
248 } as const;
249
250 return (
251 <Box
252 sx={{
253 ...baseStyles,
254 ...positionStyles[position],
255 }}
256 />
257 );
258};
259
260export default XRay;