alexui 1.0.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.
Files changed (124) hide show
  1. package/README.md +57 -0
  2. package/components/ActionTable.tsx +307 -0
  3. package/components/AlertBanner.tsx +124 -0
  4. package/components/AnimatedAccordion.tsx +95 -0
  5. package/components/Autocomplete.tsx +144 -0
  6. package/components/Avatar.tsx +123 -0
  7. package/components/Badge.tsx +80 -0
  8. package/components/Breadcrumb.tsx +74 -0
  9. package/components/Calendar.tsx +340 -0
  10. package/components/Card3D.tsx +117 -0
  11. package/components/Carousel3D.tsx +193 -0
  12. package/components/CascadeSelect.tsx +232 -0
  13. package/components/ChartShowcase.tsx +700 -0
  14. package/components/Checkbox.tsx +212 -0
  15. package/components/ChipsInput.tsx +152 -0
  16. package/components/CircularKnob.tsx +240 -0
  17. package/components/CodeVisualizer.tsx +67 -0
  18. package/components/Collapsible.tsx +72 -0
  19. package/components/ColorThemeManager.tsx +458 -0
  20. package/components/CommandMenu.tsx +191 -0
  21. package/components/ConfirmDialog.tsx +152 -0
  22. package/components/ContextMenu.tsx +192 -0
  23. package/components/DashboardLayout.tsx +115 -0
  24. package/components/DatePicker.tsx +108 -0
  25. package/components/Divider.tsx +67 -0
  26. package/components/Dock.tsx +93 -0
  27. package/components/DragDropLists.tsx +160 -0
  28. package/components/Drawer.tsx +161 -0
  29. package/components/DropdownPlus.tsx +304 -0
  30. package/components/EmptyState.tsx +49 -0
  31. package/components/ErrorPage.tsx +62 -0
  32. package/components/FileDropzone.tsx +206 -0
  33. package/components/ForgotPassword.tsx +137 -0
  34. package/components/FormField.tsx +81 -0
  35. package/components/GlassButton.tsx +56 -0
  36. package/components/GlassCard.tsx +82 -0
  37. package/components/GlassInput.tsx +96 -0
  38. package/components/GlassmorphicModal.tsx +108 -0
  39. package/components/GlowInput.tsx +111 -0
  40. package/components/GlowSelect.tsx +203 -0
  41. package/components/GlowTextArea.tsx +105 -0
  42. package/components/HorizontalTimeline.tsx +121 -0
  43. package/components/HoverCard.tsx +105 -0
  44. package/components/ImageLightbox.tsx +259 -0
  45. package/components/InputGroup.tsx +118 -0
  46. package/components/InputOTP.tsx +147 -0
  47. package/components/InteractiveNavbar.tsx +266 -0
  48. package/components/InteractiveSidebar.tsx +211 -0
  49. package/components/Kbd.tsx +51 -0
  50. package/components/LiteYouTube.tsx +118 -0
  51. package/components/LoaderCollection.tsx +368 -0
  52. package/components/LoginForm.tsx +192 -0
  53. package/components/MagneticButton.tsx +101 -0
  54. package/components/MaskedInput.tsx +79 -0
  55. package/components/MentionInput.tsx +413 -0
  56. package/components/MorphingSwitch.tsx +86 -0
  57. package/components/MultiSelect.tsx +158 -0
  58. package/components/NumberInput.tsx +203 -0
  59. package/components/Panel.tsx +104 -0
  60. package/components/PasswordInput.tsx +203 -0
  61. package/components/Popover.tsx +91 -0
  62. package/components/PricingTable.tsx +113 -0
  63. package/components/ProgressBar.tsx +152 -0
  64. package/components/RadioButton.tsx +211 -0
  65. package/components/Rating.tsx +82 -0
  66. package/components/ResizablePanel.tsx +114 -0
  67. package/components/ScrollPanel.tsx +103 -0
  68. package/components/SettingsPage.tsx +154 -0
  69. package/components/SignupForm.tsx +182 -0
  70. package/components/Skeleton.tsx +41 -0
  71. package/components/Slider.tsx +95 -0
  72. package/components/SlidingTabs.tsx +54 -0
  73. package/components/SortableList.tsx +91 -0
  74. package/components/SpeedDial.tsx +134 -0
  75. package/components/Spinner.tsx +40 -0
  76. package/components/Stepper.tsx +124 -0
  77. package/components/TabMenu.tsx +72 -0
  78. package/components/TableControls.tsx +77 -0
  79. package/components/TablePagination.tsx +88 -0
  80. package/components/TextEditor.tsx +329 -0
  81. package/components/TextReveal.tsx +99 -0
  82. package/components/ThemeSwitcher.tsx +133 -0
  83. package/components/TimelineGSAP.tsx +164 -0
  84. package/components/ToastSystem.tsx +110 -0
  85. package/components/ToggleButton.tsx +79 -0
  86. package/components/Tooltip.tsx +121 -0
  87. package/components/Tree.tsx +138 -0
  88. package/dist/commands/add.d.ts +7 -0
  89. package/dist/commands/add.js +110 -0
  90. package/dist/commands/init.d.ts +5 -0
  91. package/dist/commands/init.js +76 -0
  92. package/dist/commands/list.d.ts +1 -0
  93. package/dist/commands/list.js +32 -0
  94. package/dist/index.d.ts +2 -0
  95. package/dist/index.js +60 -0
  96. package/dist/registry.d.ts +6 -0
  97. package/dist/registry.js +38 -0
  98. package/dist/tui/browse.d.ts +3 -0
  99. package/dist/tui/browse.js +139 -0
  100. package/dist/tui/format.d.ts +11 -0
  101. package/dist/tui/format.js +52 -0
  102. package/dist/tui/main.d.ts +1 -0
  103. package/dist/tui/main.js +86 -0
  104. package/dist/tui/panels.d.ts +9 -0
  105. package/dist/tui/panels.js +50 -0
  106. package/dist/tui/theme.d.ts +28 -0
  107. package/dist/tui/theme.js +76 -0
  108. package/dist/types.d.ts +28 -0
  109. package/dist/types.js +1 -0
  110. package/dist/utils/config.d.ts +6 -0
  111. package/dist/utils/config.js +24 -0
  112. package/dist/utils/copy.d.ts +9 -0
  113. package/dist/utils/copy.js +43 -0
  114. package/dist/utils/cwd.d.ts +6 -0
  115. package/dist/utils/cwd.js +30 -0
  116. package/dist/utils/deps.d.ts +1 -0
  117. package/dist/utils/deps.js +19 -0
  118. package/dist/utils/project.d.ts +5 -0
  119. package/dist/utils/project.js +30 -0
  120. package/dist/utils/theme.d.ts +1 -0
  121. package/dist/utils/theme.js +24 -0
  122. package/package.json +52 -0
  123. package/registry.json +1133 -0
  124. package/templates/theme.css +81 -0
