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,1062 @@
1
+ #!/usr/bin/env node
2
+ #!/usr/bin/env node
3
+
4
+ // mcp/server.ts
5
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import {
8
+ CallToolRequestSchema,
9
+ ListToolsRequestSchema
10
+ } from "@modelcontextprotocol/sdk/types.js";
11
+
12
+ // mcp/registry.ts
13
+ var components = [
14
+ {
15
+ id: "loading-dots",
16
+ name: "LoadingDots",
17
+ description: "Simple, elegant bouncing dots animation. Perfect for minimal loading states and inline indicators.",
18
+ category: "loading",
19
+ dependencies: ["framer-motion"],
20
+ props: [
21
+ { name: "size", type: "'sm' | 'md' | 'lg'", required: false, description: "Size of the dots", default: "'md'" },
22
+ { name: "color", type: "string", required: false, description: "Color of the dots (hex or CSS color)", default: "'#6366F1'" }
23
+ ],
24
+ usage: `import { LoadingDots } from '@/components/LoadingDots';
25
+
26
+ function MyComponent() {
27
+ return <LoadingDots size="md" color="#6366F1" />;
28
+ }`,
29
+ source: `'use client';
30
+
31
+ import { motion } from 'framer-motion';
32
+
33
+ interface LoadingDotsProps {
34
+ size?: 'sm' | 'md' | 'lg';
35
+ color?: string;
36
+ }
37
+
38
+ const sizeMap = {
39
+ sm: { dot: 6, gap: 4 },
40
+ md: { dot: 10, gap: 6 },
41
+ lg: { dot: 14, gap: 8 },
42
+ };
43
+
44
+ export default function LoadingDots({ size = 'md', color = '#6366F1' }: LoadingDotsProps) {
45
+ const { dot, gap } = sizeMap[size];
46
+
47
+ return (
48
+ <div className="flex items-center justify-center" style={{ gap }}>
49
+ {[0, 1, 2].map((i) => (
50
+ <motion.div
51
+ key={i}
52
+ style={{
53
+ width: dot,
54
+ height: dot,
55
+ backgroundColor: color,
56
+ borderRadius: '50%',
57
+ }}
58
+ animate={{ y: [0, -dot, 0] }}
59
+ transition={{
60
+ duration: 0.6,
61
+ repeat: Infinity,
62
+ delay: i * 0.15,
63
+ ease: 'easeInOut',
64
+ }}
65
+ />
66
+ ))}
67
+ </div>
68
+ );
69
+ }`
70
+ },
71
+ {
72
+ id: "pulse-circle",
73
+ name: "PulseCircle",
74
+ description: "Circular progress indicator with expanding pulse rings and percentage display. Shows completion checkmark at 100%.",
75
+ category: "processing",
76
+ dependencies: ["framer-motion"],
77
+ props: [
78
+ { name: "isActive", type: "boolean", required: true, description: "Activates the animation" },
79
+ { name: "progress", type: "number", required: false, description: "External progress value (0-100)" },
80
+ { name: "onComplete", type: "() => void", required: false, description: "Callback when progress reaches 100%" }
81
+ ],
82
+ usage: `import { PulseCircle } from '@/components/PulseCircle';
83
+
84
+ function MyComponent() {
85
+ const [isActive, setIsActive] = useState(false);
86
+
87
+ return (
88
+ <PulseCircle
89
+ isActive={isActive}
90
+ onComplete={() => setIsActive(false)}
91
+ />
92
+ );
93
+ }`,
94
+ source: `'use client';
95
+
96
+ import { useState, useEffect } from 'react';
97
+ import { motion, AnimatePresence } from 'framer-motion';
98
+
99
+ interface PulseCircleProps {
100
+ isActive: boolean;
101
+ progress?: number;
102
+ onComplete?: () => void;
103
+ }
104
+
105
+ export default function PulseCircle({ isActive, progress: externalProgress, onComplete }: PulseCircleProps) {
106
+ const [internalProgress, setInternalProgress] = useState(0);
107
+ const progress = externalProgress ?? internalProgress;
108
+
109
+ useEffect(() => {
110
+ if (!isActive || externalProgress !== undefined) {
111
+ if (!isActive) setInternalProgress(0);
112
+ return;
113
+ }
114
+
115
+ const interval = setInterval(() => {
116
+ setInternalProgress((prev) => {
117
+ if (prev >= 100) {
118
+ clearInterval(interval);
119
+ onComplete?.();
120
+ return 100;
121
+ }
122
+ return prev + Math.random() * 3 + 1;
123
+ });
124
+ }, 100);
125
+
126
+ return () => clearInterval(interval);
127
+ }, [isActive, externalProgress, onComplete]);
128
+
129
+ const isComplete = progress >= 100;
130
+ const radius = 45;
131
+ const circumference = 2 * Math.PI * radius;
132
+ const strokeDashoffset = circumference - (Math.min(progress, 100) / 100) * circumference;
133
+
134
+ return (
135
+ <div className="relative w-32 h-32">
136
+ {/* Pulse rings */}
137
+ <AnimatePresence>
138
+ {isActive && !isComplete && [0, 1, 2].map((i) => (
139
+ <motion.div
140
+ key={i}
141
+ className="absolute inset-0 rounded-full border-2 border-blue-400"
142
+ initial={{ scale: 1, opacity: 0.6 }}
143
+ animate={{ scale: 2, opacity: 0 }}
144
+ transition={{
145
+ duration: 2,
146
+ repeat: Infinity,
147
+ delay: i * 0.6,
148
+ ease: 'easeOut',
149
+ }}
150
+ />
151
+ ))}
152
+ </AnimatePresence>
153
+
154
+ {/* Progress circle */}
155
+ <svg className="w-full h-full -rotate-90" viewBox="0 0 100 100">
156
+ <circle cx="50" cy="50" r={radius} fill="none" stroke="#e5e7eb" strokeWidth="6" />
157
+ <motion.circle
158
+ cx="50"
159
+ cy="50"
160
+ r={radius}
161
+ fill="none"
162
+ stroke={isComplete ? '#10b981' : '#3b82f6'}
163
+ strokeWidth="6"
164
+ strokeLinecap="round"
165
+ strokeDasharray={circumference}
166
+ initial={{ strokeDashoffset: circumference }}
167
+ animate={{ strokeDashoffset }}
168
+ transition={{ duration: 0.3 }}
169
+ />
170
+ </svg>
171
+
172
+ {/* Center content */}
173
+ <div className="absolute inset-0 flex items-center justify-center">
174
+ <AnimatePresence mode="wait">
175
+ {isComplete ? (
176
+ <motion.svg
177
+ key="check"
178
+ initial={{ scale: 0 }}
179
+ animate={{ scale: 1 }}
180
+ className="w-12 h-12 text-green-500"
181
+ fill="none"
182
+ viewBox="0 0 24 24"
183
+ stroke="currentColor"
184
+ >
185
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
186
+ </motion.svg>
187
+ ) : (
188
+ <motion.span
189
+ key="progress"
190
+ className="text-2xl font-bold text-gray-700"
191
+ >
192
+ {Math.round(progress)}%
193
+ </motion.span>
194
+ )}
195
+ </AnimatePresence>
196
+ </div>
197
+ </div>
198
+ );
199
+ }`
200
+ },
201
+ {
202
+ id: "code-typing",
203
+ name: "CodeTyping",
204
+ description: "Realistic code typing effect with syntax highlighting and terminal-style presentation.",
205
+ category: "creative",
206
+ dependencies: ["framer-motion"],
207
+ props: [
208
+ { name: "isTyping", type: "boolean", required: true, description: "Controls the typing animation" },
209
+ { name: "onComplete", type: "() => void", required: false, description: "Callback when typing completes" }
210
+ ],
211
+ usage: `import { CodeTyping } from '@/components/CodeTyping';
212
+
213
+ function MyComponent() {
214
+ const [isTyping, setIsTyping] = useState(false);
215
+
216
+ return (
217
+ <CodeTyping
218
+ isTyping={isTyping}
219
+ onComplete={() => setIsTyping(false)}
220
+ />
221
+ );
222
+ }`,
223
+ source: `'use client';
224
+
225
+ import { useState, useEffect } from 'react';
226
+ import { motion } from 'framer-motion';
227
+
228
+ interface CodeTypingProps {
229
+ isTyping: boolean;
230
+ onComplete?: () => void;
231
+ }
232
+
233
+ const codeLines = [
234
+ { text: 'const', color: '#c678dd' },
235
+ { text: ' ai', color: '#e06c75' },
236
+ { text: ' = ', color: '#abb2bf' },
237
+ { text: 'await', color: '#c678dd' },
238
+ { text: ' generateResponse', color: '#61afef' },
239
+ { text: '(', color: '#abb2bf' },
240
+ { text: 'prompt', color: '#e5c07b' },
241
+ { text: ');', color: '#abb2bf' },
242
+ ];
243
+
244
+ export default function CodeTyping({ isTyping, onComplete }: CodeTypingProps) {
245
+ const [displayedCode, setDisplayedCode] = useState<typeof codeLines>([]);
246
+ const [currentIndex, setCurrentIndex] = useState(0);
247
+
248
+ useEffect(() => {
249
+ if (!isTyping) {
250
+ setDisplayedCode([]);
251
+ setCurrentIndex(0);
252
+ return;
253
+ }
254
+
255
+ if (currentIndex >= codeLines.length) {
256
+ onComplete?.();
257
+ return;
258
+ }
259
+
260
+ const timeout = setTimeout(() => {
261
+ setDisplayedCode((prev) => [...prev, codeLines[currentIndex]]);
262
+ setCurrentIndex((prev) => prev + 1);
263
+ }, 150);
264
+
265
+ return () => clearTimeout(timeout);
266
+ }, [isTyping, currentIndex, onComplete]);
267
+
268
+ return (
269
+ <div className="bg-[#282c34] rounded-lg p-4 font-mono text-sm min-w-[300px]">
270
+ <div className="flex gap-2 mb-3">
271
+ <div className="w-3 h-3 rounded-full bg-red-500" />
272
+ <div className="w-3 h-3 rounded-full bg-yellow-500" />
273
+ <div className="w-3 h-3 rounded-full bg-green-500" />
274
+ </div>
275
+ <div className="flex items-center">
276
+ <span className="text-gray-500 mr-2">1</span>
277
+ {displayedCode.map((token, i) => (
278
+ <span key={i} style={{ color: token.color }}>{token.text}</span>
279
+ ))}
280
+ {isTyping && currentIndex < codeLines.length && (
281
+ <motion.span
282
+ className="w-2 h-5 bg-white ml-0.5"
283
+ animate={{ opacity: [1, 0] }}
284
+ transition={{ duration: 0.5, repeat: Infinity }}
285
+ />
286
+ )}
287
+ </div>
288
+ </div>
289
+ );
290
+ }`
291
+ },
292
+ {
293
+ id: "data-processing",
294
+ name: "DataProcessing",
295
+ description: "Data pipeline visualization showing input, processing, and output stages with flowing data cards.",
296
+ category: "processing",
297
+ dependencies: ["framer-motion", "lucide-react"],
298
+ props: [
299
+ { name: "isProcessing", type: "boolean", required: true, description: "Controls the processing animation" },
300
+ { name: "onComplete", type: "() => void", required: false, description: "Callback when processing completes" }
301
+ ],
302
+ usage: `import { DataProcessing } from '@/components/DataProcessing';
303
+
304
+ function MyComponent() {
305
+ const [isProcessing, setIsProcessing] = useState(false);
306
+
307
+ return (
308
+ <DataProcessing
309
+ isProcessing={isProcessing}
310
+ onComplete={() => setIsProcessing(false)}
311
+ />
312
+ );
313
+ }`,
314
+ source: `'use client';
315
+
316
+ import { useState, useEffect } from 'react';
317
+ import { motion, AnimatePresence } from 'framer-motion';
318
+ import { Database, Cpu, CheckCircle } from 'lucide-react';
319
+
320
+ interface DataProcessingProps {
321
+ isProcessing: boolean;
322
+ onComplete?: () => void;
323
+ }
324
+
325
+ export default function DataProcessing({ isProcessing, onComplete }: DataProcessingProps) {
326
+ const [stage, setStage] = useState<'idle' | 'input' | 'process' | 'output' | 'complete'>('idle');
327
+ const [progress, setProgress] = useState(0);
328
+
329
+ useEffect(() => {
330
+ if (!isProcessing) {
331
+ setStage('idle');
332
+ setProgress(0);
333
+ return;
334
+ }
335
+
336
+ setStage('input');
337
+ const stages = [
338
+ { stage: 'process' as const, delay: 800 },
339
+ { stage: 'output' as const, delay: 1700 },
340
+ { stage: 'complete' as const, delay: 2500 },
341
+ ];
342
+
343
+ const timeouts = stages.map(({ stage, delay }) =>
344
+ setTimeout(() => setStage(stage), delay)
345
+ );
346
+
347
+ const progressInterval = setInterval(() => {
348
+ setProgress((p) => Math.min(p + 4, 100));
349
+ }, 100);
350
+
351
+ const completeTimeout = setTimeout(() => {
352
+ onComplete?.();
353
+ }, 3000);
354
+
355
+ return () => {
356
+ timeouts.forEach(clearTimeout);
357
+ clearTimeout(completeTimeout);
358
+ clearInterval(progressInterval);
359
+ };
360
+ }, [isProcessing, onComplete]);
361
+
362
+ const stages = [
363
+ { id: 'input', icon: Database, label: 'Input', active: ['input', 'process', 'output', 'complete'].includes(stage) },
364
+ { id: 'process', icon: Cpu, label: 'Processing', active: ['process', 'output', 'complete'].includes(stage) },
365
+ { id: 'output', icon: CheckCircle, label: 'Output', active: ['output', 'complete'].includes(stage) },
366
+ ];
367
+
368
+ return (
369
+ <div className="flex flex-col items-center gap-6 p-6">
370
+ <div className="flex items-center gap-4">
371
+ {stages.map((s, i) => (
372
+ <div key={s.id} className="flex items-center">
373
+ <motion.div
374
+ className={\`w-16 h-16 rounded-xl flex items-center justify-center \${
375
+ s.active ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-400'
376
+ }\`}
377
+ animate={{ scale: stage === s.id ? [1, 1.1, 1] : 1 }}
378
+ transition={{ duration: 0.3 }}
379
+ >
380
+ <s.icon className="w-8 h-8" />
381
+ </motion.div>
382
+ {i < stages.length - 1 && (
383
+ <div className="w-12 h-1 mx-2 bg-gray-200 rounded overflow-hidden">
384
+ <motion.div
385
+ className="h-full bg-blue-500"
386
+ initial={{ width: 0 }}
387
+ animate={{ width: s.active ? '100%' : 0 }}
388
+ transition={{ duration: 0.5 }}
389
+ />
390
+ </div>
391
+ )}
392
+ </div>
393
+ ))}
394
+ </div>
395
+ <div className="text-center">
396
+ <p className="text-lg font-medium text-gray-700 capitalize">{stage === 'idle' ? 'Ready' : stage}</p>
397
+ <p className="text-sm text-gray-500">{progress}% complete</p>
398
+ </div>
399
+ </div>
400
+ );
401
+ }`
402
+ },
403
+ {
404
+ id: "ai-creating",
405
+ name: "AiCreating",
406
+ description: "Multi-stage animation visualizing AI creation process with thinking, writing, building, and completion phases.",
407
+ category: "creative",
408
+ dependencies: ["framer-motion"],
409
+ props: [
410
+ { name: "isLoading", type: "boolean", required: true, description: "Controls the animation state" },
411
+ { name: "size", type: "'sm' | 'md' | 'lg'", required: false, description: "Size of the animation", default: "'md'" },
412
+ { name: "onComplete", type: "() => void", required: false, description: "Callback when animation completes" }
413
+ ],
414
+ usage: `import { AiCreating } from '@/components/AiCreating';
415
+
416
+ function MyComponent() {
417
+ const [isLoading, setIsLoading] = useState(false);
418
+
419
+ return (
420
+ <AiCreating
421
+ isLoading={isLoading}
422
+ size="md"
423
+ onComplete={() => setIsLoading(false)}
424
+ />
425
+ );
426
+ }`,
427
+ source: `'use client';
428
+
429
+ import { useState, useEffect } from 'react';
430
+ import { motion, AnimatePresence } from 'framer-motion';
431
+ import './AiCreating.css';
432
+
433
+ interface AiCreatingProps {
434
+ isLoading: boolean;
435
+ size?: 'sm' | 'md' | 'lg';
436
+ onComplete?: () => void;
437
+ }
438
+
439
+ const stages = ['thinking', 'writing', 'building', 'complete'] as const;
440
+ type Stage = typeof stages[number];
441
+
442
+ const stageMessages: Record<Stage, string> = {
443
+ thinking: 'Thinking...',
444
+ writing: 'Writing...',
445
+ building: 'Building...',
446
+ complete: 'Done!',
447
+ };
448
+
449
+ export default function AiCreating({ isLoading, size = 'md', onComplete }: AiCreatingProps) {
450
+ const [stage, setStage] = useState<Stage>('thinking');
451
+
452
+ const sizeClasses = {
453
+ sm: 'w-16 h-16',
454
+ md: 'w-24 h-24',
455
+ lg: 'w-32 h-32',
456
+ };
457
+
458
+ useEffect(() => {
459
+ if (!isLoading) {
460
+ setStage('thinking');
461
+ return;
462
+ }
463
+
464
+ const timings = [2000, 4000, 6000];
465
+ const timeouts = timings.map((delay, i) =>
466
+ setTimeout(() => setStage(stages[i + 1]), delay)
467
+ );
468
+
469
+ const completeTimeout = setTimeout(() => onComplete?.(), 7000);
470
+
471
+ return () => {
472
+ timeouts.forEach(clearTimeout);
473
+ clearTimeout(completeTimeout);
474
+ };
475
+ }, [isLoading, onComplete]);
476
+
477
+ if (!isLoading) return null;
478
+
479
+ return (
480
+ <div className="flex flex-col items-center gap-4">
481
+ <div className={\`relative \${sizeClasses[size]}\`}>
482
+ <motion.div
483
+ className="ai-robot w-full h-full"
484
+ animate={{
485
+ y: stage === 'thinking' ? [0, -5, 0] : 0,
486
+ rotate: stage === 'building' ? [0, 5, -5, 0] : 0,
487
+ }}
488
+ transition={{ duration: 1, repeat: stage !== 'complete' ? Infinity : 0 }}
489
+ >
490
+ {/* Robot SVG */}
491
+ <svg viewBox="0 0 100 100" className="w-full h-full">
492
+ <rect x="25" y="30" width="50" height="45" rx="8" fill="#6366f1" />
493
+ <rect x="35" y="45" width="10" height="10" rx="2" fill="white" />
494
+ <rect x="55" y="45" width="10" height="10" rx="2" fill="white" />
495
+ <rect x="40" y="62" width="20" height="5" rx="2" fill="white" />
496
+ <rect x="42" y="20" width="16" height="15" rx="3" fill="#6366f1" />
497
+ <circle cx="50" cy="12" r="4" fill="#fbbf24" />
498
+ </svg>
499
+ </motion.div>
500
+ </div>
501
+ <motion.p
502
+ key={stage}
503
+ initial={{ opacity: 0, y: 10 }}
504
+ animate={{ opacity: 1, y: 0 }}
505
+ className="text-gray-600 font-medium"
506
+ >
507
+ {stageMessages[stage]}
508
+ </motion.p>
509
+ </div>
510
+ );
511
+ }`
512
+ },
513
+ {
514
+ id: "ai-creating-2",
515
+ name: "AiCreating2",
516
+ description: "Full-screen AI brain animation with rotating rings, floating particles, and dynamic status messages.",
517
+ category: "creative",
518
+ dependencies: ["framer-motion", "lucide-react"],
519
+ props: [
520
+ { name: "isLoading", type: "boolean", required: true, description: "Controls animation visibility" },
521
+ { name: "message", type: "string", required: false, description: "Main loading message" },
522
+ { name: "subMessage", type: "string", required: false, description: "Secondary description text" },
523
+ { name: "primaryColor", type: "string", required: false, description: "Primary color (hex)", default: "'#6366F1'" },
524
+ { name: "backgroundColor", type: "string", required: false, description: "Background color (hex)", default: "'#0f172a'" },
525
+ { name: "textColor", type: "string", required: false, description: "Text color (hex)", default: "'#ffffff'" },
526
+ { name: "contained", type: "boolean", required: false, description: "Render in container vs full overlay", default: "false" },
527
+ { name: "onComplete", type: "() => void", required: false, description: "Callback when complete" }
528
+ ],
529
+ usage: `import { AiCreating2 } from '@/components/AiCreating2';
530
+
531
+ function MyComponent() {
532
+ const [isLoading, setIsLoading] = useState(false);
533
+
534
+ return (
535
+ <AiCreating2
536
+ isLoading={isLoading}
537
+ message="Creating your plan..."
538
+ primaryColor="#6366F1"
539
+ onComplete={() => setIsLoading(false)}
540
+ />
541
+ );
542
+ }`,
543
+ source: `'use client';
544
+
545
+ import { useEffect, useState } from 'react';
546
+ import { motion, AnimatePresence } from 'framer-motion';
547
+ import { Brain, Sparkles } from 'lucide-react';
548
+
549
+ interface AiCreating2Props {
550
+ isLoading: boolean;
551
+ message?: string;
552
+ subMessage?: string;
553
+ primaryColor?: string;
554
+ backgroundColor?: string;
555
+ textColor?: string;
556
+ contained?: boolean;
557
+ onComplete?: () => void;
558
+ }
559
+
560
+ export default function AiCreating2({
561
+ isLoading,
562
+ message = 'AI is creating...',
563
+ subMessage = 'This may take a moment',
564
+ primaryColor = '#6366F1',
565
+ backgroundColor = '#0f172a',
566
+ textColor = '#ffffff',
567
+ contained = false,
568
+ onComplete,
569
+ }: AiCreating2Props) {
570
+ const [progress, setProgress] = useState(0);
571
+
572
+ useEffect(() => {
573
+ if (!isLoading) {
574
+ setProgress(0);
575
+ return;
576
+ }
577
+
578
+ const interval = setInterval(() => {
579
+ setProgress((p) => {
580
+ if (p >= 100) {
581
+ clearInterval(interval);
582
+ setTimeout(() => onComplete?.(), 500);
583
+ return 100;
584
+ }
585
+ return p + Math.random() * 2 + 0.5;
586
+ });
587
+ }, 100);
588
+
589
+ return () => clearInterval(interval);
590
+ }, [isLoading, onComplete]);
591
+
592
+ const containerClass = contained
593
+ ? 'w-full h-full flex items-center justify-center'
594
+ : 'fixed inset-0 z-50 flex items-center justify-center';
595
+
596
+ return (
597
+ <AnimatePresence>
598
+ {isLoading && (
599
+ <motion.div
600
+ initial={{ opacity: 0 }}
601
+ animate={{ opacity: 1 }}
602
+ exit={{ opacity: 0 }}
603
+ className={containerClass}
604
+ style={{ backgroundColor }}
605
+ >
606
+ <div className="flex flex-col items-center gap-8">
607
+ {/* Brain with rings */}
608
+ <div className="relative w-32 h-32">
609
+ {[0, 1, 2].map((i) => (
610
+ <motion.div
611
+ key={i}
612
+ className="absolute inset-0 rounded-full border-2"
613
+ style={{ borderColor: primaryColor }}
614
+ animate={{ rotate: 360, scale: [1, 1.1, 1] }}
615
+ transition={{
616
+ rotate: { duration: 3 + i, repeat: Infinity, ease: 'linear' },
617
+ scale: { duration: 2, repeat: Infinity, delay: i * 0.3 },
618
+ }}
619
+ />
620
+ ))}
621
+ <div className="absolute inset-0 flex items-center justify-center">
622
+ <Brain className="w-12 h-12" style={{ color: primaryColor }} />
623
+ </div>
624
+ </div>
625
+
626
+ {/* Messages */}
627
+ <div className="text-center">
628
+ <motion.h2
629
+ className="text-xl font-semibold mb-2 flex items-center gap-2 justify-center"
630
+ style={{ color: textColor }}
631
+ >
632
+ <Sparkles className="w-5 h-5" style={{ color: primaryColor }} />
633
+ {message}
634
+ </motion.h2>
635
+ <p className="text-sm opacity-70" style={{ color: textColor }}>{subMessage}</p>
636
+ </div>
637
+
638
+ {/* Progress bar */}
639
+ <div className="w-64 h-2 bg-white/10 rounded-full overflow-hidden">
640
+ <motion.div
641
+ className="h-full rounded-full"
642
+ style={{ backgroundColor: primaryColor }}
643
+ initial={{ width: 0 }}
644
+ animate={{ width: \`\${progress}%\` }}
645
+ />
646
+ </div>
647
+ </div>
648
+ </motion.div>
649
+ )}
650
+ </AnimatePresence>
651
+ );
652
+ }`
653
+ },
654
+ {
655
+ id: "floating-login",
656
+ name: "FloatingLogin",
657
+ description: "Animated floating login form with smooth bobbing motion, supporting local and OAuth authentication.",
658
+ category: "auth",
659
+ dependencies: ["framer-motion", "lucide-react"],
660
+ props: [
661
+ { name: "mode", type: "'light' | 'dark'", required: false, description: "Theme mode", default: "'light'" },
662
+ { name: "primaryColor", type: "string", required: false, description: "Accent color (hex)", default: "'#6366F1'" },
663
+ { name: "floatingEnabled", type: "boolean", required: false, description: "Enable floating animation", default: "true" },
664
+ { name: "floatIntensity", type: "number", required: false, description: "Float intensity (1-10)", default: "5" },
665
+ { name: "onLogin", type: "(data: LoginData) => void", required: false, description: "Login callback with email/password" },
666
+ { name: "onGoogleLogin", type: "() => void", required: false, description: "Google OAuth callback" },
667
+ { name: "onAppleLogin", type: "() => void", required: false, description: "Apple Sign-In callback" }
668
+ ],
669
+ usage: `import { FloatingLogin } from '@/components/FloatingLogin';
670
+
671
+ function MyComponent() {
672
+ return (
673
+ <FloatingLogin
674
+ mode="dark"
675
+ primaryColor="#6366F1"
676
+ floatIntensity={5}
677
+ onLogin={(data) => console.log(data)}
678
+ onGoogleLogin={() => signInWithGoogle()}
679
+ onAppleLogin={() => signInWithApple()}
680
+ />
681
+ );
682
+ }`,
683
+ source: `'use client';
684
+
685
+ import { useState } from 'react';
686
+ import { motion } from 'framer-motion';
687
+ import { Mail, Lock, Eye, EyeOff } from 'lucide-react';
688
+
689
+ interface LoginData {
690
+ email: string;
691
+ password: string;
692
+ }
693
+
694
+ interface FloatingLoginProps {
695
+ mode?: 'light' | 'dark';
696
+ primaryColor?: string;
697
+ floatingEnabled?: boolean;
698
+ floatIntensity?: number;
699
+ onLogin?: (data: LoginData) => void | Promise<void>;
700
+ onGoogleLogin?: () => void | Promise<void>;
701
+ onAppleLogin?: () => void | Promise<void>;
702
+ }
703
+
704
+ export default function FloatingLogin({
705
+ mode = 'light',
706
+ primaryColor = '#6366F1',
707
+ floatingEnabled = true,
708
+ floatIntensity = 5,
709
+ onLogin,
710
+ onGoogleLogin,
711
+ onAppleLogin,
712
+ }: FloatingLoginProps) {
713
+ const [email, setEmail] = useState('');
714
+ const [password, setPassword] = useState('');
715
+ const [showPassword, setShowPassword] = useState(false);
716
+ const [isLoading, setIsLoading] = useState(false);
717
+
718
+ const isDark = mode === 'dark';
719
+ const intensity = Math.max(1, Math.min(10, floatIntensity));
720
+ const yOffset = intensity * 2;
721
+
722
+ const handleSubmit = async (e: React.FormEvent) => {
723
+ e.preventDefault();
724
+ if (!onLogin) return;
725
+ setIsLoading(true);
726
+ try {
727
+ await onLogin({ email, password });
728
+ } finally {
729
+ setIsLoading(false);
730
+ }
731
+ };
732
+
733
+ const cardBg = isDark ? 'bg-gray-900' : 'bg-white';
734
+ const textColor = isDark ? 'text-white' : 'text-gray-900';
735
+ const inputBg = isDark ? 'bg-gray-800 border-gray-700' : 'bg-gray-50 border-gray-200';
736
+
737
+ return (
738
+ <div className="flex items-center justify-center p-8">
739
+ <motion.div
740
+ animate={floatingEnabled ? {
741
+ y: [0, -yOffset, 0],
742
+ rotate: [0, 1, -1, 0],
743
+ } : {}}
744
+ transition={{
745
+ duration: 4,
746
+ repeat: Infinity,
747
+ ease: 'easeInOut',
748
+ }}
749
+ className={\`\${cardBg} rounded-2xl shadow-2xl p-8 w-full max-w-md\`}
750
+ >
751
+ <h2 className={\`text-2xl font-bold \${textColor} mb-6 text-center\`}>Welcome back</h2>
752
+
753
+ <form onSubmit={handleSubmit} className="space-y-4">
754
+ <div className="relative">
755
+ <Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
756
+ <input
757
+ type="email"
758
+ placeholder="Email"
759
+ value={email}
760
+ onChange={(e) => setEmail(e.target.value)}
761
+ className={\`w-full pl-10 pr-4 py-3 rounded-lg border \${inputBg} \${textColor} focus:outline-none focus:ring-2\`}
762
+ style={{ '--tw-ring-color': primaryColor } as any}
763
+ />
764
+ </div>
765
+
766
+ <div className="relative">
767
+ <Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
768
+ <input
769
+ type={showPassword ? 'text' : 'password'}
770
+ placeholder="Password"
771
+ value={password}
772
+ onChange={(e) => setPassword(e.target.value)}
773
+ className={\`w-full pl-10 pr-12 py-3 rounded-lg border \${inputBg} \${textColor} focus:outline-none focus:ring-2\`}
774
+ style={{ '--tw-ring-color': primaryColor } as any}
775
+ />
776
+ <button
777
+ type="button"
778
+ onClick={() => setShowPassword(!showPassword)}
779
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400"
780
+ >
781
+ {showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
782
+ </button>
783
+ </div>
784
+
785
+ <button
786
+ type="submit"
787
+ disabled={isLoading}
788
+ className="w-full py-3 rounded-lg text-white font-medium transition-opacity disabled:opacity-50"
789
+ style={{ backgroundColor: primaryColor }}
790
+ >
791
+ {isLoading ? 'Signing in...' : 'Sign in'}
792
+ </button>
793
+ </form>
794
+
795
+ <div className="mt-6 flex items-center gap-4">
796
+ <div className="flex-1 h-px bg-gray-300" />
797
+ <span className="text-sm text-gray-500">or</span>
798
+ <div className="flex-1 h-px bg-gray-300" />
799
+ </div>
800
+
801
+ <div className="mt-6 space-y-3">
802
+ {onGoogleLogin && (
803
+ <button
804
+ onClick={onGoogleLogin}
805
+ className={\`w-full py-3 rounded-lg border \${inputBg} \${textColor} font-medium flex items-center justify-center gap-2\`}
806
+ >
807
+ <svg className="w-5 h-5" viewBox="0 0 24 24">
808
+ <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"/>
809
+ <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"/>
810
+ <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"/>
811
+ <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"/>
812
+ </svg>
813
+ Continue with Google
814
+ </button>
815
+ )}
816
+
817
+ {onAppleLogin && (
818
+ <button
819
+ onClick={onAppleLogin}
820
+ 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\`}
821
+ >
822
+ <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
823
+ <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"/>
824
+ </svg>
825
+ Continue with Apple
826
+ </button>
827
+ )}
828
+ </div>
829
+ </motion.div>
830
+ </div>
831
+ );
832
+ }`
833
+ }
834
+ ];
835
+ function getComponent(id) {
836
+ return components.find((c) => c.id === id || c.name.toLowerCase() === id.toLowerCase());
837
+ }
838
+ function listComponents(category) {
839
+ if (!category || category === "all") return components;
840
+ return components.filter((c) => c.category === category);
841
+ }
842
+ function getCategories() {
843
+ return ["all", ...new Set(components.map((c) => c.category))];
844
+ }
845
+
846
+ // mcp/server.ts
847
+ import * as path from "path";
848
+ var server = new Server(
849
+ {
850
+ name: "ai-react-animations",
851
+ version: "1.0.0"
852
+ },
853
+ {
854
+ capabilities: {
855
+ tools: {}
856
+ }
857
+ }
858
+ );
859
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
860
+ return {
861
+ tools: [
862
+ {
863
+ name: "list_components",
864
+ description: "List all available AI animation components. Optionally filter by category (loading, processing, creative, auth).",
865
+ inputSchema: {
866
+ type: "object",
867
+ properties: {
868
+ category: {
869
+ type: "string",
870
+ description: "Filter by category: loading, processing, creative, auth, or all",
871
+ enum: ["all", "loading", "processing", "creative", "auth"]
872
+ }
873
+ }
874
+ }
875
+ },
876
+ {
877
+ name: "get_component",
878
+ description: "Get detailed information about a specific component including props, usage example, and source code.",
879
+ inputSchema: {
880
+ type: "object",
881
+ properties: {
882
+ name: {
883
+ type: "string",
884
+ description: 'Component name or ID (e.g., "LoadingDots", "loading-dots", "PulseCircle")'
885
+ }
886
+ },
887
+ required: ["name"]
888
+ }
889
+ },
890
+ {
891
+ name: "add_component",
892
+ description: "Add an animation component to the user's project. Creates the component file in the specified directory.",
893
+ inputSchema: {
894
+ type: "object",
895
+ properties: {
896
+ name: {
897
+ type: "string",
898
+ description: "Component name or ID to add"
899
+ },
900
+ directory: {
901
+ type: "string",
902
+ description: "Directory path where to create the component (default: ./components)"
903
+ }
904
+ },
905
+ required: ["name"]
906
+ }
907
+ },
908
+ {
909
+ name: "get_install_command",
910
+ description: "Get the npm install command for required dependencies of a component.",
911
+ inputSchema: {
912
+ type: "object",
913
+ properties: {
914
+ name: {
915
+ type: "string",
916
+ description: "Component name or ID"
917
+ }
918
+ },
919
+ required: ["name"]
920
+ }
921
+ }
922
+ ]
923
+ };
924
+ });
925
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
926
+ const { name, arguments: args } = request.params;
927
+ switch (name) {
928
+ case "list_components": {
929
+ const category = args?.category || "all";
930
+ const filtered = listComponents(category);
931
+ const summary = filtered.map((c) => ({
932
+ name: c.name,
933
+ id: c.id,
934
+ category: c.category,
935
+ description: c.description
936
+ }));
937
+ return {
938
+ content: [
939
+ {
940
+ type: "text",
941
+ text: JSON.stringify({
942
+ total: filtered.length,
943
+ categories: getCategories(),
944
+ components: summary
945
+ }, null, 2)
946
+ }
947
+ ]
948
+ };
949
+ }
950
+ case "get_component": {
951
+ const componentName = args.name;
952
+ const component = getComponent(componentName);
953
+ if (!component) {
954
+ return {
955
+ content: [
956
+ {
957
+ type: "text",
958
+ text: `Component "${componentName}" not found. Use list_components to see available components.`
959
+ }
960
+ ],
961
+ isError: true
962
+ };
963
+ }
964
+ return {
965
+ content: [
966
+ {
967
+ type: "text",
968
+ text: JSON.stringify({
969
+ name: component.name,
970
+ id: component.id,
971
+ description: component.description,
972
+ category: component.category,
973
+ dependencies: component.dependencies,
974
+ props: component.props,
975
+ usage: component.usage,
976
+ source: component.source
977
+ }, null, 2)
978
+ }
979
+ ]
980
+ };
981
+ }
982
+ case "add_component": {
983
+ const componentName = args.name;
984
+ const directory = args.directory || "./components";
985
+ const component = getComponent(componentName);
986
+ if (!component) {
987
+ return {
988
+ content: [
989
+ {
990
+ type: "text",
991
+ text: `Component "${componentName}" not found. Use list_components to see available components.`
992
+ }
993
+ ],
994
+ isError: true
995
+ };
996
+ }
997
+ const filePath = path.join(directory, `${component.name}.tsx`);
998
+ return {
999
+ content: [
1000
+ {
1001
+ type: "text",
1002
+ text: JSON.stringify({
1003
+ success: true,
1004
+ component: component.name,
1005
+ filePath,
1006
+ source: component.source,
1007
+ dependencies: component.dependencies,
1008
+ installCommand: `npm install ${component.dependencies.join(" ")}`,
1009
+ usage: component.usage,
1010
+ instructions: `
1011
+ 1. Create the file at: ${filePath}
1012
+ 2. Install dependencies: npm install ${component.dependencies.join(" ")}
1013
+ 3. Import and use the component as shown in the usage example.
1014
+
1015
+ Note: Make sure you have 'use client' directive if using Next.js App Router.
1016
+ `.trim()
1017
+ }, null, 2)
1018
+ }
1019
+ ]
1020
+ };
1021
+ }
1022
+ case "get_install_command": {
1023
+ const componentName = args.name;
1024
+ const component = getComponent(componentName);
1025
+ if (!component) {
1026
+ return {
1027
+ content: [
1028
+ {
1029
+ type: "text",
1030
+ text: `Component "${componentName}" not found.`
1031
+ }
1032
+ ],
1033
+ isError: true
1034
+ };
1035
+ }
1036
+ return {
1037
+ content: [
1038
+ {
1039
+ type: "text",
1040
+ text: `npm install ${component.dependencies.join(" ")}`
1041
+ }
1042
+ ]
1043
+ };
1044
+ }
1045
+ default:
1046
+ return {
1047
+ content: [
1048
+ {
1049
+ type: "text",
1050
+ text: `Unknown tool: ${name}`
1051
+ }
1052
+ ],
1053
+ isError: true
1054
+ };
1055
+ }
1056
+ });
1057
+ async function main() {
1058
+ const transport = new StdioServerTransport();
1059
+ await server.connect(transport);
1060
+ console.error("AI React Animations MCP Server running on stdio");
1061
+ }
1062
+ main().catch(console.error);