@m13v/seo-components 0.1.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 (42) hide show
  1. package/package.json +52 -0
  2. package/src/components/AnimatedBeam.tsx +273 -0
  3. package/src/components/AnimatedChecklist.tsx +68 -0
  4. package/src/components/AnimatedCodeBlock.tsx +68 -0
  5. package/src/components/AnimatedDemo.tsx +186 -0
  6. package/src/components/AnimatedMetric.tsx +54 -0
  7. package/src/components/AnimatedSection.tsx +31 -0
  8. package/src/components/ArticleMeta.tsx +51 -0
  9. package/src/components/BackgroundGrid.tsx +59 -0
  10. package/src/components/BeforeAfter.tsx +121 -0
  11. package/src/components/BentoGrid.tsx +67 -0
  12. package/src/components/Breadcrumbs.tsx +40 -0
  13. package/src/components/CodeComparison.tsx +115 -0
  14. package/src/components/ComparisonTable.tsx +66 -0
  15. package/src/components/FaqSection.tsx +40 -0
  16. package/src/components/FlowDiagram.tsx +86 -0
  17. package/src/components/GlowCard.tsx +58 -0
  18. package/src/components/GradientText.tsx +43 -0
  19. package/src/components/InlineCta.tsx +50 -0
  20. package/src/components/InlineTestimonial.tsx +53 -0
  21. package/src/components/LottiePlayer.tsx +63 -0
  22. package/src/components/Marquee.tsx +67 -0
  23. package/src/components/MetricsRow.tsx +32 -0
  24. package/src/components/MorphingText.tsx +133 -0
  25. package/src/components/MotionSequence.tsx +150 -0
  26. package/src/components/NumberTicker.tsx +68 -0
  27. package/src/components/OrbitingCircles.tsx +83 -0
  28. package/src/components/ParallaxSection.tsx +56 -0
  29. package/src/components/Particles.tsx +268 -0
  30. package/src/components/ProofBand.tsx +66 -0
  31. package/src/components/ProofBanner.tsx +31 -0
  32. package/src/components/RemotionClip.tsx +216 -0
  33. package/src/components/SequenceDiagram.tsx +144 -0
  34. package/src/components/ShimmerButton.tsx +51 -0
  35. package/src/components/ShineBorder.tsx +87 -0
  36. package/src/components/StepTimeline.tsx +108 -0
  37. package/src/components/StickyBottomCta.tsx +53 -0
  38. package/src/components/TerminalOutput.tsx +93 -0
  39. package/src/components/TextShimmer.tsx +59 -0
  40. package/src/components/TypingAnimation.tsx +54 -0
  41. package/src/index.ts +69 -0
  42. package/src/lib/json-ld.ts +116 -0
