@moontra/moonui-pro 2.18.6 → 2.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +643 -283
- package/package.json +1 -1
- package/src/components/animated-button/index.tsx +240 -53
- package/src/components/index.ts +5 -1
- package/src/components/ui/hover-card-3d.tsx +397 -28
- package/src/components/ui/index.ts +5 -0
- package/src/components/ui/animated-button.tsx +0 -185
|
@@ -1,55 +1,246 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
|
|
4
|
+
import { motion, useMotionValue, useSpring, useTransform, type SpringOptions } from "framer-motion";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
6
|
import { cn } from "../../lib/utils";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
// Gelişmiş spring konfigürasyonu tipleri
|
|
9
|
+
export interface SpringConfig extends SpringOptions {
|
|
10
|
+
stiffness?: number;
|
|
11
|
+
damping?: number;
|
|
12
|
+
mass?: number;
|
|
13
|
+
velocity?: number;
|
|
14
|
+
restDelta?: number;
|
|
15
|
+
restSpeed?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Overlay render prop tipi
|
|
19
|
+
export type OverlayRenderProp = (props: {
|
|
20
|
+
isHovered: boolean;
|
|
21
|
+
rotateX: number;
|
|
22
|
+
rotateY: number;
|
|
23
|
+
}) => React.ReactNode;
|
|
24
|
+
|
|
25
|
+
// 3D efekt varyantları
|
|
26
|
+
const hoverCard3DVariants = cva(
|
|
27
|
+
"relative w-full h-full",
|
|
28
|
+
{
|
|
29
|
+
variants: {
|
|
30
|
+
variant: {
|
|
31
|
+
subtle: "",
|
|
32
|
+
dramatic: "",
|
|
33
|
+
gaming: "",
|
|
34
|
+
elegant: "",
|
|
35
|
+
neon: "",
|
|
36
|
+
},
|
|
37
|
+
shadowIntensity: {
|
|
38
|
+
none: "",
|
|
39
|
+
light: "",
|
|
40
|
+
medium: "",
|
|
41
|
+
heavy: "",
|
|
42
|
+
extreme: "",
|
|
43
|
+
},
|
|
44
|
+
glowEffect: {
|
|
45
|
+
none: "",
|
|
46
|
+
subtle: "",
|
|
47
|
+
vibrant: "",
|
|
48
|
+
neon: "",
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
defaultVariants: {
|
|
52
|
+
variant: "subtle",
|
|
53
|
+
shadowIntensity: "medium",
|
|
54
|
+
glowEffect: "none",
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
export interface HoverCard3DProps
|
|
60
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
61
|
+
VariantProps<typeof hoverCard3DVariants> {
|
|
62
|
+
/** İçerik */
|
|
8
63
|
children: React.ReactNode;
|
|
9
|
-
|
|
64
|
+
/** Maksimum rotasyon açısı (derece) */
|
|
65
|
+
maxRotation?: number;
|
|
66
|
+
/** Hover durumunda ölçekleme faktörü */
|
|
10
67
|
scale?: number;
|
|
68
|
+
/** 3D perspektif değeri (px) */
|
|
11
69
|
perspective?: number;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
70
|
+
/** Animasyon hızı (0-1 arası, 1 en hızlı) */
|
|
71
|
+
animationSpeed?: number;
|
|
72
|
+
/** Spring animasyon konfigürasyonu */
|
|
73
|
+
springConfig?: SpringConfig;
|
|
74
|
+
/** Özel overlay içeriği veya render prop */
|
|
75
|
+
overlay?: React.ReactNode | OverlayRenderProp;
|
|
76
|
+
/** Overlay'in her zaman görünür olup olmayacağı */
|
|
77
|
+
overlayAlwaysVisible?: boolean;
|
|
78
|
+
/** Glow efekti rengi (CSS color değeri) */
|
|
79
|
+
glowColor?: string;
|
|
80
|
+
/** Glow efekti blur değeri (px) */
|
|
81
|
+
glowBlur?: number;
|
|
82
|
+
/** Glow efekti spread değeri (px) */
|
|
83
|
+
glowSpread?: number;
|
|
84
|
+
/** Touch desteği aktif mi? */
|
|
85
|
+
enableTouch?: boolean;
|
|
86
|
+
/** Klavye desteği aktif mi? */
|
|
87
|
+
enableKeyboard?: boolean;
|
|
88
|
+
/** Rotasyon eksenleri */
|
|
89
|
+
rotateAxes?: {
|
|
90
|
+
x?: boolean;
|
|
91
|
+
y?: boolean;
|
|
15
92
|
};
|
|
93
|
+
/** Animasyon başlangıç gecikmesi (ms) */
|
|
94
|
+
animationDelay?: number;
|
|
95
|
+
/** Hover'da tetiklenecek callback */
|
|
96
|
+
onHoverStart?: () => void;
|
|
97
|
+
/** Hover bittiğinde tetiklenecek callback */
|
|
98
|
+
onHoverEnd?: () => void;
|
|
99
|
+
/** Rotasyon değiştiğinde tetiklenecek callback */
|
|
100
|
+
onRotationChange?: (rotateX: number, rotateY: number) => void;
|
|
101
|
+
/** ARIA label */
|
|
102
|
+
ariaLabel?: string;
|
|
103
|
+
/** Otomatik odaklanma */
|
|
104
|
+
autoFocus?: boolean;
|
|
16
105
|
}
|
|
17
106
|
|
|
107
|
+
// Varyant bazlı konfigürasyonlar
|
|
108
|
+
const variantConfigs = {
|
|
109
|
+
subtle: {
|
|
110
|
+
maxRotation: 10,
|
|
111
|
+
scale: 1.02,
|
|
112
|
+
springConfig: { stiffness: 400, damping: 30 },
|
|
113
|
+
},
|
|
114
|
+
dramatic: {
|
|
115
|
+
maxRotation: 25,
|
|
116
|
+
scale: 1.1,
|
|
117
|
+
springConfig: { stiffness: 200, damping: 20 },
|
|
118
|
+
},
|
|
119
|
+
gaming: {
|
|
120
|
+
maxRotation: 30,
|
|
121
|
+
scale: 1.15,
|
|
122
|
+
springConfig: { stiffness: 300, damping: 15 },
|
|
123
|
+
},
|
|
124
|
+
elegant: {
|
|
125
|
+
maxRotation: 8,
|
|
126
|
+
scale: 1.03,
|
|
127
|
+
springConfig: { stiffness: 500, damping: 40 },
|
|
128
|
+
},
|
|
129
|
+
neon: {
|
|
130
|
+
maxRotation: 20,
|
|
131
|
+
scale: 1.08,
|
|
132
|
+
springConfig: { stiffness: 250, damping: 25 },
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Shadow intensity değerleri
|
|
137
|
+
const shadowIntensityMap = {
|
|
138
|
+
none: "none",
|
|
139
|
+
light: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
|
|
140
|
+
medium: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
|
|
141
|
+
heavy: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)",
|
|
142
|
+
extreme: "0 25px 50px -12px rgb(0 0 0 / 0.25)",
|
|
143
|
+
};
|
|
144
|
+
|
|
18
145
|
const HoverCard3D = React.forwardRef<HTMLDivElement, HoverCard3DProps>(
|
|
19
146
|
(
|
|
20
147
|
{
|
|
21
148
|
children,
|
|
22
149
|
className,
|
|
23
|
-
|
|
24
|
-
|
|
150
|
+
variant = "subtle",
|
|
151
|
+
shadowIntensity = "medium",
|
|
152
|
+
glowEffect = "none",
|
|
153
|
+
maxRotation,
|
|
154
|
+
scale,
|
|
25
155
|
perspective = 1000,
|
|
26
|
-
|
|
156
|
+
animationSpeed = 1,
|
|
157
|
+
springConfig,
|
|
158
|
+
overlay,
|
|
159
|
+
overlayAlwaysVisible = false,
|
|
160
|
+
glowColor = "rgb(99, 102, 241)",
|
|
161
|
+
glowBlur = 20,
|
|
162
|
+
glowSpread = 5,
|
|
163
|
+
enableTouch = true,
|
|
164
|
+
enableKeyboard = true,
|
|
165
|
+
rotateAxes = { x: true, y: true },
|
|
166
|
+
animationDelay = 0,
|
|
167
|
+
onHoverStart,
|
|
168
|
+
onHoverEnd,
|
|
169
|
+
onRotationChange,
|
|
170
|
+
ariaLabel,
|
|
171
|
+
autoFocus = false,
|
|
27
172
|
...props
|
|
28
173
|
},
|
|
29
174
|
ref
|
|
30
175
|
) => {
|
|
31
176
|
const cardRef = React.useRef<HTMLDivElement>(null);
|
|
177
|
+
const [isHovered, setIsHovered] = React.useState(false);
|
|
178
|
+
const [isFocused, setIsFocused] = React.useState(false);
|
|
179
|
+
|
|
180
|
+
// Varyant bazlı default değerler
|
|
181
|
+
const variantConfig = variantConfigs[variant as keyof typeof variantConfigs] || variantConfigs.subtle;
|
|
182
|
+
const finalMaxRotation = maxRotation ?? variantConfig.maxRotation;
|
|
183
|
+
const finalScale = scale ?? variantConfig.scale;
|
|
184
|
+
const finalSpringConfig = springConfig ?? variantConfig.springConfig;
|
|
185
|
+
|
|
186
|
+
// Animasyon hızı faktörü
|
|
187
|
+
const speedFactor = Math.max(0.1, Math.min(1, animationSpeed));
|
|
188
|
+
const adjustedSpringConfig = {
|
|
189
|
+
...finalSpringConfig,
|
|
190
|
+
stiffness: (finalSpringConfig.stiffness || 300) * speedFactor,
|
|
191
|
+
damping: (finalSpringConfig.damping || 20) / speedFactor,
|
|
192
|
+
};
|
|
193
|
+
|
|
32
194
|
const mouseX = useMotionValue(0);
|
|
33
195
|
const mouseY = useMotionValue(0);
|
|
34
196
|
|
|
197
|
+
// Rotasyon değerleri
|
|
35
198
|
const rotateX = useSpring(
|
|
36
|
-
useTransform(
|
|
37
|
-
|
|
199
|
+
useTransform(
|
|
200
|
+
mouseY,
|
|
201
|
+
[-0.5, 0.5],
|
|
202
|
+
rotateAxes.x ? [finalMaxRotation, -finalMaxRotation] : [0, 0]
|
|
203
|
+
),
|
|
204
|
+
adjustedSpringConfig
|
|
38
205
|
);
|
|
39
206
|
const rotateY = useSpring(
|
|
40
|
-
useTransform(
|
|
41
|
-
|
|
207
|
+
useTransform(
|
|
208
|
+
mouseX,
|
|
209
|
+
[-0.5, 0.5],
|
|
210
|
+
rotateAxes.y ? [-finalMaxRotation, finalMaxRotation] : [0, 0]
|
|
211
|
+
),
|
|
212
|
+
adjustedSpringConfig
|
|
42
213
|
);
|
|
43
214
|
|
|
44
|
-
|
|
45
|
-
|
|
215
|
+
// Rotasyon değişim callback'i
|
|
216
|
+
React.useEffect(() => {
|
|
217
|
+
if (onRotationChange) {
|
|
218
|
+
const unsubscribeX = rotateX.on("change", (x) => {
|
|
219
|
+
const y = rotateY.get();
|
|
220
|
+
onRotationChange(x, y);
|
|
221
|
+
});
|
|
222
|
+
const unsubscribeY = rotateY.on("change", (y) => {
|
|
223
|
+
const x = rotateX.get();
|
|
224
|
+
onRotationChange(x, y);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return () => {
|
|
228
|
+
unsubscribeX();
|
|
229
|
+
unsubscribeY();
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}, [rotateX, rotateY, onRotationChange]);
|
|
233
|
+
|
|
234
|
+
// Mouse/Touch pozisyon hesaplama
|
|
235
|
+
const calculatePosition = React.useCallback(
|
|
236
|
+
(clientX: number, clientY: number) => {
|
|
46
237
|
if (!cardRef.current) return;
|
|
47
238
|
|
|
48
239
|
const rect = cardRef.current.getBoundingClientRect();
|
|
49
240
|
const width = rect.width;
|
|
50
241
|
const height = rect.height;
|
|
51
|
-
const x =
|
|
52
|
-
const y =
|
|
242
|
+
const x = clientX - rect.left;
|
|
243
|
+
const y = clientY - rect.top;
|
|
53
244
|
|
|
54
245
|
const xPct = x / width - 0.5;
|
|
55
246
|
const yPct = y / height - 0.5;
|
|
@@ -60,10 +251,131 @@ const HoverCard3D = React.forwardRef<HTMLDivElement, HoverCard3DProps>(
|
|
|
60
251
|
[mouseX, mouseY]
|
|
61
252
|
);
|
|
62
253
|
|
|
254
|
+
// Event handler'lar
|
|
255
|
+
const handleMouseMove = React.useCallback(
|
|
256
|
+
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
257
|
+
calculatePosition(e.clientX, e.clientY);
|
|
258
|
+
},
|
|
259
|
+
[calculatePosition]
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const handleTouchMove = React.useCallback(
|
|
263
|
+
(e: React.TouchEvent<HTMLDivElement>) => {
|
|
264
|
+
if (!enableTouch) return;
|
|
265
|
+
const touch = e.touches[0];
|
|
266
|
+
calculatePosition(touch.clientX, touch.clientY);
|
|
267
|
+
},
|
|
268
|
+
[calculatePosition, enableTouch]
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const handleMouseEnter = React.useCallback(() => {
|
|
272
|
+
setIsHovered(true);
|
|
273
|
+
onHoverStart?.();
|
|
274
|
+
}, [onHoverStart]);
|
|
275
|
+
|
|
63
276
|
const handleMouseLeave = React.useCallback(() => {
|
|
64
277
|
mouseX.set(0);
|
|
65
278
|
mouseY.set(0);
|
|
66
|
-
|
|
279
|
+
setIsHovered(false);
|
|
280
|
+
onHoverEnd?.();
|
|
281
|
+
}, [mouseX, mouseY, onHoverEnd]);
|
|
282
|
+
|
|
283
|
+
const handleTouchEnd = React.useCallback(() => {
|
|
284
|
+
if (!enableTouch) return;
|
|
285
|
+
handleMouseLeave();
|
|
286
|
+
}, [handleMouseLeave, enableTouch]);
|
|
287
|
+
|
|
288
|
+
// Klavye desteği
|
|
289
|
+
const handleKeyDown = React.useCallback(
|
|
290
|
+
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
291
|
+
if (!enableKeyboard) return;
|
|
292
|
+
|
|
293
|
+
const step = 0.1;
|
|
294
|
+
let newX = mouseX.get();
|
|
295
|
+
let newY = mouseY.get();
|
|
296
|
+
|
|
297
|
+
switch (e.key) {
|
|
298
|
+
case "ArrowUp":
|
|
299
|
+
newY = Math.max(-0.5, newY - step);
|
|
300
|
+
break;
|
|
301
|
+
case "ArrowDown":
|
|
302
|
+
newY = Math.min(0.5, newY + step);
|
|
303
|
+
break;
|
|
304
|
+
case "ArrowLeft":
|
|
305
|
+
newX = Math.max(-0.5, newX - step);
|
|
306
|
+
break;
|
|
307
|
+
case "ArrowRight":
|
|
308
|
+
newX = Math.min(0.5, newX + step);
|
|
309
|
+
break;
|
|
310
|
+
case "Enter":
|
|
311
|
+
case " ":
|
|
312
|
+
setIsHovered(!isHovered);
|
|
313
|
+
if (!isHovered) {
|
|
314
|
+
onHoverStart?.();
|
|
315
|
+
} else {
|
|
316
|
+
onHoverEnd?.();
|
|
317
|
+
}
|
|
318
|
+
break;
|
|
319
|
+
case "Escape":
|
|
320
|
+
newX = 0;
|
|
321
|
+
newY = 0;
|
|
322
|
+
setIsHovered(false);
|
|
323
|
+
onHoverEnd?.();
|
|
324
|
+
break;
|
|
325
|
+
default:
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
e.preventDefault();
|
|
330
|
+
mouseX.set(newX);
|
|
331
|
+
mouseY.set(newY);
|
|
332
|
+
},
|
|
333
|
+
[mouseX, mouseY, isHovered, enableKeyboard, onHoverStart, onHoverEnd]
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const handleFocus = React.useCallback(() => {
|
|
337
|
+
setIsFocused(true);
|
|
338
|
+
}, []);
|
|
339
|
+
|
|
340
|
+
const handleBlur = React.useCallback(() => {
|
|
341
|
+
setIsFocused(false);
|
|
342
|
+
if (!isHovered) {
|
|
343
|
+
mouseX.set(0);
|
|
344
|
+
mouseY.set(0);
|
|
345
|
+
}
|
|
346
|
+
}, [mouseX, mouseY, isHovered]);
|
|
347
|
+
|
|
348
|
+
// Glow efekti stili
|
|
349
|
+
const glowStyle = React.useMemo(() => {
|
|
350
|
+
if (glowEffect === "none") return {};
|
|
351
|
+
|
|
352
|
+
const intensity = {
|
|
353
|
+
subtle: 0.3,
|
|
354
|
+
vibrant: 0.6,
|
|
355
|
+
neon: 1,
|
|
356
|
+
}[glowEffect as string] || 0.3;
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
boxShadow: isHovered
|
|
360
|
+
? `0 0 ${glowBlur}px ${glowSpread}px ${glowColor}${Math.round(intensity * 255).toString(16).padStart(2, '0')}`
|
|
361
|
+
: undefined,
|
|
362
|
+
};
|
|
363
|
+
}, [glowEffect, glowColor, glowBlur, glowSpread, isHovered]);
|
|
364
|
+
|
|
365
|
+
// Overlay içeriği
|
|
366
|
+
const overlayContent = React.useMemo(() => {
|
|
367
|
+
if (!overlay) return null;
|
|
368
|
+
|
|
369
|
+
if (typeof overlay === "function") {
|
|
370
|
+
return overlay({
|
|
371
|
+
isHovered,
|
|
372
|
+
rotateX: rotateX.get(),
|
|
373
|
+
rotateY: rotateY.get(),
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return overlay;
|
|
378
|
+
}, [overlay, isHovered, rotateX, rotateY]);
|
|
67
379
|
|
|
68
380
|
return (
|
|
69
381
|
<div
|
|
@@ -74,23 +386,80 @@ const HoverCard3D = React.forwardRef<HTMLDivElement, HoverCard3DProps>(
|
|
|
74
386
|
>
|
|
75
387
|
<motion.div
|
|
76
388
|
ref={cardRef}
|
|
77
|
-
className=
|
|
389
|
+
className={cn(
|
|
390
|
+
hoverCard3DVariants({ variant, shadowIntensity, glowEffect }),
|
|
391
|
+
"transition-shadow duration-300",
|
|
392
|
+
isFocused && "ring-2 ring-primary ring-offset-2 ring-offset-background"
|
|
393
|
+
)}
|
|
78
394
|
style={{
|
|
79
395
|
rotateX,
|
|
80
396
|
rotateY,
|
|
81
397
|
transformStyle: "preserve-3d",
|
|
398
|
+
boxShadow: shadowIntensityMap[shadowIntensity as keyof typeof shadowIntensityMap],
|
|
399
|
+
...glowStyle,
|
|
82
400
|
}}
|
|
83
401
|
onMouseMove={handleMouseMove}
|
|
402
|
+
onMouseEnter={handleMouseEnter}
|
|
84
403
|
onMouseLeave={handleMouseLeave}
|
|
85
|
-
|
|
86
|
-
|
|
404
|
+
onTouchMove={handleTouchMove}
|
|
405
|
+
onTouchEnd={handleTouchEnd}
|
|
406
|
+
onKeyDown={handleKeyDown}
|
|
407
|
+
onFocus={handleFocus}
|
|
408
|
+
onBlur={handleBlur}
|
|
409
|
+
whileHover={{ scale: finalScale }}
|
|
410
|
+
initial={{ scale: 1 }}
|
|
411
|
+
transition={{
|
|
412
|
+
type: "spring",
|
|
413
|
+
delay: animationDelay / 1000,
|
|
414
|
+
...adjustedSpringConfig,
|
|
415
|
+
}}
|
|
416
|
+
tabIndex={enableKeyboard ? 0 : -1}
|
|
417
|
+
role="button"
|
|
418
|
+
aria-label={ariaLabel || "3D hover card"}
|
|
419
|
+
autoFocus={autoFocus}
|
|
87
420
|
>
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
421
|
+
{/* Overlay katmanı */}
|
|
422
|
+
{(overlayAlwaysVisible || isHovered) && overlayContent && (
|
|
423
|
+
<div
|
|
424
|
+
className="absolute inset-0 rounded-lg pointer-events-none"
|
|
425
|
+
style={{
|
|
426
|
+
transform: "translateZ(1px)",
|
|
427
|
+
}}
|
|
428
|
+
>
|
|
429
|
+
{overlayContent}
|
|
430
|
+
</div>
|
|
431
|
+
)}
|
|
432
|
+
|
|
433
|
+
{/* Varsayılan highlight efekti */}
|
|
434
|
+
{!overlay && variant !== "neon" && (
|
|
435
|
+
<div
|
|
436
|
+
className={cn(
|
|
437
|
+
"absolute inset-0 rounded-lg bg-gradient-to-br from-white/20 to-white/0",
|
|
438
|
+
"opacity-0 transition-opacity duration-300 pointer-events-none",
|
|
439
|
+
isHovered && "opacity-100"
|
|
440
|
+
)}
|
|
441
|
+
style={{
|
|
442
|
+
transform: "translateZ(1px)",
|
|
443
|
+
}}
|
|
444
|
+
/>
|
|
445
|
+
)}
|
|
446
|
+
|
|
447
|
+
{/* Neon varyantı için özel efekt */}
|
|
448
|
+
{variant === "neon" && (
|
|
449
|
+
<div
|
|
450
|
+
className={cn(
|
|
451
|
+
"absolute inset-0 rounded-lg",
|
|
452
|
+
"opacity-0 transition-opacity duration-300 pointer-events-none",
|
|
453
|
+
isHovered && "opacity-100"
|
|
454
|
+
)}
|
|
455
|
+
style={{
|
|
456
|
+
transform: "translateZ(2px)",
|
|
457
|
+
background: `linear-gradient(45deg, ${glowColor}20, transparent, ${glowColor}20)`,
|
|
458
|
+
filter: "blur(10px)",
|
|
459
|
+
}}
|
|
460
|
+
/>
|
|
461
|
+
)}
|
|
462
|
+
|
|
94
463
|
{children}
|
|
95
464
|
</motion.div>
|
|
96
465
|
</div>
|
|
@@ -100,4 +469,4 @@ const HoverCard3D = React.forwardRef<HTMLDivElement, HoverCard3DProps>(
|
|
|
100
469
|
|
|
101
470
|
HoverCard3D.displayName = "HoverCard3D";
|
|
102
471
|
|
|
103
|
-
export { HoverCard3D };
|
|
472
|
+
export { HoverCard3D, hoverCard3DVariants };
|
|
@@ -170,4 +170,9 @@ export {
|
|
|
170
170
|
HoverCard, HoverCardTrigger, HoverCardContent
|
|
171
171
|
} from './hover-card';
|
|
172
172
|
|
|
173
|
+
export {
|
|
174
|
+
HoverCard3D, hoverCard3DVariants
|
|
175
|
+
} from './hover-card-3d';
|
|
176
|
+
export type { HoverCard3DProps, SpringConfig, OverlayRenderProp } from './hover-card-3d';
|
|
177
|
+
|
|
173
178
|
// Note: Micro-interaction components are exported from their individual directories
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
import { motion, AnimatePresence } from "framer-motion";
|
|
5
|
-
import { Check, X, Loader2 } from "lucide-react";
|
|
6
|
-
import { cn } from "../../lib/utils";
|
|
7
|
-
import { cva, type VariantProps } from "class-variance-authority";
|
|
8
|
-
|
|
9
|
-
const animatedButtonVariants = cva(
|
|
10
|
-
"relative inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
11
|
-
{
|
|
12
|
-
variants: {
|
|
13
|
-
variant: {
|
|
14
|
-
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
15
|
-
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
16
|
-
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
17
|
-
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
18
|
-
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
19
|
-
link: "text-primary underline-offset-4 hover:underline",
|
|
20
|
-
},
|
|
21
|
-
size: {
|
|
22
|
-
default: "h-10 px-4 py-2",
|
|
23
|
-
sm: "h-9 rounded-md px-3",
|
|
24
|
-
lg: "h-11 rounded-md px-8",
|
|
25
|
-
icon: "h-10 w-10",
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
defaultVariants: {
|
|
29
|
-
variant: "default",
|
|
30
|
-
size: "default",
|
|
31
|
-
},
|
|
32
|
-
}
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
export interface AnimatedButtonProps
|
|
36
|
-
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
37
|
-
VariantProps<typeof animatedButtonVariants> {
|
|
38
|
-
state?: "idle" | "loading" | "success" | "error";
|
|
39
|
-
loadingText?: string;
|
|
40
|
-
successText?: string;
|
|
41
|
-
errorText?: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const AnimatedButton = React.forwardRef<HTMLButtonElement, AnimatedButtonProps>(
|
|
45
|
-
(
|
|
46
|
-
{
|
|
47
|
-
className,
|
|
48
|
-
variant,
|
|
49
|
-
size,
|
|
50
|
-
state = "idle",
|
|
51
|
-
loadingText = "Loading...",
|
|
52
|
-
successText = "Success!",
|
|
53
|
-
errorText = "Error!",
|
|
54
|
-
children,
|
|
55
|
-
disabled,
|
|
56
|
-
...props
|
|
57
|
-
},
|
|
58
|
-
ref
|
|
59
|
-
) => {
|
|
60
|
-
const [buttonWidth, setButtonWidth] = React.useState<number | "auto">("auto");
|
|
61
|
-
const buttonRef = React.useRef<HTMLButtonElement>(null);
|
|
62
|
-
|
|
63
|
-
React.useEffect(() => {
|
|
64
|
-
if (buttonRef.current && state === "idle") {
|
|
65
|
-
setButtonWidth(buttonRef.current.offsetWidth);
|
|
66
|
-
}
|
|
67
|
-
}, [state, children]);
|
|
68
|
-
|
|
69
|
-
const isDisabled = disabled || state !== "idle";
|
|
70
|
-
|
|
71
|
-
const contentVariants = {
|
|
72
|
-
idle: { opacity: 1, y: 0 },
|
|
73
|
-
loading: { opacity: 0, y: 10 },
|
|
74
|
-
success: { opacity: 0, y: 10 },
|
|
75
|
-
error: { opacity: 0, y: 10 },
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const iconVariants = {
|
|
79
|
-
hidden: { opacity: 0, scale: 0.8, y: -10 },
|
|
80
|
-
visible: { opacity: 1, scale: 1, y: 0 },
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<motion.button
|
|
85
|
-
ref={(el) => {
|
|
86
|
-
buttonRef.current = el;
|
|
87
|
-
if (ref) {
|
|
88
|
-
if (typeof ref === "function") ref(el);
|
|
89
|
-
else ref.current = el;
|
|
90
|
-
}
|
|
91
|
-
}}
|
|
92
|
-
className={cn(animatedButtonVariants({ variant, size, className }))}
|
|
93
|
-
disabled={isDisabled}
|
|
94
|
-
animate={{
|
|
95
|
-
width: state !== "idle" ? buttonWidth : "auto",
|
|
96
|
-
}}
|
|
97
|
-
transition={{ duration: 0.2 }}
|
|
98
|
-
{...props}
|
|
99
|
-
>
|
|
100
|
-
<AnimatePresence mode="wait">
|
|
101
|
-
{state === "idle" && (
|
|
102
|
-
<motion.span
|
|
103
|
-
key="idle"
|
|
104
|
-
variants={contentVariants}
|
|
105
|
-
initial="loading"
|
|
106
|
-
animate="idle"
|
|
107
|
-
exit="loading"
|
|
108
|
-
transition={{ duration: 0.2 }}
|
|
109
|
-
className="inline-flex items-center"
|
|
110
|
-
>
|
|
111
|
-
{children}
|
|
112
|
-
</motion.span>
|
|
113
|
-
)}
|
|
114
|
-
|
|
115
|
-
{state === "loading" && (
|
|
116
|
-
<motion.span
|
|
117
|
-
key="loading"
|
|
118
|
-
className="inline-flex items-center gap-2"
|
|
119
|
-
initial={{ opacity: 0, y: -10 }}
|
|
120
|
-
animate={{ opacity: 1, y: 0 }}
|
|
121
|
-
exit={{ opacity: 0, y: 10 }}
|
|
122
|
-
transition={{ duration: 0.2 }}
|
|
123
|
-
>
|
|
124
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
125
|
-
{loadingText}
|
|
126
|
-
</motion.span>
|
|
127
|
-
)}
|
|
128
|
-
|
|
129
|
-
{state === "success" && (
|
|
130
|
-
<motion.span
|
|
131
|
-
key="success"
|
|
132
|
-
className="inline-flex items-center gap-2"
|
|
133
|
-
initial={{ opacity: 0, y: -10 }}
|
|
134
|
-
animate={{ opacity: 1, y: 0 }}
|
|
135
|
-
exit={{ opacity: 0, y: 10 }}
|
|
136
|
-
transition={{ duration: 0.2 }}
|
|
137
|
-
>
|
|
138
|
-
<motion.div
|
|
139
|
-
variants={iconVariants}
|
|
140
|
-
initial="hidden"
|
|
141
|
-
animate="visible"
|
|
142
|
-
transition={{ duration: 0.3, delay: 0.1 }}
|
|
143
|
-
>
|
|
144
|
-
<Check className="h-4 w-4" />
|
|
145
|
-
</motion.div>
|
|
146
|
-
{successText}
|
|
147
|
-
</motion.span>
|
|
148
|
-
)}
|
|
149
|
-
|
|
150
|
-
{state === "error" && (
|
|
151
|
-
<motion.span
|
|
152
|
-
key="error"
|
|
153
|
-
className="inline-flex items-center gap-2"
|
|
154
|
-
initial={{ opacity: 0, y: -10 }}
|
|
155
|
-
animate={{ opacity: 1, y: 0 }}
|
|
156
|
-
exit={{ opacity: 0, y: 10 }}
|
|
157
|
-
transition={{ duration: 0.2 }}
|
|
158
|
-
>
|
|
159
|
-
<motion.div
|
|
160
|
-
variants={iconVariants}
|
|
161
|
-
initial="hidden"
|
|
162
|
-
animate="visible"
|
|
163
|
-
transition={{ duration: 0.3, delay: 0.1 }}
|
|
164
|
-
animate={{
|
|
165
|
-
x: [0, -2, 2, -2, 2, 0],
|
|
166
|
-
}}
|
|
167
|
-
transition={{
|
|
168
|
-
duration: 0.4,
|
|
169
|
-
delay: 0.1,
|
|
170
|
-
}}
|
|
171
|
-
>
|
|
172
|
-
<X className="h-4 w-4" />
|
|
173
|
-
</motion.div>
|
|
174
|
-
{errorText}
|
|
175
|
-
</motion.span>
|
|
176
|
-
)}
|
|
177
|
-
</AnimatePresence>
|
|
178
|
-
</motion.button>
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
AnimatedButton.displayName = "AnimatedButton";
|
|
184
|
-
|
|
185
|
-
export { AnimatedButton, animatedButtonVariants };
|