ai-react-animations 1.1.3 → 1.2.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.
@@ -0,0 +1,857 @@
1
+ /**
2
+ * AI React Animations - Component Registry
3
+ * Contains metadata and source code for all animation components
4
+ */
5
+
6
+ export interface ComponentInfo {
7
+ id: string;
8
+ name: string;
9
+ description: string;
10
+ category: 'loading' | 'processing' | 'creative' | 'auth';
11
+ dependencies: string[];
12
+ props: Array<{
13
+ name: string;
14
+ type: string;
15
+ required: boolean;
16
+ description: string;
17
+ default?: string;
18
+ }>;
19
+ usage: string;
20
+ source: string;
21
+ }
22
+
23
+ export const components: ComponentInfo[] = [
24
+ {
25
+ id: 'loading-dots',
26
+ name: 'LoadingDots',
27
+ description: 'Simple, elegant bouncing dots animation. Perfect for minimal loading states and inline indicators.',
28
+ category: 'loading',
29
+ dependencies: ['framer-motion'],
30
+ props: [
31
+ { name: 'size', type: "'sm' | 'md' | 'lg'", required: false, description: 'Size of the dots', default: "'md'" },
32
+ { name: 'color', type: 'string', required: false, description: 'Color of the dots (hex or CSS color)', default: "'#6366F1'" },
33
+ ],
34
+ usage: `import { LoadingDots } from '@/components/LoadingDots';
35
+
36
+ function MyComponent() {
37
+ return <LoadingDots size="md" color="#6366F1" />;
38
+ }`,
39
+ source: `'use client';
40
+
41
+ import { motion } from 'framer-motion';
42
+
43
+ interface LoadingDotsProps {
44
+ size?: 'sm' | 'md' | 'lg';
45
+ color?: string;
46
+ }
47
+
48
+ const sizeMap = {
49
+ sm: { dot: 6, gap: 4 },
50
+ md: { dot: 10, gap: 6 },
51
+ lg: { dot: 14, gap: 8 },
52
+ };
53
+
54
+ export default function LoadingDots({ size = 'md', color = '#6366F1' }: LoadingDotsProps) {
55
+ const { dot, gap } = sizeMap[size];
56
+
57
+ return (
58
+ <div className="flex items-center justify-center" style={{ gap }}>
59
+ {[0, 1, 2].map((i) => (
60
+ <motion.div
61
+ key={i}
62
+ style={{
63
+ width: dot,
64
+ height: dot,
65
+ backgroundColor: color,
66
+ borderRadius: '50%',
67
+ }}
68
+ animate={{ y: [0, -dot, 0] }}
69
+ transition={{
70
+ duration: 0.6,
71
+ repeat: Infinity,
72
+ delay: i * 0.15,
73
+ ease: 'easeInOut',
74
+ }}
75
+ />
76
+ ))}
77
+ </div>
78
+ );
79
+ }`,
80
+ },
81
+ {
82
+ id: 'pulse-circle',
83
+ name: 'PulseCircle',
84
+ description: 'Circular progress indicator with expanding pulse rings and percentage display. Shows completion checkmark at 100%.',
85
+ category: 'processing',
86
+ dependencies: ['framer-motion'],
87
+ props: [
88
+ { name: 'isActive', type: 'boolean', required: true, description: 'Activates the animation' },
89
+ { name: 'progress', type: 'number', required: false, description: 'External progress value (0-100)' },
90
+ { name: 'onComplete', type: '() => void', required: false, description: 'Callback when progress reaches 100%' },
91
+ ],
92
+ usage: `import { PulseCircle } from '@/components/PulseCircle';
93
+
94
+ function MyComponent() {
95
+ const [isActive, setIsActive] = useState(false);
96
+
97
+ return (
98
+ <PulseCircle
99
+ isActive={isActive}
100
+ onComplete={() => setIsActive(false)}
101
+ />
102
+ );
103
+ }`,
104
+ source: `'use client';
105
+
106
+ import { useState, useEffect } from 'react';
107
+ import { motion, AnimatePresence } from 'framer-motion';
108
+
109
+ interface PulseCircleProps {
110
+ isActive: boolean;
111
+ progress?: number;
112
+ onComplete?: () => void;
113
+ }
114
+
115
+ export default function PulseCircle({ isActive, progress: externalProgress, onComplete }: PulseCircleProps) {
116
+ const [internalProgress, setInternalProgress] = useState(0);
117
+ const progress = externalProgress ?? internalProgress;
118
+
119
+ useEffect(() => {
120
+ if (!isActive || externalProgress !== undefined) {
121
+ if (!isActive) setInternalProgress(0);
122
+ return;
123
+ }
124
+
125
+ const interval = setInterval(() => {
126
+ setInternalProgress((prev) => {
127
+ if (prev >= 100) {
128
+ clearInterval(interval);
129
+ onComplete?.();
130
+ return 100;
131
+ }
132
+ return prev + Math.random() * 3 + 1;
133
+ });
134
+ }, 100);
135
+
136
+ return () => clearInterval(interval);
137
+ }, [isActive, externalProgress, onComplete]);
138
+
139
+ const isComplete = progress >= 100;
140
+ const radius = 45;
141
+ const circumference = 2 * Math.PI * radius;
142
+ const strokeDashoffset = circumference - (Math.min(progress, 100) / 100) * circumference;
143
+
144
+ return (
145
+ <div className="relative w-32 h-32">
146
+ {/* Pulse rings */}
147
+ <AnimatePresence>
148
+ {isActive && !isComplete && [0, 1, 2].map((i) => (
149
+ <motion.div
150
+ key={i}
151
+ className="absolute inset-0 rounded-full border-2 border-blue-400"
152
+ initial={{ scale: 1, opacity: 0.6 }}
153
+ animate={{ scale: 2, opacity: 0 }}
154
+ transition={{
155
+ duration: 2,
156
+ repeat: Infinity,
157
+ delay: i * 0.6,
158
+ ease: 'easeOut',
159
+ }}
160
+ />
161
+ ))}
162
+ </AnimatePresence>
163
+
164
+ {/* Progress circle */}
165
+ <svg className="w-full h-full -rotate-90" viewBox="0 0 100 100">
166
+ <circle cx="50" cy="50" r={radius} fill="none" stroke="#e5e7eb" strokeWidth="6" />
167
+ <motion.circle
168
+ cx="50"
169
+ cy="50"
170
+ r={radius}
171
+ fill="none"
172
+ stroke={isComplete ? '#10b981' : '#3b82f6'}
173
+ strokeWidth="6"
174
+ strokeLinecap="round"
175
+ strokeDasharray={circumference}
176
+ initial={{ strokeDashoffset: circumference }}
177
+ animate={{ strokeDashoffset }}
178
+ transition={{ duration: 0.3 }}
179
+ />
180
+ </svg>
181
+
182
+ {/* Center content */}
183
+ <div className="absolute inset-0 flex items-center justify-center">
184
+ <AnimatePresence mode="wait">
185
+ {isComplete ? (
186
+ <motion.svg
187
+ key="check"
188
+ initial={{ scale: 0 }}
189
+ animate={{ scale: 1 }}
190
+ className="w-12 h-12 text-green-500"
191
+ fill="none"
192
+ viewBox="0 0 24 24"
193
+ stroke="currentColor"
194
+ >
195
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
196
+ </motion.svg>
197
+ ) : (
198
+ <motion.span
199
+ key="progress"
200
+ className="text-2xl font-bold text-gray-700"
201
+ >
202
+ {Math.round(progress)}%
203
+ </motion.span>
204
+ )}
205
+ </AnimatePresence>
206
+ </div>
207
+ </div>
208
+ );
209
+ }`,
210
+ },
211
+ {
212
+ id: 'code-typing',
213
+ name: 'CodeTyping',
214
+ description: 'Realistic code typing effect with syntax highlighting and terminal-style presentation.',
215
+ category: 'creative',
216
+ dependencies: ['framer-motion'],
217
+ props: [
218
+ { name: 'isTyping', type: 'boolean', required: true, description: 'Controls the typing animation' },
219
+ { name: 'onComplete', type: '() => void', required: false, description: 'Callback when typing completes' },
220
+ ],
221
+ usage: `import { CodeTyping } from '@/components/CodeTyping';
222
+
223
+ function MyComponent() {
224
+ const [isTyping, setIsTyping] = useState(false);
225
+
226
+ return (
227
+ <CodeTyping
228
+ isTyping={isTyping}
229
+ onComplete={() => setIsTyping(false)}
230
+ />
231
+ );
232
+ }`,
233
+ source: `'use client';
234
+
235
+ import { useState, useEffect } from 'react';
236
+ import { motion } from 'framer-motion';
237
+
238
+ interface CodeTypingProps {
239
+ isTyping: boolean;
240
+ onComplete?: () => void;
241
+ }
242
+
243
+ const codeLines = [
244
+ { text: 'const', color: '#c678dd' },
245
+ { text: ' ai', color: '#e06c75' },
246
+ { text: ' = ', color: '#abb2bf' },
247
+ { text: 'await', color: '#c678dd' },
248
+ { text: ' generateResponse', color: '#61afef' },
249
+ { text: '(', color: '#abb2bf' },
250
+ { text: 'prompt', color: '#e5c07b' },
251
+ { text: ');', color: '#abb2bf' },
252
+ ];
253
+
254
+ export default function CodeTyping({ isTyping, onComplete }: CodeTypingProps) {
255
+ const [displayedCode, setDisplayedCode] = useState<typeof codeLines>([]);
256
+ const [currentIndex, setCurrentIndex] = useState(0);
257
+
258
+ useEffect(() => {
259
+ if (!isTyping) {
260
+ setDisplayedCode([]);
261
+ setCurrentIndex(0);
262
+ return;
263
+ }
264
+
265
+ if (currentIndex >= codeLines.length) {
266
+ onComplete?.();
267
+ return;
268
+ }
269
+
270
+ const timeout = setTimeout(() => {
271
+ setDisplayedCode((prev) => [...prev, codeLines[currentIndex]]);
272
+ setCurrentIndex((prev) => prev + 1);
273
+ }, 150);
274
+
275
+ return () => clearTimeout(timeout);
276
+ }, [isTyping, currentIndex, onComplete]);
277
+
278
+ return (
279
+ <div className="bg-[#282c34] rounded-lg p-4 font-mono text-sm min-w-[300px]">
280
+ <div className="flex gap-2 mb-3">
281
+ <div className="w-3 h-3 rounded-full bg-red-500" />
282
+ <div className="w-3 h-3 rounded-full bg-yellow-500" />
283
+ <div className="w-3 h-3 rounded-full bg-green-500" />
284
+ </div>
285
+ <div className="flex items-center">
286
+ <span className="text-gray-500 mr-2">1</span>
287
+ {displayedCode.map((token, i) => (
288
+ <span key={i} style={{ color: token.color }}>{token.text}</span>
289
+ ))}
290
+ {isTyping && currentIndex < codeLines.length && (
291
+ <motion.span
292
+ className="w-2 h-5 bg-white ml-0.5"
293
+ animate={{ opacity: [1, 0] }}
294
+ transition={{ duration: 0.5, repeat: Infinity }}
295
+ />
296
+ )}
297
+ </div>
298
+ </div>
299
+ );
300
+ }`,
301
+ },
302
+ {
303
+ id: 'data-processing',
304
+ name: 'DataProcessing',
305
+ description: 'Data pipeline visualization showing input, processing, and output stages with flowing data cards.',
306
+ category: 'processing',
307
+ dependencies: ['framer-motion', 'lucide-react'],
308
+ props: [
309
+ { name: 'isProcessing', type: 'boolean', required: true, description: 'Controls the processing animation' },
310
+ { name: 'onComplete', type: '() => void', required: false, description: 'Callback when processing completes' },
311
+ ],
312
+ usage: `import { DataProcessing } from '@/components/DataProcessing';
313
+
314
+ function MyComponent() {
315
+ const [isProcessing, setIsProcessing] = useState(false);
316
+
317
+ return (
318
+ <DataProcessing
319
+ isProcessing={isProcessing}
320
+ onComplete={() => setIsProcessing(false)}
321
+ />
322
+ );
323
+ }`,
324
+ source: `'use client';
325
+
326
+ import { useState, useEffect } from 'react';
327
+ import { motion, AnimatePresence } from 'framer-motion';
328
+ import { Database, Cpu, CheckCircle } from 'lucide-react';
329
+
330
+ interface DataProcessingProps {
331
+ isProcessing: boolean;
332
+ onComplete?: () => void;
333
+ }
334
+
335
+ export default function DataProcessing({ isProcessing, onComplete }: DataProcessingProps) {
336
+ const [stage, setStage] = useState<'idle' | 'input' | 'process' | 'output' | 'complete'>('idle');
337
+ const [progress, setProgress] = useState(0);
338
+
339
+ useEffect(() => {
340
+ if (!isProcessing) {
341
+ setStage('idle');
342
+ setProgress(0);
343
+ return;
344
+ }
345
+
346
+ setStage('input');
347
+ const stages = [
348
+ { stage: 'process' as const, delay: 800 },
349
+ { stage: 'output' as const, delay: 1700 },
350
+ { stage: 'complete' as const, delay: 2500 },
351
+ ];
352
+
353
+ const timeouts = stages.map(({ stage, delay }) =>
354
+ setTimeout(() => setStage(stage), delay)
355
+ );
356
+
357
+ const progressInterval = setInterval(() => {
358
+ setProgress((p) => Math.min(p + 4, 100));
359
+ }, 100);
360
+
361
+ const completeTimeout = setTimeout(() => {
362
+ onComplete?.();
363
+ }, 3000);
364
+
365
+ return () => {
366
+ timeouts.forEach(clearTimeout);
367
+ clearTimeout(completeTimeout);
368
+ clearInterval(progressInterval);
369
+ };
370
+ }, [isProcessing, onComplete]);
371
+
372
+ const stages = [
373
+ { id: 'input', icon: Database, label: 'Input', active: ['input', 'process', 'output', 'complete'].includes(stage) },
374
+ { id: 'process', icon: Cpu, label: 'Processing', active: ['process', 'output', 'complete'].includes(stage) },
375
+ { id: 'output', icon: CheckCircle, label: 'Output', active: ['output', 'complete'].includes(stage) },
376
+ ];
377
+
378
+ return (
379
+ <div className="flex flex-col items-center gap-6 p-6">
380
+ <div className="flex items-center gap-4">
381
+ {stages.map((s, i) => (
382
+ <div key={s.id} className="flex items-center">
383
+ <motion.div
384
+ className={\`w-16 h-16 rounded-xl flex items-center justify-center \${
385
+ s.active ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-400'
386
+ }\`}
387
+ animate={{ scale: stage === s.id ? [1, 1.1, 1] : 1 }}
388
+ transition={{ duration: 0.3 }}
389
+ >
390
+ <s.icon className="w-8 h-8" />
391
+ </motion.div>
392
+ {i < stages.length - 1 && (
393
+ <div className="w-12 h-1 mx-2 bg-gray-200 rounded overflow-hidden">
394
+ <motion.div
395
+ className="h-full bg-blue-500"
396
+ initial={{ width: 0 }}
397
+ animate={{ width: s.active ? '100%' : 0 }}
398
+ transition={{ duration: 0.5 }}
399
+ />
400
+ </div>
401
+ )}
402
+ </div>
403
+ ))}
404
+ </div>
405
+ <div className="text-center">
406
+ <p className="text-lg font-medium text-gray-700 capitalize">{stage === 'idle' ? 'Ready' : stage}</p>
407
+ <p className="text-sm text-gray-500">{progress}% complete</p>
408
+ </div>
409
+ </div>
410
+ );
411
+ }`,
412
+ },
413
+ {
414
+ id: 'ai-creating',
415
+ name: 'AiCreating',
416
+ description: 'Multi-stage animation visualizing AI creation process with thinking, writing, building, and completion phases.',
417
+ category: 'creative',
418
+ dependencies: ['framer-motion'],
419
+ props: [
420
+ { name: 'isLoading', type: 'boolean', required: true, description: 'Controls the animation state' },
421
+ { name: 'size', type: "'sm' | 'md' | 'lg'", required: false, description: 'Size of the animation', default: "'md'" },
422
+ { name: 'onComplete', type: '() => void', required: false, description: 'Callback when animation completes' },
423
+ ],
424
+ usage: `import { AiCreating } from '@/components/AiCreating';
425
+
426
+ function MyComponent() {
427
+ const [isLoading, setIsLoading] = useState(false);
428
+
429
+ return (
430
+ <AiCreating
431
+ isLoading={isLoading}
432
+ size="md"
433
+ onComplete={() => setIsLoading(false)}
434
+ />
435
+ );
436
+ }`,
437
+ source: `'use client';
438
+
439
+ import { useState, useEffect } from 'react';
440
+ import { motion, AnimatePresence } from 'framer-motion';
441
+ import './AiCreating.css';
442
+
443
+ interface AiCreatingProps {
444
+ isLoading: boolean;
445
+ size?: 'sm' | 'md' | 'lg';
446
+ onComplete?: () => void;
447
+ }
448
+
449
+ const stages = ['thinking', 'writing', 'building', 'complete'] as const;
450
+ type Stage = typeof stages[number];
451
+
452
+ const stageMessages: Record<Stage, string> = {
453
+ thinking: 'Thinking...',
454
+ writing: 'Writing...',
455
+ building: 'Building...',
456
+ complete: 'Done!',
457
+ };
458
+
459
+ export default function AiCreating({ isLoading, size = 'md', onComplete }: AiCreatingProps) {
460
+ const [stage, setStage] = useState<Stage>('thinking');
461
+
462
+ const sizeClasses = {
463
+ sm: 'w-16 h-16',
464
+ md: 'w-24 h-24',
465
+ lg: 'w-32 h-32',
466
+ };
467
+
468
+ useEffect(() => {
469
+ if (!isLoading) {
470
+ setStage('thinking');
471
+ return;
472
+ }
473
+
474
+ const timings = [2000, 4000, 6000];
475
+ const timeouts = timings.map((delay, i) =>
476
+ setTimeout(() => setStage(stages[i + 1]), delay)
477
+ );
478
+
479
+ const completeTimeout = setTimeout(() => onComplete?.(), 7000);
480
+
481
+ return () => {
482
+ timeouts.forEach(clearTimeout);
483
+ clearTimeout(completeTimeout);
484
+ };
485
+ }, [isLoading, onComplete]);
486
+
487
+ if (!isLoading) return null;
488
+
489
+ return (
490
+ <div className="flex flex-col items-center gap-4">
491
+ <div className={\`relative \${sizeClasses[size]}\`}>
492
+ <motion.div
493
+ className="ai-robot w-full h-full"
494
+ animate={{
495
+ y: stage === 'thinking' ? [0, -5, 0] : 0,
496
+ rotate: stage === 'building' ? [0, 5, -5, 0] : 0,
497
+ }}
498
+ transition={{ duration: 1, repeat: stage !== 'complete' ? Infinity : 0 }}
499
+ >
500
+ {/* Robot SVG */}
501
+ <svg viewBox="0 0 100 100" className="w-full h-full">
502
+ <rect x="25" y="30" width="50" height="45" rx="8" fill="#6366f1" />
503
+ <rect x="35" y="45" width="10" height="10" rx="2" fill="white" />
504
+ <rect x="55" y="45" width="10" height="10" rx="2" fill="white" />
505
+ <rect x="40" y="62" width="20" height="5" rx="2" fill="white" />
506
+ <rect x="42" y="20" width="16" height="15" rx="3" fill="#6366f1" />
507
+ <circle cx="50" cy="12" r="4" fill="#fbbf24" />
508
+ </svg>
509
+ </motion.div>
510
+ </div>
511
+ <motion.p
512
+ key={stage}
513
+ initial={{ opacity: 0, y: 10 }}
514
+ animate={{ opacity: 1, y: 0 }}
515
+ className="text-gray-600 font-medium"
516
+ >
517
+ {stageMessages[stage]}
518
+ </motion.p>
519
+ </div>
520
+ );
521
+ }`,
522
+ },
523
+ {
524
+ id: 'ai-creating-2',
525
+ name: 'AiCreating2',
526
+ description: 'Full-screen AI brain animation with rotating rings, floating particles, and dynamic status messages.',
527
+ category: 'creative',
528
+ dependencies: ['framer-motion', 'lucide-react'],
529
+ props: [
530
+ { name: 'isLoading', type: 'boolean', required: true, description: 'Controls animation visibility' },
531
+ { name: 'message', type: 'string', required: false, description: 'Main loading message' },
532
+ { name: 'subMessage', type: 'string', required: false, description: 'Secondary description text' },
533
+ { name: 'primaryColor', type: 'string', required: false, description: 'Primary color (hex)', default: "'#6366F1'" },
534
+ { name: 'backgroundColor', type: 'string', required: false, description: 'Background color (hex)', default: "'#0f172a'" },
535
+ { name: 'textColor', type: 'string', required: false, description: 'Text color (hex)', default: "'#ffffff'" },
536
+ { name: 'contained', type: 'boolean', required: false, description: 'Render in container vs full overlay', default: 'false' },
537
+ { name: 'onComplete', type: '() => void', required: false, description: 'Callback when complete' },
538
+ ],
539
+ usage: `import { AiCreating2 } from '@/components/AiCreating2';
540
+
541
+ function MyComponent() {
542
+ const [isLoading, setIsLoading] = useState(false);
543
+
544
+ return (
545
+ <AiCreating2
546
+ isLoading={isLoading}
547
+ message="Creating your plan..."
548
+ primaryColor="#6366F1"
549
+ onComplete={() => setIsLoading(false)}
550
+ />
551
+ );
552
+ }`,
553
+ source: `'use client';
554
+
555
+ import { useEffect, useState } from 'react';
556
+ import { motion, AnimatePresence } from 'framer-motion';
557
+ import { Brain, Sparkles } from 'lucide-react';
558
+
559
+ interface AiCreating2Props {
560
+ isLoading: boolean;
561
+ message?: string;
562
+ subMessage?: string;
563
+ primaryColor?: string;
564
+ backgroundColor?: string;
565
+ textColor?: string;
566
+ contained?: boolean;
567
+ onComplete?: () => void;
568
+ }
569
+
570
+ export default function AiCreating2({
571
+ isLoading,
572
+ message = 'AI is creating...',
573
+ subMessage = 'This may take a moment',
574
+ primaryColor = '#6366F1',
575
+ backgroundColor = '#0f172a',
576
+ textColor = '#ffffff',
577
+ contained = false,
578
+ onComplete,
579
+ }: AiCreating2Props) {
580
+ const [progress, setProgress] = useState(0);
581
+
582
+ useEffect(() => {
583
+ if (!isLoading) {
584
+ setProgress(0);
585
+ return;
586
+ }
587
+
588
+ const interval = setInterval(() => {
589
+ setProgress((p) => {
590
+ if (p >= 100) {
591
+ clearInterval(interval);
592
+ setTimeout(() => onComplete?.(), 500);
593
+ return 100;
594
+ }
595
+ return p + Math.random() * 2 + 0.5;
596
+ });
597
+ }, 100);
598
+
599
+ return () => clearInterval(interval);
600
+ }, [isLoading, onComplete]);
601
+
602
+ const containerClass = contained
603
+ ? 'w-full h-full flex items-center justify-center'
604
+ : 'fixed inset-0 z-50 flex items-center justify-center';
605
+
606
+ return (
607
+ <AnimatePresence>
608
+ {isLoading && (
609
+ <motion.div
610
+ initial={{ opacity: 0 }}
611
+ animate={{ opacity: 1 }}
612
+ exit={{ opacity: 0 }}
613
+ className={containerClass}
614
+ style={{ backgroundColor }}
615
+ >
616
+ <div className="flex flex-col items-center gap-8">
617
+ {/* Brain with rings */}
618
+ <div className="relative w-32 h-32">
619
+ {[0, 1, 2].map((i) => (
620
+ <motion.div
621
+ key={i}
622
+ className="absolute inset-0 rounded-full border-2"
623
+ style={{ borderColor: primaryColor }}
624
+ animate={{ rotate: 360, scale: [1, 1.1, 1] }}
625
+ transition={{
626
+ rotate: { duration: 3 + i, repeat: Infinity, ease: 'linear' },
627
+ scale: { duration: 2, repeat: Infinity, delay: i * 0.3 },
628
+ }}
629
+ />
630
+ ))}
631
+ <div className="absolute inset-0 flex items-center justify-center">
632
+ <Brain className="w-12 h-12" style={{ color: primaryColor }} />
633
+ </div>
634
+ </div>
635
+
636
+ {/* Messages */}
637
+ <div className="text-center">
638
+ <motion.h2
639
+ className="text-xl font-semibold mb-2 flex items-center gap-2 justify-center"
640
+ style={{ color: textColor }}
641
+ >
642
+ <Sparkles className="w-5 h-5" style={{ color: primaryColor }} />
643
+ {message}
644
+ </motion.h2>
645
+ <p className="text-sm opacity-70" style={{ color: textColor }}>{subMessage}</p>
646
+ </div>
647
+
648
+ {/* Progress bar */}
649
+ <div className="w-64 h-2 bg-white/10 rounded-full overflow-hidden">
650
+ <motion.div
651
+ className="h-full rounded-full"
652
+ style={{ backgroundColor: primaryColor }}
653
+ initial={{ width: 0 }}
654
+ animate={{ width: \`\${progress}%\` }}
655
+ />
656
+ </div>
657
+ </div>
658
+ </motion.div>
659
+ )}
660
+ </AnimatePresence>
661
+ );
662
+ }`,
663
+ },
664
+ {
665
+ id: 'floating-login',
666
+ name: 'FloatingLogin',
667
+ description: 'Animated floating login form with smooth bobbing motion, supporting local and OAuth authentication.',
668
+ category: 'auth',
669
+ dependencies: ['framer-motion', 'lucide-react'],
670
+ props: [
671
+ { name: 'mode', type: "'light' | 'dark'", required: false, description: 'Theme mode', default: "'light'" },
672
+ { name: 'primaryColor', type: 'string', required: false, description: 'Accent color (hex)', default: "'#6366F1'" },
673
+ { name: 'floatingEnabled', type: 'boolean', required: false, description: 'Enable floating animation', default: 'true' },
674
+ { name: 'floatIntensity', type: 'number', required: false, description: 'Float intensity (1-10)', default: '5' },
675
+ { name: 'onLogin', type: '(data: LoginData) => void', required: false, description: 'Login callback with email/password' },
676
+ { name: 'onGoogleLogin', type: '() => void', required: false, description: 'Google OAuth callback' },
677
+ { name: 'onAppleLogin', type: '() => void', required: false, description: 'Apple Sign-In callback' },
678
+ ],
679
+ usage: `import { FloatingLogin } from '@/components/FloatingLogin';
680
+
681
+ function MyComponent() {
682
+ return (
683
+ <FloatingLogin
684
+ mode="dark"
685
+ primaryColor="#6366F1"
686
+ floatIntensity={5}
687
+ onLogin={(data) => console.log(data)}
688
+ onGoogleLogin={() => signInWithGoogle()}
689
+ onAppleLogin={() => signInWithApple()}
690
+ />
691
+ );
692
+ }`,
693
+ source: `'use client';
694
+
695
+ import { useState } from 'react';
696
+ import { motion } from 'framer-motion';
697
+ import { Mail, Lock, Eye, EyeOff } from 'lucide-react';
698
+
699
+ interface LoginData {
700
+ email: string;
701
+ password: string;
702
+ }
703
+
704
+ interface FloatingLoginProps {
705
+ mode?: 'light' | 'dark';
706
+ primaryColor?: string;
707
+ floatingEnabled?: boolean;
708
+ floatIntensity?: number;
709
+ onLogin?: (data: LoginData) => void | Promise<void>;
710
+ onGoogleLogin?: () => void | Promise<void>;
711
+ onAppleLogin?: () => void | Promise<void>;
712
+ }
713
+
714
+ export default function FloatingLogin({
715
+ mode = 'light',
716
+ primaryColor = '#6366F1',
717
+ floatingEnabled = true,
718
+ floatIntensity = 5,
719
+ onLogin,
720
+ onGoogleLogin,
721
+ onAppleLogin,
722
+ }: FloatingLoginProps) {
723
+ const [email, setEmail] = useState('');
724
+ const [password, setPassword] = useState('');
725
+ const [showPassword, setShowPassword] = useState(false);
726
+ const [isLoading, setIsLoading] = useState(false);
727
+
728
+ const isDark = mode === 'dark';
729
+ const intensity = Math.max(1, Math.min(10, floatIntensity));
730
+ const yOffset = intensity * 2;
731
+
732
+ const handleSubmit = async (e: React.FormEvent) => {
733
+ e.preventDefault();
734
+ if (!onLogin) return;
735
+ setIsLoading(true);
736
+ try {
737
+ await onLogin({ email, password });
738
+ } finally {
739
+ setIsLoading(false);
740
+ }
741
+ };
742
+
743
+ const cardBg = isDark ? 'bg-gray-900' : 'bg-white';
744
+ const textColor = isDark ? 'text-white' : 'text-gray-900';
745
+ const inputBg = isDark ? 'bg-gray-800 border-gray-700' : 'bg-gray-50 border-gray-200';
746
+
747
+ return (
748
+ <div className="flex items-center justify-center p-8">
749
+ <motion.div
750
+ animate={floatingEnabled ? {
751
+ y: [0, -yOffset, 0],
752
+ rotate: [0, 1, -1, 0],
753
+ } : {}}
754
+ transition={{
755
+ duration: 4,
756
+ repeat: Infinity,
757
+ ease: 'easeInOut',
758
+ }}
759
+ className={\`\${cardBg} rounded-2xl shadow-2xl p-8 w-full max-w-md\`}
760
+ >
761
+ <h2 className={\`text-2xl font-bold \${textColor} mb-6 text-center\`}>Welcome back</h2>
762
+
763
+ <form onSubmit={handleSubmit} className="space-y-4">
764
+ <div className="relative">
765
+ <Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
766
+ <input
767
+ type="email"
768
+ placeholder="Email"
769
+ value={email}
770
+ onChange={(e) => setEmail(e.target.value)}
771
+ className={\`w-full pl-10 pr-4 py-3 rounded-lg border \${inputBg} \${textColor} focus:outline-none focus:ring-2\`}
772
+ style={{ '--tw-ring-color': primaryColor } as any}
773
+ />
774
+ </div>
775
+
776
+ <div className="relative">
777
+ <Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
778
+ <input
779
+ type={showPassword ? 'text' : 'password'}
780
+ placeholder="Password"
781
+ value={password}
782
+ onChange={(e) => setPassword(e.target.value)}
783
+ className={\`w-full pl-10 pr-12 py-3 rounded-lg border \${inputBg} \${textColor} focus:outline-none focus:ring-2\`}
784
+ style={{ '--tw-ring-color': primaryColor } as any}
785
+ />
786
+ <button
787
+ type="button"
788
+ onClick={() => setShowPassword(!showPassword)}
789
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400"
790
+ >
791
+ {showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
792
+ </button>
793
+ </div>
794
+
795
+ <button
796
+ type="submit"
797
+ disabled={isLoading}
798
+ className="w-full py-3 rounded-lg text-white font-medium transition-opacity disabled:opacity-50"
799
+ style={{ backgroundColor: primaryColor }}
800
+ >
801
+ {isLoading ? 'Signing in...' : 'Sign in'}
802
+ </button>
803
+ </form>
804
+
805
+ <div className="mt-6 flex items-center gap-4">
806
+ <div className="flex-1 h-px bg-gray-300" />
807
+ <span className="text-sm text-gray-500">or</span>
808
+ <div className="flex-1 h-px bg-gray-300" />
809
+ </div>
810
+
811
+ <div className="mt-6 space-y-3">
812
+ {onGoogleLogin && (
813
+ <button
814
+ onClick={onGoogleLogin}
815
+ className={\`w-full py-3 rounded-lg border \${inputBg} \${textColor} font-medium flex items-center justify-center gap-2\`}
816
+ >
817
+ <svg className="w-5 h-5" viewBox="0 0 24 24">
818
+ <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
819
+ <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
820
+ <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
821
+ <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
822
+ </svg>
823
+ Continue with Google
824
+ </button>
825
+ )}
826
+
827
+ {onAppleLogin && (
828
+ <button
829
+ onClick={onAppleLogin}
830
+ className={\`w-full py-3 rounded-lg \${isDark ? 'bg-white text-black' : 'bg-black text-white'} font-medium flex items-center justify-center gap-2\`}
831
+ >
832
+ <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
833
+ <path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/>
834
+ </svg>
835
+ Continue with Apple
836
+ </button>
837
+ )}
838
+ </div>
839
+ </motion.div>
840
+ </div>
841
+ );
842
+ }`,
843
+ },
844
+ ];
845
+
846
+ export function getComponent(id: string): ComponentInfo | undefined {
847
+ return components.find(c => c.id === id || c.name.toLowerCase() === id.toLowerCase());
848
+ }
849
+
850
+ export function listComponents(category?: string): ComponentInfo[] {
851
+ if (!category || category === 'all') return components;
852
+ return components.filter(c => c.category === category);
853
+ }
854
+
855
+ export function getCategories(): string[] {
856
+ return ['all', ...new Set(components.map(c => c.category))];
857
+ }