@@ -0,0 +1,31 @@
1
+ "use client";
2
+
3
+ import { motion } from "framer-motion";
4
+ import type { ReactNode } from "react";
5
+
6
+ interface AnimatedSectionProps {
7
+ children: ReactNode;
8
+ id?: string;
9
+ className?: string;
10
+ delay?: number;
11
+ }
12
+
13
+ export function AnimatedSection({
14
+ children,
15
+ id,
16
+ className = "mb-14",
17
+ delay = 0,
18
+ }: AnimatedSectionProps) {
19
+ return (
20
+ <motion.section
21
+ id={id}
22
+ className={className}
23
+ initial={{ opacity: 0, y: 24 }}
24
+ whileInView={{ opacity: 1, y: 0 }}
25
+ viewport={{ once: true, margin: "-80px" }}
26
+ transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1], delay }}
27
+ >
28
+ {children}
29
+ </motion.section>
30
+ );
31
+ }
@@ -0,0 +1,51 @@
1
+ interface ArticleMetaProps {
2
+ author: string;
3
+ authorRole?: string;
4
+ datePublished: string;
5
+ dateModified?: string;
6
+ readingTime?: string;
7
+ className?: string;
8
+ }
9
+
10
+ function formatDate(value: string) {
11
+ const d = new Date(value);
12
+ if (Number.isNaN(d.getTime())) return value;
13
+ return d.toLocaleDateString("en-US", {
14
+ year: "numeric",
15
+ month: "long",
16
+ day: "numeric",
17
+ });
18
+ }
19
+
20
+ export function ArticleMeta({
21
+ author,
22
+ authorRole,
23
+ datePublished,
24
+ dateModified,
25
+ readingTime,
26
+ className = "",
27
+ }: ArticleMetaProps) {
28
+ const showUpdated = dateModified && dateModified !== datePublished;
29
+ return (
30
+ <div className={`flex flex-wrap items-center gap-x-3 gap-y-2 text-sm text-zinc-500 ${className}`}>
31
+ <div className="flex items-center gap-2">
32
+ <div className="w-7 h-7 rounded-full bg-gradient-to-br from-teal-500/40 to-teal-500/10 flex items-center justify-center text-xs font-semibold text-white">
33
+ {author.charAt(0)}
34
+ </div>
35
+ <span className="text-zinc-900 font-medium">{author}</span>
36
+ {authorRole && <span className="hidden sm:inline text-zinc-500">, {authorRole}</span>}
37
+ </div>
38
+ <span aria-hidden="true">&middot;</span>
39
+ <time dateTime={datePublished}>
40
+ {showUpdated ? "Updated " : "Published "}
41
+ {formatDate(dateModified || datePublished)}
42
+ </time>
43
+ {readingTime && (
44
+ <>
45
+ <span aria-hidden="true">&middot;</span>
46
+ <span>{readingTime}</span>
47
+ </>
48
+ )}
49
+ </div>
50
+ );
51
+ }
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ interface BackgroundGridProps {
4
+ children: React.ReactNode;
5
+ /** Grid cell size in pixels */
6
+ size?: number;
7
+ /** "dots" or "lines" */
8
+ pattern?: "dots" | "lines";
9
+ /** Add a radial teal glow behind the content */
10
+ glow?: boolean;
11
+ className?: string;
12
+ }
13
+
14
+ /**
15
+ * Vercel/Linear style background with a subtle grid pattern and
16
+ * optional radial glow. Use as a wrapper around hero sections or
17
+ * standout callouts. Inner content renders with full z-index above
18
+ * the pattern.
19
+ */
20
+ export function BackgroundGrid({
21
+ children,
22
+ size = 36,
23
+ pattern = "dots",
24
+ glow = true,
25
+ className = "",
26
+ }: BackgroundGridProps) {
27
+ const bg =
28
+ pattern === "dots"
29
+ ? `radial-gradient(circle, rgba(20, 184, 166, 0.18) 1px, transparent 1px)`
30
+ : `linear-gradient(rgba(20, 184, 166, 0.12) 1px, transparent 1px), linear-gradient(90deg, rgba(20, 184, 166, 0.12) 1px, transparent 1px)`;
31
+
32
+ return (
33
+ <div
34
+ className={`relative overflow-hidden rounded-2xl border border-zinc-200 my-10 ${className}`}
35
+ >
36
+ <div
37
+ className="absolute inset-0 pointer-events-none"
38
+ style={{
39
+ backgroundImage: bg,
40
+ backgroundSize: `${size}px ${size}px`,
41
+ maskImage:
42
+ "radial-gradient(ellipse at center, black 40%, transparent 85%)",
43
+ WebkitMaskImage:
44
+ "radial-gradient(ellipse at center, black 40%, transparent 85%)",
45
+ }}
46
+ />
47
+ {glow && (
48
+ <div
49
+ className="absolute inset-0 pointer-events-none"
50
+ style={{
51
+ background:
52
+ "radial-gradient(ellipse at center, rgba(20, 184, 166, 0.15) 0%, transparent 60%)",
53
+ }}
54
+ />
55
+ )}
56
+ <div className="relative z-10 p-8 md:p-12">{children}</div>
57
+ </div>
58
+ );
59
+ }
@@ -0,0 +1,121 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { motion, AnimatePresence } from "framer-motion";
5
+
6
+ interface BeforeAfterProps {
7
+ title?: string;
8
+ before: { label: string; content: string; highlights?: string[] };
9
+ after: { label: string; content: string; highlights?: string[] };
10
+ className?: string;
11
+ }
12
+
13
+ export function BeforeAfter({
14
+ title,
15
+ before,
16
+ after,
17
+ className = "",
18
+ }: BeforeAfterProps) {
19
+ const [showAfter, setShowAfter] = useState(false);
20
+
21
+ return (
22
+ <motion.div
23
+ initial={{ opacity: 0, y: 20 }}
24
+ whileInView={{ opacity: 1, y: 0 }}
25
+ viewport={{ once: true, margin: "-40px" }}
26
+ transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
27
+ className={`my-10 ${className}`}
28
+ >
29
+ {title && (
30
+ <h3 className="text-lg font-semibold text-zinc-900 mb-4">{title}</h3>
31
+ )}
32
+
33
+ {/* Toggle */}
34
+ <div className="flex items-center gap-1 mb-4 bg-zinc-100 rounded-lg p-1 w-fit">
35
+ <button
36
+ onClick={() => setShowAfter(false)}
37
+ className={`px-4 py-2 text-sm font-medium rounded-md transition-all duration-200 ${
38
+ !showAfter
39
+ ? "bg-white text-zinc-900 shadow-sm"
40
+ : "text-zinc-500 hover:text-zinc-700"
41
+ }`}
42
+ >
43
+ {before.label}
44
+ </button>
45
+ <button
46
+ onClick={() => setShowAfter(true)}
47
+ className={`px-4 py-2 text-sm font-medium rounded-md transition-all duration-200 ${
48
+ showAfter
49
+ ? "bg-gradient-to-r from-cyan-500 to-teal-500 text-white shadow-sm"
50
+ : "text-zinc-500 hover:text-zinc-700"
51
+ }`}
52
+ >
53
+ {after.label}
54
+ </button>
55
+ </div>
56
+
57
+ {/* Content panel */}
58
+ <div className="relative rounded-xl border border-zinc-200 bg-zinc-50 overflow-hidden min-h-[200px]">
59
+ <AnimatePresence mode="wait">
60
+ {!showAfter ? (
61
+ <motion.div
62
+ key="before"
63
+ initial={{ opacity: 0, x: -12 }}
64
+ animate={{ opacity: 1, x: 0 }}
65
+ exit={{ opacity: 0, x: 12 }}
66
+ transition={{ duration: 0.25, ease: [0.16, 1, 0.3, 1] }}
67
+ className="p-6"
68
+ >
69
+ <p className="text-sm text-zinc-600 leading-relaxed whitespace-pre-line">
70
+ {before.content}
71
+ </p>
72
+ {before.highlights && (
73
+ <ul className="mt-4 space-y-2">
74
+ {before.highlights.map((h, i) => (
75
+ <li
76
+ key={i}
77
+ className="flex items-center gap-2 text-sm text-red-600"
78
+ >
79
+ <svg className="w-4 h-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
80
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
81
+ </svg>
82
+ {h}
83
+ </li>
84
+ ))}
85
+ </ul>
86
+ )}
87
+ </motion.div>
88
+ ) : (
89
+ <motion.div
90
+ key="after"
91
+ initial={{ opacity: 0, x: 12 }}
92
+ animate={{ opacity: 1, x: 0 }}
93
+ exit={{ opacity: 0, x: -12 }}
94
+ transition={{ duration: 0.25, ease: [0.16, 1, 0.3, 1] }}
95
+ className="p-6"
96
+ >
97
+ <p className="text-sm text-zinc-600 leading-relaxed whitespace-pre-line">
98
+ {after.content}
99
+ </p>
100
+ {after.highlights && (
101
+ <ul className="mt-4 space-y-2">
102
+ {after.highlights.map((h, i) => (
103
+ <li
104
+ key={i}
105
+ className="flex items-center gap-2 text-sm text-teal-600"
106
+ >
107
+ <svg className="w-4 h-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
108
+ <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
109
+ </svg>
110
+ {h}
111
+ </li>
112
+ ))}
113
+ </ul>
114
+ )}
115
+ </motion.div>
116
+ )}
117
+ </AnimatePresence>
118
+ </div>
119
+ </motion.div>
120
+ );
121
+ }
@@ -0,0 +1,67 @@
1
+ "use client";
2
+
3
+ import { motion } from "framer-motion";
4
+
5
+ export interface BentoCard {
6
+ title: string;
7
+ description: string;
8
+ /** "1x1" | "2x1" (wide) | "1x2" (tall) | "2x2" (hero) */
9
+ size?: "1x1" | "2x1" | "1x2" | "2x2";
10
+ icon?: string;
11
+ accent?: boolean;
12
+ content?: React.ReactNode;
13
+ }
14
+
15
+ interface BentoGridProps {
16
+ cards: BentoCard[];
17
+ className?: string;
18
+ }
19
+
20
+ const SIZE_CLASSES: Record<string, string> = {
21
+ "1x1": "col-span-1 row-span-1",
22
+ "2x1": "col-span-2 row-span-1",
23
+ "1x2": "col-span-1 row-span-2",
24
+ "2x2": "col-span-2 row-span-2",
25
+ };
26
+
27
+ export function BentoGrid({ cards, className = "" }: BentoGridProps) {
28
+ return (
29
+ <div
30
+ className={`grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 my-10 ${className}`}
31
+ >
32
+ {cards.map((card, i) => (
33
+ <motion.div
34
+ key={i}
35
+ initial={{ opacity: 0, y: 20, scale: 0.97 }}
36
+ whileInView={{ opacity: 1, y: 0, scale: 1 }}
37
+ viewport={{ once: true, margin: "-40px" }}
38
+ transition={{
39
+ duration: 0.45,
40
+ ease: [0.16, 1, 0.3, 1],
41
+ delay: i * 0.06,
42
+ }}
43
+ className={`${SIZE_CLASSES[card.size || "1x1"]} rounded-2xl border ${
44
+ card.accent
45
+ ? "border-teal-200 bg-gradient-to-br from-teal-50 to-cyan-50"
46
+ : "border-zinc-200 bg-zinc-50"
47
+ } p-6 flex flex-col justify-between group hover:border-teal-300 hover:shadow-md transition-all duration-300`}
48
+ >
49
+ <div>
50
+ {card.icon && (
51
+ <span className="text-2xl mb-3 block">{card.icon}</span>
52
+ )}
53
+ <h3 className="text-base font-semibold text-zinc-900 mb-2">
54
+ {card.title}
55
+ </h3>
56
+ <p className="text-sm text-zinc-500 leading-relaxed">
57
+ {card.description}
58
+ </p>
59
+ </div>
60
+ {card.content && (
61
+ <div className="mt-4 overflow-hidden rounded-lg">{card.content}</div>
62
+ )}
63
+ </motion.div>
64
+ ))}
65
+ </div>
66
+ );
67
+ }
@@ -0,0 +1,40 @@
1
+ import Link from "next/link";
2
+ import { Fragment } from "react";
3
+
4
+ export interface BreadcrumbCrumb {
5
+ label: string;
6
+ href?: string;
7
+ }
8
+
9
+ interface BreadcrumbsProps {
10
+ items: BreadcrumbCrumb[];
11
+ className?: string;
12
+ align?: "start" | "center";
13
+ }
14
+
15
+ export function Breadcrumbs({ items, className = "", align = "start" }: BreadcrumbsProps) {
16
+ const justify = align === "center" ? "justify-center" : "justify-start";
17
+ return (
18
+ <nav aria-label="Breadcrumb" className={className}>
19
+ <ol className={`flex flex-wrap items-center ${justify} gap-2 text-sm text-zinc-500`}>
20
+ {items.map((item, i) => {
21
+ const isLast = i === items.length - 1;
22
+ return (
23
+ <Fragment key={`${item.label}-${i}`}>
24
+ <li className={isLast ? "text-zinc-900 line-clamp-1" : ""}>
25
+ {item.href && !isLast ? (
26
+ <Link href={item.href} className="hover:text-zinc-900 transition-colors">
27
+ {item.label}
28
+ </Link>
29
+ ) : (
30
+ item.label
31
+ )}
32
+ </li>
33
+ {!isLast && <li aria-hidden="true">/</li>}
34
+ </Fragment>
35
+ );
36
+ })}
37
+ </ol>
38
+ </nav>
39
+ );
40
+ }
@@ -0,0 +1,115 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { motion, AnimatePresence } from "framer-motion";
5
+
6
+ interface CodeComparisonProps {
7
+ leftCode: string;
8
+ rightCode: string;
9
+ leftLines: number;
10
+ rightLines: number;
11
+ leftLabel: string;
12
+ rightLabel: string;
13
+ title?: string;
14
+ reductionSuffix?: string;
15
+ }
16
+
17
+ export function CodeComparison({
18
+ leftCode,
19
+ rightCode,
20
+ leftLines,
21
+ rightLines,
22
+ leftLabel,
23
+ rightLabel,
24
+ title,
25
+ reductionSuffix = "fewer lines",
26
+ }: CodeComparisonProps) {
27
+ const [tab, setTab] = useState<"left" | "right">("left");
28
+ const reduction = Math.round((1 - rightLines / leftLines) * 100);
29
+
30
+ return (
31
+ <motion.div
32
+ className="my-8 rounded-2xl border border-zinc-200 bg-white overflow-hidden"
33
+ initial={{ opacity: 0, y: 12 }}
34
+ whileInView={{ opacity: 1, y: 0 }}
35
+ viewport={{ once: true }}
36
+ transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
37
+ >
38
+ <div className="flex items-center justify-between px-5 py-3 border-b border-zinc-200 bg-white/50">
39
+ {title && (
40
+ <p className="text-xs font-mono uppercase tracking-widest text-emerald-500">{title}</p>
41
+ )}
42
+ <div className="flex gap-1 rounded-lg bg-white p-0.5 border border-zinc-200">
43
+ <button
44
+ onClick={() => setTab("left")}
45
+ className={`text-[11px] font-medium px-3 py-1 rounded-md transition-colors ${
46
+ tab === "left"
47
+ ? "bg-teal-500/10 text-teal-600"
48
+ : "text-zinc-500 hover:text-zinc-900"
49
+ }`}
50
+ >
51
+ {leftLabel} ({leftLines} lines)
52
+ </button>
53
+ <button
54
+ onClick={() => setTab("right")}
55
+ className={`text-[11px] font-medium px-3 py-1 rounded-md transition-colors ${
56
+ tab === "right"
57
+ ? "bg-emerald-500/10 text-emerald-500"
58
+ : "text-zinc-500 hover:text-zinc-900"
59
+ }`}
60
+ >
61
+ {rightLabel} ({rightLines} lines)
62
+ </button>
63
+ </div>
64
+ </div>
65
+
66
+ <div className="relative min-h-[200px]">
67
+ <AnimatePresence mode="wait">
68
+ {tab === "left" ? (
69
+ <motion.pre
70
+ key="left"
71
+ className="p-5 text-sm font-mono overflow-x-auto"
72
+ initial={{ opacity: 0, x: -8 }}
73
+ animate={{ opacity: 1, x: 0 }}
74
+ exit={{ opacity: 0, x: 8 }}
75
+ transition={{ duration: 0.2 }}
76
+ >
77
+ <code className="text-code-text">{leftCode}</code>
78
+ </motion.pre>
79
+ ) : (
80
+ <motion.pre
81
+ key="right"
82
+ className="p-5 text-sm font-mono overflow-x-auto"
83
+ initial={{ opacity: 0, x: -8 }}
84
+ animate={{ opacity: 1, x: 0 }}
85
+ exit={{ opacity: 0, x: 8 }}
86
+ transition={{ duration: 0.2 }}
87
+ >
88
+ <code className="text-code-text">{rightCode}</code>
89
+ </motion.pre>
90
+ )}
91
+ </AnimatePresence>
92
+ </div>
93
+
94
+ <div className="px-5 py-3 border-t border-zinc-200 bg-white/50 flex items-center gap-3">
95
+ <motion.div
96
+ className="h-1.5 rounded-full bg-emerald-500/20 flex-1 overflow-hidden"
97
+ initial={{ opacity: 0 }}
98
+ whileInView={{ opacity: 1 }}
99
+ viewport={{ once: true }}
100
+ >
101
+ <motion.div
102
+ className="h-full rounded-full bg-emerald-500"
103
+ initial={{ width: "100%" }}
104
+ whileInView={{ width: `${100 - reduction}%` }}
105
+ viewport={{ once: true }}
106
+ transition={{ duration: 1.2, ease: [0.16, 1, 0.3, 1], delay: 0.3 }}
107
+ />
108
+ </motion.div>
109
+ <span className="text-[11px] font-medium text-emerald-500 whitespace-nowrap">
110
+ {reduction}% {reductionSuffix}
111
+ </span>
112
+ </div>
113
+ </motion.div>
114
+ );
115
+ }
@@ -0,0 +1,66 @@
1
+ export interface ComparisonRow {
2
+ feature: string;
3
+ competitor: string;
4
+ ours: string;
5
+ }
6
+
7
+ interface ComparisonTableProps {
8
+ heading?: string;
9
+ intro?: string;
10
+ productName: string;
11
+ competitorName: string;
12
+ rows: ComparisonRow[];
13
+ caveat?: string;
14
+ className?: string;
15
+ }
16
+
17
+ export function ComparisonTable({
18
+ heading,
19
+ intro,
20
+ productName,
21
+ competitorName,
22
+ rows,
23
+ caveat,
24
+ className = "",
25
+ }: ComparisonTableProps) {
26
+ return (
27
+ <section className={className}>
28
+ {heading && (
29
+ <h2 className="text-3xl sm:text-4xl font-bold text-zinc-900 mb-4 text-center">
30
+ {heading}
31
+ </h2>
32
+ )}
33
+ {intro && (
34
+ <p className="text-zinc-500 text-lg text-center mb-8 max-w-2xl mx-auto">
35
+ {intro}
36
+ </p>
37
+ )}
38
+ <div className="rounded-2xl bg-zinc-50 border border-zinc-200 overflow-hidden">
39
+ <table className="w-full text-left">
40
+ <thead>
41
+ <tr className="border-b border-zinc-200">
42
+ <th className="px-5 py-4 text-zinc-500 text-sm font-medium">Feature</th>
43
+ <th className="px-5 py-4 text-zinc-500 text-sm font-medium">{competitorName}</th>
44
+ <th className="px-5 py-4 text-teal-600 text-sm font-medium">{productName}</th>
45
+ </tr>
46
+ </thead>
47
+ <tbody>
48
+ {rows.map((row, i) => (
49
+ <tr
50
+ key={i}
51
+ className={i < rows.length - 1 ? "border-b border-zinc-200" : ""}
52
+ >
53
+ <td className="px-5 py-3 text-zinc-900 text-sm font-medium">{row.feature}</td>
54
+ <td className="px-5 py-3 text-zinc-500 text-sm">{row.competitor}</td>
55
+ <td className="px-5 py-3 text-zinc-900 text-sm">{row.ours}</td>
56
+ </tr>
57
+ ))}
58
+ </tbody>
59
+ </table>
60
+ </div>
61
+ {caveat && (
62
+ <p className="text-zinc-500 text-sm mt-4 text-center">{caveat}</p>
63
+ )}
64
+ </section>
65
+ );
66
+ }
@@ -0,0 +1,40 @@
1
+ import type { FaqItem } from "../lib/json-ld";
2
+
3
+ interface FaqSectionProps {
4
+ items: FaqItem[];
5
+ heading?: string;
6
+ className?: string;
7
+ }
8
+
9
+ export function FaqSection({
10
+ items,
11
+ heading = "Frequently asked questions",
12
+ className = "",
13
+ }: FaqSectionProps) {
14
+ return (
15
+ <section className={className}>
16
+ <h2 className="text-3xl sm:text-4xl font-bold text-zinc-900 mb-8">{heading}</h2>
17
+ <div className="space-y-4">
18
+ {items.map((faq) => (
19
+ <details
20
+ key={faq.q}
21
+ className="group p-5 rounded-xl bg-zinc-50 border border-zinc-200"
22
+ >
23
+ <summary className="text-zinc-900 font-semibold cursor-pointer list-none flex items-center justify-between gap-4">
24
+ <span>{faq.q}</span>
25
+ <span
26
+ className="text-zinc-500 group-open:rotate-45 transition-transform text-xl flex-shrink-0"
27
+ aria-hidden="true"
28
+ >
29
+ +
30
+ </span>
31
+ </summary>
32
+ <p className="text-zinc-500 mt-3 leading-relaxed">{faq.a}</p>
33
+ </details>
34
+ ))}
35
+ </div>
36
+ </section>
37
+ );
38
+ }
39
+
40
+ export type { FaqItem };
@@ -0,0 +1,86 @@
1
+ "use client";
2
+
3
+ import { motion } from "framer-motion";
4
+
5
+ interface FlowStep {
6
+ label: string;
7
+ detail?: string;
8
+ icon?: "browser" | "server" | "redirect" | "iframe" | "webhook" | "lock" | "check" | "error" | "wallet" | "email";
9
+ }
10
+
11
+ interface FlowDiagramProps {
12
+ title: string;
13
+ steps: FlowStep[];
14
+ }
15
+
16
+ const icons: Record<string, string> = {
17
+ browser: "\uD83C\uDF10",
18
+ server: "\u2699\uFE0F",
19
+ redirect: "\u21AA\uFE0F",
20
+ iframe: "\uD83D\uDCE6",
21
+ webhook: "\uD83D\uDD14",
22
+ lock: "\uD83D\uDD12",
23
+ check: "\u2705",
24
+ error: "\u274C",
25
+ wallet: "\uD83D\uDCB3",
26
+ email: "\uD83D\uDCE7",
27
+ };
28
+
29
+ export function FlowDiagram({ title, steps }: FlowDiagramProps) {
30
+ return (
31
+ <div className="my-10 rounded-2xl border border-zinc-200 bg-white p-6 overflow-x-auto">
32
+ <p className="text-xs font-mono uppercase tracking-widest text-emerald-500 mb-5">
33
+ {title}
34
+ </p>
35
+ <div className="flex items-start gap-0 min-w-max">
36
+ {steps.map((step, i) => (
37
+ <motion.div
38
+ key={i}
39
+ className="flex items-start"
40
+ initial={{ opacity: 0, x: -12 }}
41
+ whileInView={{ opacity: 1, x: 0 }}
42
+ viewport={{ once: true }}
43
+ transition={{
44
+ duration: 0.35,
45
+ ease: [0.16, 1, 0.3, 1],
46
+ delay: i * 0.08,
47
+ }}
48
+ >
49
+ <div className="flex flex-col items-center w-28 shrink-0">
50
+ <div className="w-11 h-11 rounded-xl bg-white border border-zinc-200 flex items-center justify-center text-lg mb-2">
51
+ {step.icon ? icons[step.icon] || "\u2022" : `${i + 1}`}
52
+ </div>
53
+ <p className="text-xs font-medium text-zinc-900 text-center leading-tight">
54
+ {step.label}
55
+ </p>
56
+ {step.detail && (
57
+ <p className="text-[10px] text-zinc-500 text-center leading-tight mt-1">
58
+ {step.detail}
59
+ </p>
60
+ )}
61
+ </div>
62
+ {i < steps.length - 1 && (
63
+ <motion.div
64
+ className="flex items-center h-11 shrink-0"
65
+ initial={{ scaleX: 0 }}
66
+ whileInView={{ scaleX: 1 }}
67
+ viewport={{ once: true }}
68
+ transition={{
69
+ duration: 0.25,
70
+ ease: [0.16, 1, 0.3, 1],
71
+ delay: i * 0.08 + 0.15,
72
+ }}
73
+ style={{ originX: 0 }}
74
+ >
75
+ <svg width="32" height="12" viewBox="0 0 32 12" className="text-zinc-500">
76
+ <line x1="0" y1="6" x2="24" y2="6" stroke="currentColor" strokeWidth="1.5" />
77
+ <polygon points="24,2 32,6 24,10" fill="currentColor" />
78
+ </svg>
79
+ </motion.div>
80
+ )}
81
+ </motion.div>
82
+ ))}
83
+ </div>
84
+ </div>
85
+ );
86
+ }