@@ -0,0 +1,368 @@
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+
4
+ interface BaseLoaderProps {
5
+ color?: string;
6
+ size?: 'sm' | 'md' | 'lg';
7
+ className?: string;
8
+ }
9
+
10
+ // Helper to resolve sizes
11
+ const getPixelSize = (size: 'sm' | 'md' | 'lg', sm: number, md: number, lg: number) => {
12
+ if (size === 'sm') return sm;
13
+ if (size === 'lg') return lg;
14
+ return md;
15
+ };
16
+
17
+ // 1. PulseBars: Soundwave-like pulsing vertical bars
18
+ export const PulseBars: React.FC<BaseLoaderProps> = ({
19
+ color = 'var(--color-accent)',
20
+ size = 'md',
21
+ className = ''
22
+ }) => {
23
+ const height = getPixelSize(size, 20, 36, 52);
24
+ const width = getPixelSize(size, 3, 5, 7);
25
+ const gap = getPixelSize(size, 3, 4, 6);
26
+
27
+ const containerVariants = {
28
+ animate: {
29
+ transition: {
30
+ staggerChildren: 0.1,
31
+ }
32
+ }
33
+ };
34
+
35
+ const barVariants = {
36
+ initial: { scaleY: 0.3 },
37
+ animate: {
38
+ scaleY: [0.3, 1, 0.3],
39
+ transition: {
40
+ duration: 0.8,
41
+ repeat: Infinity,
42
+ ease: 'easeInOut' as const
43
+ }
44
+ }
45
+ };
46
+
47
+ return (
48
+ <motion.div
49
+ variants={containerVariants}
50
+ initial="initial"
51
+ animate="animate"
52
+ className={`flex items-center justify-center ${className}`}
53
+ style={{ height, gap }}
54
+ >
55
+ {[...Array(5)].map((_, i) => (
56
+ <motion.div
57
+ key={i}
58
+ variants={barVariants}
59
+ className="rounded-full origin-center"
60
+ style={{
61
+ backgroundColor: color,
62
+ width,
63
+ height: '100%',
64
+ boxShadow: `0 0 10px ${color}50`
65
+ }}
66
+ />
67
+ ))}
68
+ </motion.div>
69
+ );
70
+ };
71
+
72
+ // 2. BouncingDots: Classic sequential bouncing spheres
73
+ export const BouncingDots: React.FC<BaseLoaderProps> = ({
74
+ color = 'var(--color-accent)',
75
+ size = 'md',
76
+ className = ''
77
+ }) => {
78
+ const dotSize = getPixelSize(size, 8, 12, 16);
79
+ const gap = getPixelSize(size, 4, 6, 8);
80
+
81
+ const containerVariants = {
82
+ animate: {
83
+ transition: {
84
+ staggerChildren: 0.15,
85
+ }
86
+ }
87
+ };
88
+
89
+ const dotVariants = {
90
+ initial: { y: 0 },
91
+ animate: {
92
+ y: [0, -12, 0],
93
+ transition: {
94
+ duration: 0.6,
95
+ repeat: Infinity,
96
+ ease: 'easeInOut' as const
97
+ }
98
+ }
99
+ };
100
+
101
+ return (
102
+ <motion.div
103
+ variants={containerVariants}
104
+ initial="initial"
105
+ animate="animate"
106
+ className={`flex items-center justify-center ${className}`}
107
+ style={{ gap, height: dotSize + 16 }}
108
+ >
109
+ {[...Array(3)].map((_, i) => (
110
+ <motion.div
111
+ key={i}
112
+ variants={dotVariants}
113
+ className="rounded-full"
114
+ style={{
115
+ backgroundColor: color,
116
+ width: dotSize,
117
+ height: dotSize,
118
+ boxShadow: `0 0 10px ${color}50`
119
+ }}
120
+ />
121
+ ))}
122
+ </motion.div>
123
+ );
124
+ };
125
+
126
+ // 3. ElasticGrid: 3x3 staggered grid scaling
127
+ export const ElasticGrid: React.FC<BaseLoaderProps> = ({
128
+ color = 'var(--color-accent)',
129
+ size = 'md',
130
+ className = ''
131
+ }) => {
132
+ const dotSize = getPixelSize(size, 6, 10, 14);
133
+ const gap = getPixelSize(size, 4, 6, 8);
134
+ const gridWidth = dotSize * 3 + gap * 2;
135
+
136
+ const dotVariants = (index: number) => ({
137
+ animate: {
138
+ scale: [0.4, 1, 0.4],
139
+ opacity: [0.3, 1, 0.3],
140
+ transition: {
141
+ duration: 1.2,
142
+ repeat: Infinity,
143
+ ease: 'easeInOut' as const,
144
+ delay: (index % 3 + Math.floor(index / 3)) * 0.15
145
+ }
146
+ }
147
+ });
148
+
149
+ return (
150
+ <div
151
+ className={`grid grid-cols-3 justify-center items-center ${className}`}
152
+ style={{ gap, width: gridWidth, height: gridWidth }}
153
+ >
154
+ {[...Array(9)].map((_, i) => (
155
+ <motion.div
156
+ key={i}
157
+ animate="animate"
158
+ variants={dotVariants(i)}
159
+ className="rounded-full"
160
+ style={{
161
+ backgroundColor: color,
162
+ width: dotSize,
163
+ height: dotSize,
164
+ boxShadow: `0 0 8px ${color}40`
165
+ }}
166
+ />
167
+ ))}
168
+ </div>
169
+ );
170
+ };
171
+
172
+ // 4. ConcentricCircles: Rings rotating in opposite directions
173
+ export const ConcentricCircles: React.FC<BaseLoaderProps> = ({
174
+ color = 'var(--color-accent)',
175
+ size = 'md',
176
+ className = ''
177
+ }) => {
178
+ const outerSize = getPixelSize(size, 28, 48, 68);
179
+ const midSize = outerSize * 0.65;
180
+ const innerSize = outerSize * 0.35;
181
+
182
+ return (
183
+ <div
184
+ className={`relative flex items-center justify-center ${className}`}
185
+ style={{ width: outerSize, height: outerSize }}
186
+ >
187
+ {/* Outer Ring */}
188
+ <motion.div
189
+ animate={{ rotate: 360 }}
190
+ transition={{ duration: 1.8, repeat: Infinity, ease: 'linear' }}
191
+ className="absolute rounded-full border-2 border-transparent border-t-accent"
192
+ style={{
193
+ width: '100%',
194
+ height: '100%',
195
+ borderColor: `${color} 2px`,
196
+ borderStyle: 'solid',
197
+ borderLeftColor: 'transparent',
198
+ borderRightColor: 'transparent',
199
+ borderBottomColor: 'transparent',
200
+ }}
201
+ />
202
+ {/* Middle Ring */}
203
+ <motion.div
204
+ animate={{ rotate: -360 }}
205
+ transition={{ duration: 1.2, repeat: Infinity, ease: 'linear' }}
206
+ className="absolute rounded-full border-2 border-transparent"
207
+ style={{
208
+ width: midSize,
209
+ height: midSize,
210
+ borderColor: `${color}40 2px`,
211
+ borderStyle: 'solid',
212
+ borderTopColor: color,
213
+ borderBottomColor: color,
214
+ }}
215
+ />
216
+ {/* Inner Dot */}
217
+ <motion.div
218
+ animate={{ scale: [0.8, 1.2, 0.8] }}
219
+ transition={{ duration: 1, repeat: Infinity, ease: 'easeInOut' }}
220
+ className="absolute rounded-full"
221
+ style={{
222
+ width: innerSize,
223
+ height: innerSize,
224
+ backgroundColor: color,
225
+ boxShadow: `0 0 12px ${color}`
226
+ }}
227
+ />
228
+ </div>
229
+ );
230
+ };
231
+
232
+ // 5. MeshSpinner: orbital nodes rotating in a ring
233
+ export const MeshSpinner: React.FC<BaseLoaderProps> = ({
234
+ color = 'var(--color-accent)',
235
+ size = 'md',
236
+ className = ''
237
+ }) => {
238
+ const containerSize = getPixelSize(size, 32, 54, 76);
239
+ const nodeSize = getPixelSize(size, 5, 8, 11);
240
+
241
+ return (
242
+ <div
243
+ className={`relative flex items-center justify-center ${className}`}
244
+ style={{ width: containerSize, height: containerSize }}
245
+ >
246
+ <motion.div
247
+ animate={{ rotate: 360 }}
248
+ transition={{ duration: 1.5, repeat: Infinity, ease: 'linear' }}
249
+ className="relative w-full h-full"
250
+ >
251
+ {[...Array(6)].map((_, i) => {
252
+ const angle = (i * 360) / 6;
253
+ const radius = (containerSize - nodeSize) / 2;
254
+ const x = radius * Math.cos((angle * Math.PI) / 180);
255
+ const y = radius * Math.sin((angle * Math.PI) / 180);
256
+
257
+ return (
258
+ <motion.div
259
+ key={i}
260
+ animate={{
261
+ scale: [0.5, 1.1, 0.5],
262
+ opacity: [0.4, 1, 0.4]
263
+ }}
264
+ transition={{
265
+ duration: 1.2,
266
+ repeat: Infinity,
267
+ delay: i * 0.12,
268
+ ease: 'easeInOut' as const
269
+ }}
270
+ className="absolute rounded-full"
271
+ style={{
272
+ backgroundColor: color,
273
+ width: nodeSize,
274
+ height: nodeSize,
275
+ left: `calc(50% - ${nodeSize / 2}px + ${x}px)`,
276
+ top: `calc(50% - ${nodeSize / 2}px + ${y}px)`,
277
+ boxShadow: `0 0 10px ${color}`
278
+ }}
279
+ />
280
+ );
281
+ })}
282
+ </motion.div>
283
+ </div>
284
+ );
285
+ };
286
+
287
+ // 6. InfinityLoop: Animated infinity SVG loop
288
+ export const InfinityLoop: React.FC<BaseLoaderProps> = ({
289
+ color = 'var(--color-accent)',
290
+ size = 'md',
291
+ className = ''
292
+ }) => {
293
+ const width = getPixelSize(size, 48, 80, 112);
294
+ const height = width / 2;
295
+
296
+ return (
297
+ <div
298
+ className={`flex items-center justify-center ${className}`}
299
+ style={{ width, height }}
300
+ >
301
+ <svg
302
+ viewBox="0 0 100 50"
303
+ className="w-full h-full overflow-visible"
304
+ >
305
+ {/* Draw Infinity Path Background */}
306
+ <path
307
+ d="M 30,25 C 15,5 5,17 5,25 C 5,33 15,45 30,25 C 45,5 55,5 70,25 C 85,45 95,33 95,25 C 95,17 85,5 70,25 C 55,45 45,45 30,25 Z"
308
+ fill="none"
309
+ stroke={`${color}20`}
310
+ strokeWidth="3.5"
311
+ strokeLinecap="round"
312
+ />
313
+
314
+ {/* Animated Moving Path Overlay */}
315
+ <motion.path
316
+ d="M 30,25 C 15,5 5,17 5,25 C 5,33 15,45 30,25 C 45,5 55,5 70,25 C 85,45 95,33 95,25 C 95,17 85,5 70,25 C 55,45 45,45 30,25 Z"
317
+ fill="none"
318
+ stroke={color}
319
+ strokeWidth="3.5"
320
+ strokeLinecap="round"
321
+ strokeDasharray="20, 100"
322
+ animate={{
323
+ strokeDashoffset: [0, -120]
324
+ }}
325
+ transition={{
326
+ duration: 1.6,
327
+ repeat: Infinity,
328
+ ease: 'linear'
329
+ }}
330
+ style={{
331
+ filter: `drop-shadow(0 0 6px ${color})`
332
+ }}
333
+ />
334
+ </svg>
335
+ </div>
336
+ );
337
+ };
338
+
339
+ // Collection Wrapper component to showcase or render dynamic types
340
+ export interface LoaderCollectionProps {
341
+ type?: 'pulse' | 'bounce' | 'grid' | 'concentric' | 'mesh' | 'infinity';
342
+ color?: string;
343
+ size?: 'sm' | 'md' | 'lg';
344
+ className?: string;
345
+ }
346
+
347
+ export const LoaderCollection: React.FC<LoaderCollectionProps> = ({
348
+ type = 'pulse',
349
+ color,
350
+ size,
351
+ className
352
+ }) => {
353
+ switch (type) {
354
+ case 'bounce':
355
+ return <BouncingDots color={color} size={size} className={className} />;
356
+ case 'grid':
357
+ return <ElasticGrid color={color} size={size} className={className} />;
358
+ case 'concentric':
359
+ return <ConcentricCircles color={color} size={size} className={className} />;
360
+ case 'mesh':
361
+ return <MeshSpinner color={color} size={size} className={className} />;
362
+ case 'infinity':
363
+ return <InfinityLoop color={color} size={size} className={className} />;
364
+ case 'pulse':
365
+ default:
366
+ return <PulseBars color={color} size={size} className={className} />;
367
+ }
368
+ };
@@ -0,0 +1,192 @@
1
+ import React, { useState } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { GlowInput } from './GlowInput';
4
+ import { MorphingSwitch } from './MorphingSwitch';
5
+ import { MagneticButton } from './MagneticButton';
6
+ import { Sparkles } from 'lucide-react';
7
+ import { Skeleton } from './Skeleton';
8
+
9
+ export interface LoginFormProps {
10
+ onSubmit?: (data: { email: string; pass: string; remember: boolean }) => void;
11
+ className?: string;
12
+ isLoading?: boolean;
13
+ disabled?: boolean;
14
+ }
15
+
16
+ export const LoginForm: React.FC<LoginFormProps> = ({
17
+ onSubmit,
18
+ className = '',
19
+ isLoading = false,
20
+ disabled = false
21
+ }) => {
22
+ const [email, setEmail] = useState('');
23
+ const [password, setPassword] = useState('');
24
+ const [remember, setRemember] = useState(false);
25
+ const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
26
+
27
+ const validate = () => {
28
+ const nextErrors: { email?: string; password?: string } = {};
29
+ if (!email) {
30
+ nextErrors.email = 'El correo es obligatorio';
31
+ } else if (!email.includes('@')) {
32
+ nextErrors.email = 'Correo electrónico no válido';
33
+ }
34
+
35
+ if (!password) {
36
+ nextErrors.password = 'La contraseña es obligatoria';
37
+ } else if (password.length < 6) {
38
+ nextErrors.password = 'Debe contener al menos 6 caracteres';
39
+ }
40
+
41
+ setErrors(nextErrors);
42
+ return Object.keys(nextErrors).length === 0;
43
+ };
44
+
45
+ const handleSubmit = (e: React.FormEvent) => {
46
+ e.preventDefault();
47
+ if (disabled) return;
48
+ if (validate() && onSubmit) {
49
+ onSubmit({ email, pass: password, remember });
50
+ }
51
+ };
52
+
53
+ // Staggered layout animations
54
+ const formContainerVariants = {
55
+ hidden: { opacity: 0, scale: 0.95, y: 15 },
56
+ show: {
57
+ opacity: 1,
58
+ scale: 1,
59
+ y: 0,
60
+ transition: {
61
+ type: 'spring' as const,
62
+ stiffness: 300,
63
+ damping: 25,
64
+ staggerChildren: 0.08,
65
+ delayChildren: 0.1
66
+ }
67
+ }
68
+ };
69
+
70
+ const itemVariants = {
71
+ hidden: { opacity: 0, y: 10 },
72
+ show: { opacity: 1, y: 0, transition: { type: 'spring' as const, stiffness: 300, damping: 25 } }
73
+ };
74
+
75
+ return (
76
+ <motion.div
77
+ variants={formContainerVariants}
78
+ initial="hidden"
79
+ animate="show"
80
+ className={`w-full max-w-md glass rounded-3xl p-8 bg-bg-card/70 border border-border-app shadow-2xl ${className}`}
81
+ >
82
+ {isLoading ? (
83
+ <div className="flex flex-col gap-6">
84
+ <div className="text-center flex flex-col items-center gap-1">
85
+ <Skeleton variant="circle" className="w-10 h-10 mb-2" />
86
+ <Skeleton variant="text" className="w-32 h-5 mb-1 animate-pulse" />
87
+ <Skeleton variant="text" className="w-48 h-3 animate-pulse" />
88
+ </div>
89
+ <div className="flex flex-col gap-6">
90
+ <div className="flex flex-col gap-2">
91
+ <Skeleton variant="text" className="w-24 h-3 animate-pulse" />
92
+ <Skeleton variant="rect" className="w-full h-11" />
93
+ </div>
94
+ <div className="flex flex-col gap-2">
95
+ <Skeleton variant="text" className="w-20 h-3 animate-pulse" />
96
+ <Skeleton variant="rect" className="w-full h-11" />
97
+ </div>
98
+ </div>
99
+ <div className="flex items-center justify-between text-xs">
100
+ <div className="flex items-center gap-2">
101
+ <Skeleton variant="rect" className="w-10 h-6 rounded-full" />
102
+ <Skeleton variant="text" className="w-16 h-3 animate-pulse" />
103
+ </div>
104
+ <Skeleton variant="text" className="w-28 h-3 animate-pulse" />
105
+ </div>
106
+ <Skeleton variant="rect" className="w-full h-12 mt-2" />
107
+ </div>
108
+ ) : (
109
+ <form onSubmit={handleSubmit} className="flex flex-col gap-6">
110
+
111
+ {/* Title Group */}
112
+ <motion.div variants={itemVariants} className="text-center flex flex-col items-center gap-1">
113
+ <div className="w-10 h-10 rounded-2xl bg-accent/15 border border-accent/20 flex items-center justify-center text-accent mb-2">
114
+ <Sparkles className="w-5 h-5 animate-pulse" />
115
+ </div>
116
+ <h2 className="text-2xl font-extrabold tracking-tight text-text-main font-display">
117
+ ¡Te damos la bienvenida!
118
+ </h2>
119
+ <p className="text-xs text-text-muted">
120
+ Ingresá tus credenciales para acceder a tu panel
121
+ </p>
122
+ </motion.div>
123
+
124
+ {/* Inputs */}
125
+ <div className="flex flex-col gap-4">
126
+ <motion.div variants={itemVariants}>
127
+ <GlowInput
128
+ label="Correo Electrónico"
129
+ type="email"
130
+ value={email}
131
+ onChange={(e) => setEmail(e.target.value)}
132
+ error={errors.email}
133
+ placeholder="ejemplo@correo.com"
134
+ disabled={disabled}
135
+ />
136
+ </motion.div>
137
+
138
+ <motion.div variants={itemVariants}>
139
+ <GlowInput
140
+ label="Contraseña"
141
+ type="password"
142
+ value={password}
143
+ onChange={(e) => setPassword(e.target.value)}
144
+ error={errors.password}
145
+ placeholder="••••••••"
146
+ disabled={disabled}
147
+ />
148
+ </motion.div>
149
+ </div>
150
+
151
+ {/* Remember / Forget Password settings */}
152
+ <motion.div
153
+ variants={itemVariants}
154
+ className="flex items-center justify-between text-xs"
155
+ >
156
+ <div className="flex items-center gap-2">
157
+ <MorphingSwitch
158
+ checked={remember}
159
+ onChange={setRemember}
160
+ disabled={disabled}
161
+ />
162
+ <span className="font-semibold text-text-muted select-none">Recordarme</span>
163
+ </div>
164
+
165
+ <a
166
+ href="#"
167
+ onClick={(e) => disabled && e.preventDefault()}
168
+ className={`font-bold text-accent transition-colors ${
169
+ disabled ? 'opacity-40 cursor-not-allowed text-text-muted' : 'hover:text-accent-hover'
170
+ }`}
171
+ >
172
+ ¿Olvidaste tu contraseña?
173
+ </a>
174
+ </motion.div>
175
+
176
+ {/* Submit button */}
177
+ <motion.div variants={itemVariants} className="w-full flex justify-center mt-2">
178
+ <MagneticButton
179
+ type="submit"
180
+ className="w-full py-3.5"
181
+ range={60}
182
+ disabled={disabled || isLoading}
183
+ >
184
+ Iniciar Sesión
185
+ </MagneticButton>
186
+ </motion.div>
187
+
188
+ </form>
189
+ )}
190
+ </motion.div>
191
+ );
192
+ };
@@ -0,0 +1,101 @@
1
+ import React, { useRef } from 'react';
2
+ import { motion, useMotionValue, useSpring } from 'framer-motion';
3
+
4
+ type MotionSafeButtonProps = Omit<
5
+ React.ButtonHTMLAttributes<HTMLButtonElement>,
6
+ 'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration' | 'onDrag' | 'onDragEnd' | 'onDragStart'
7
+ >;
8
+
9
+ export interface MagneticButtonProps extends MotionSafeButtonProps {
10
+ children: React.ReactNode;
11
+ className?: string;
12
+ range?: number; // Mouse distance range where magnetic effect activates
13
+ }
14
+
15
+ export const MagneticButton: React.FC<MagneticButtonProps> = ({
16
+ children,
17
+ className = '',
18
+ range = 60,
19
+ ...props
20
+ }) => {
21
+ const buttonRef = useRef<HTMLButtonElement>(null);
22
+
23
+ // Motion values for button container movement
24
+ const x = useMotionValue(0);
25
+ const y = useMotionValue(0);
26
+
27
+ // Motion values for internal text movement (parallax)
28
+ const textX = useMotionValue(0);
29
+ const textY = useMotionValue(0);
30
+
31
+ // Smooth springs for button
32
+ const springX = useSpring(x, { stiffness: 80, damping: 15, mass: 0.8 });
33
+ const springY = useSpring(y, { stiffness: 80, damping: 15, mass: 0.8 });
34
+
35
+ // Smooth springs for text
36
+ const textSpringX = useSpring(textX, { stiffness: 120, damping: 15, mass: 0.6 });
37
+ const textSpringY = useSpring(textY, { stiffness: 120, damping: 15, mass: 0.6 });
38
+
39
+ const handleMouseMove = (e: React.MouseEvent) => {
40
+ if (!buttonRef.current || props.disabled) return;
41
+
42
+ const { clientX, clientY } = e;
43
+ const { left, top, width, height } = buttonRef.current.getBoundingClientRect();
44
+
45
+ const centerX = left + width / 2;
46
+ const centerY = top + height / 2;
47
+
48
+ const distanceX = clientX - centerX;
49
+ const distanceY = clientY - centerY;
50
+ const distance = Math.hypot(distanceX, distanceY);
51
+
52
+ if (distance < range) {
53
+ // Calculate pull strength based on distance (closer = stronger pull)
54
+ const pull = 0.4;
55
+ x.set(distanceX * pull);
56
+ y.set(distanceY * pull);
57
+
58
+ // Text moves a bit less for parallax depth
59
+ textX.set(distanceX * 0.2);
60
+ textY.set(distanceY * 0.2);
61
+ } else {
62
+ handleMouseLeave();
63
+ }
64
+ };
65
+
66
+ const handleMouseLeave = () => {
67
+ x.set(0);
68
+ y.set(0);
69
+ textX.set(0);
70
+ textY.set(0);
71
+ };
72
+
73
+ return (
74
+ <motion.button
75
+ ref={buttonRef}
76
+ onMouseMove={handleMouseMove}
77
+ onMouseLeave={handleMouseLeave}
78
+ style={{
79
+ x: springX,
80
+ y: springY,
81
+ }}
82
+ className={`relative inline-flex items-center justify-center rounded-xl bg-accent px-6 py-3 font-semibold text-white shadow-md transition-colors select-none ${
83
+ props.disabled
84
+ ? 'opacity-40 cursor-not-allowed bg-accent/60'
85
+ : 'hover:bg-accent-hover active:scale-95 cursor-pointer'
86
+ } ${className}`}
87
+ {...props}
88
+ >
89
+ <motion.span
90
+ style={{
91
+ x: textSpringX,
92
+ y: textSpringY,
93
+ display: 'block',
94
+ }}
95
+ className="pointer-events-none"
96
+ >
97
+ {children}
98
+ </motion.span>
99
+ </motion.button>
100
+ );
101
+ };