blocks-dusted 0.1.1 → 0.1.3

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 (56) hide show
  1. package/README.md +967 -3
  2. package/package.json +2 -2
  3. package/src/commands/add.js +143 -45
  4. package/src/commands/list.js +2 -2
  5. package/src/installers/checkRequirements.js +1 -0
  6. package/src/installers/copyTemplateFiles.js +23 -2
  7. package/src/installers/patchPayloadConfigCollections.js +92 -0
  8. package/src/installers/writeInstalledBlockReadme.js +17 -4
  9. package/src/registry/listTemplates.js +16 -7
  10. package/src/registry/loadTemplateManifest.js +20 -11
  11. package/src/registry/validateTemplateManifest.js +6 -5
  12. package/src/utils/paths.js +7 -0
  13. package/templates/blocks/DD-BookCall/Component.tsx +535 -573
  14. package/templates/blocks/DD-CardTemplate01/Component.tsx +45 -49
  15. package/templates/blocks/DD-Carousel-A/Component.tsx +249 -266
  16. package/templates/blocks/DD-Carousel-B/Component.tsx +403 -432
  17. package/templates/blocks/DD-ComparisonTable/Component.tsx +256 -272
  18. package/templates/blocks/DD-Contact/Component.tsx +434 -439
  19. package/templates/blocks/DD-Contact/config.ts +8 -3
  20. package/templates/blocks/DD-FeatCat/Component.tsx +302 -361
  21. package/templates/blocks/DD-FeatCat/config.ts +201 -204
  22. package/templates/blocks/DD-FeatStrip/Component.tsx +171 -201
  23. package/templates/blocks/DD-Hero/Component.tsx +297 -317
  24. package/templates/blocks/DD-HorizontalScroll/Component.tsx +244 -303
  25. package/templates/blocks/DD-MasonaryMedia/Component.tsx +202 -228
  26. package/templates/blocks/DD-MasonaryMedia/config.ts +13 -14
  27. package/templates/blocks/DD-Pricing/Component.tsx +372 -380
  28. package/templates/blocks/DD-Process/Component.tsx +149 -155
  29. package/templates/blocks/DD-Section/Component.tsx +266 -327
  30. package/templates/blocks/DD-Services/Component.tsx +176 -188
  31. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +307 -317
  32. package/templates/blocks/DD-Slider-A/Component.tsx +237 -252
  33. package/templates/blocks/DD-StackCards/Component.tsx +137 -160
  34. package/templates/blocks/DD-Team/Component.tsx +247 -265
  35. package/templates/blocks/DD-TechStack/Component.tsx +57 -72
  36. package/templates/blocks/DD-Testimonials/Component.tsx +147 -147
  37. package/templates/blocks/DD-Testimonials/config.ts +66 -66
  38. package/templates/blocks/DD-Work/Component.tsx +315 -328
  39. package/templates/blocks/DD-Work-B/Component.tsx +294 -320
  40. package/templates/collections/Testimonials/README.md +11 -0
  41. package/templates/collections/Testimonials/Testimonials.ts +161 -0
  42. package/templates/collections/Testimonials/manifest.json +55 -0
  43. package/templates/components/HeaderDD02/MobileBottomNav/index.tsx +545 -0
  44. package/templates/components/HeaderDD02/README.md +45 -0
  45. package/templates/components/HeaderDD02/RowLabel.tsx +33 -0
  46. package/templates/components/HeaderDD02/config.ts +302 -0
  47. package/templates/components/HeaderDD02/index.tsx +517 -0
  48. package/templates/components/HeaderDD02/manifest.json +123 -0
  49. package/templates/components/RichText/README.md +13 -0
  50. package/templates/components/RichText/converter/componentConverter/blocks.tsx +43 -0
  51. package/templates/components/RichText/converter/componentConverter/types.ts +11 -0
  52. package/templates/components/RichText/converter/index.tsx +18 -0
  53. package/templates/components/RichText/converter/internalLinks.tsx +45 -0
  54. package/templates/components/RichText/converter/textConverter.tsx +27 -0
  55. package/templates/components/RichText/index.tsx +35 -0
  56. package/templates/components/RichText/manifest.json +80 -0
@@ -1,317 +1,297 @@
1
- "use client";
2
-
3
- import React, { useState } from "react";
4
- import Image from "next/image";
5
- import { motion } from "framer-motion";
6
- import { TextFade } from "@/components/ui/anims/fade";
7
- import * as LucideIcons from "lucide-react";
8
-
9
- export type DDHeroProps = Record<string, any>;
10
- type Media = { url?: string | null; alt?: string | null; [key: string]: any };
11
- type ButtonItem = {
12
- href?: string | null;
13
- class?: string | null;
14
- label?: string | null;
15
- };
16
- type StatItem = { value?: string | null; label?: string | null };
17
-
18
- function isLocalAssetSrc(src?: string | null): src is string {
19
- return typeof src === "string" && src.startsWith("/api/media/file/");
20
- }
21
-
22
- function HeroPlaceholder() {
23
- return (
24
- <svg
25
- aria-hidden="true"
26
- className="h-full w-full"
27
- viewBox="0 0 1600 900"
28
- preserveAspectRatio="none"
29
- >
30
- <defs>
31
- <linearGradient id="dd-hero-placeholder" x1="0" y1="0" x2="1" y2="1">
32
- <stop offset="0%" stopColor="#09090b" />
33
- <stop offset="45%" stopColor="#241244" />
34
- <stop offset="100%" stopColor="#111827" />
35
- </linearGradient>
36
- <pattern
37
- id="dd-hero-grid"
38
- width="80"
39
- height="80"
40
- patternUnits="userSpaceOnUse"
41
- >
42
- <path
43
- d="M 80 0 L 0 0 0 80"
44
- fill="none"
45
- stroke="#ffffff"
46
- strokeOpacity="0.06"
47
- strokeWidth="1"
48
- />
49
- </pattern>
50
- </defs>
51
- <rect width="1600" height="900" fill="url(#dd-hero-placeholder)" />
52
- <rect width="1600" height="900" fill="url(#dd-hero-grid)" />
53
- <path
54
- d="M 1040 140 C 1260 220 1380 390 1320 570 C 1240 800 910 790 760 640 C 620 500 790 260 1040 140 Z"
55
- fill="#ffffff"
56
- fillOpacity="0.08"
57
- />
58
- <path
59
- d="M 180 620 C 360 470 570 490 690 640 C 810 790 600 900 360 840 C 210 800 90 740 180 620 Z"
60
- fill="#a3e635"
61
- fillOpacity="0.08"
62
- />
63
- </svg>
64
- );
65
- }
66
-
67
- function DynamicIcon({
68
- name,
69
- size = 16,
70
- ...props
71
- }: { name?: string | null } & Omit<LucideIcons.LucideProps, "name">) {
72
- if (!name) return null;
73
- const Icon = (
74
- LucideIcons as unknown as Record<string, React.FC<LucideIcons.LucideProps>>
75
- )[name];
76
- if (!Icon) return null;
77
- return <Icon size={size} {...props} />;
78
- }
79
-
80
- export const DDHero: React.FC<DDHeroProps> = ({
81
- badge,
82
- headlineMain,
83
- headlineAccent,
84
- description,
85
- // primaryCta,
86
- // secondaryCta,
87
- stats,
88
- backgroundImage,
89
- backgroundImageFallbackUrl,
90
- sectionId,
91
- showScrollIndicator,
92
- customCSS,
93
- showButtons = true,
94
- buttons = [
95
- { variant: "v1", icon: "Goal", label: "Choose Lane" },
96
- { variant: "v1", icon: "Feather", label: "How it works" },
97
- ],
98
- }) => {
99
- const bgMedia =
100
- backgroundImage && typeof backgroundImage === "object"
101
- ? (backgroundImage as Media)
102
- : null;
103
- const bgUrl = bgMedia?.url ?? backgroundImageFallbackUrl ?? undefined;
104
- const safeBgUrl = isLocalAssetSrc(bgUrl) ? bgUrl : null;
105
- const isVideo = Boolean(
106
- safeBgUrl && (bgMedia?.mimeType?.startsWith("video/") ?? false),
107
- );
108
- const buttonItems = buttons as ButtonItem[];
109
- const statItems = (stats ?? []) as StatItem[];
110
-
111
- const [bgLoaded, setBgLoaded] = useState(false);
112
-
113
- // Get dynamic icon component
114
- const ChipIcon = LucideIcons.Sparkles;
115
-
116
- return (
117
- <>
118
- {customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
119
-
120
- <div
121
- id={sectionId ?? undefined}
122
- className="relative min-h-screen flex items-center overflow-hidden"
123
- >
124
- {/* Background Image */}
125
- <div className="absolute inset-0">
126
- {safeBgUrl ? (
127
- <>
128
- {!bgLoaded && (
129
- <div className="absolute inset-0 bg-stone-900 animate-pulse" />
130
- )}
131
- {isVideo ? (
132
- <video
133
- src={safeBgUrl}
134
- autoPlay
135
- muted
136
- loop
137
- playsInline
138
- preload="none"
139
- className="w-full h-full object-cover object-top"
140
- onLoadedData={() => setBgLoaded(true)}
141
- />
142
- ) : (
143
- <Image
144
- src={safeBgUrl}
145
- alt="Studio hero background"
146
- fill
147
- priority
148
- loading="eager"
149
- className="object-cover object-top"
150
- onLoad={() => setBgLoaded(true)}
151
- />
152
- )}
153
- </>
154
- ) : (
155
- <HeroPlaceholder />
156
- )}
157
- <div className="absolute inset-0 bg-gradient-to-r from-stone-950/95 via-stone-950/80 to-stone-950/40" />
158
- <div className="absolute inset-0 bg-linear-to-t from-stone-950/60 via-transparent to-transparent" />
159
- </div>
160
-
161
- {/* Content */}
162
- <div className="relative z-10 w-full max-w-6xl lg:max-w-7xl mx-auto px-4 md:px-6 pt-28 pb-24">
163
- <div className="max-w-2xl">
164
- {/* Badge */}
165
- {/* {badge?.show && badge.text && (
166
- <div className="inline-flex items-center gap-2 bg-stone-50/10 border border-white/20 rounded-full px-4 py-1.5 mb-8">
167
- <span className="w-1.5 h-1.5 rounded-full bg-lime-400 animate-pulse" />
168
- <span className="text-stone-50/80 text-xs font-medium tracking-wide">
169
- {badge.text}
170
- </span>
171
- </div>
172
- )} */}
173
-
174
- {/* Badge - to convert to dynamic */}
175
- {badge?.show && badge.text && (
176
- <motion.div
177
- variants={{
178
- hidden: { opacity: 0, y: -20, filter: "blur(6px)" },
179
- visible: { opacity: 1, y: 0, filter: "none" },
180
- }}
181
- transition={{ duration: 0.5, ease: "easeOut" }}
182
- >
183
- <span className="inline-flex items-center gap-2 rounded-full border border-lime-500 bg-black/70 px-4 py-1.5 text-xs text-lime-100">
184
- {/* TODO: Reconnect Badge/AnimatedShinyText if this project provides them. */}
185
- <ChipIcon className="mr-2 h-3.5 w-3.5" />
186
- {badge.text}
187
- </span>
188
- </motion.div>
189
- )}
190
-
191
- {/* Headline */}
192
- {/* <h1 className="text-5xl md:text-6xl lg:text-7xl font-semibold text-stone-900 dark:text-stone-50 leading-[1.05] tracking-tight mb-6">
193
- {headlineMain}
194
- <br />
195
- <span className="text-stone-400">{headlineAccent}</span>
196
- </h1> */}
197
-
198
- {/* Headline */}
199
- <motion.div
200
- className="relative mt-4 z-50 headline-main"
201
- variants={{
202
- hidden: { opacity: 0, y: 40 },
203
- visible: { opacity: 1, y: 0 },
204
- }}
205
- viewport={{ once: false, amount: 0.3 }}
206
- transition={{ duration: 0.6, ease: "easeOut" }}
207
- style={{ filter: "drop-shadow(3px 3px 1px #151515)" }}
208
- >
209
- <span className="block text-6xl sm:text-6xl font-semibold text-left tracking-tight md:text-7xl text-purple-600 dark:text-violet-600 hyphens-auto">
210
- {/* TODO: Reconnect SplittingText animation if this project provides it. */}
211
- {headlineMain}
212
- </span>
213
- </motion.div>
214
- <motion.div
215
- className="relative z-50 headline-accent"
216
- variants={{
217
- hidden: { opacity: 0, y: 40 },
218
- visible: { opacity: 1, y: 0 },
219
- }}
220
- viewport={{ once: false, amount: 0.3 }}
221
- transition={{ duration: 0.6, ease: "easeOut" }}
222
- style={{ filter: "drop-shadow(3px 3px 1px #151515)" }}
223
- >
224
- <span className="block text-6xl sm:text-6xl font-semibold text-left tracking-tight md:text-7xl text-purple-900 dark:text-violet-900 hyphens-auto">
225
- {/* TODO: Reconnect SplittingText animation if this project provides it. */}
226
- {headlineAccent}
227
- </span>
228
- </motion.div>
229
-
230
- {/* Description */}
231
- <TextFade direction="down" className="mt-10" delay={0.5}>
232
- {description && (
233
- <p className="text-stone-50/65 leading-relaxed mt-4 mb-0 max-w-xl">
234
- {description}
235
- </p>
236
- )}
237
- </TextFade>
238
-
239
- {/* CTA Buttons */}
240
- {/* <div className="flex flex-col sm:flex-row gap-3 mx-4">
241
- {primaryCta?.label && (
242
- <a
243
- href={primaryCta.href ?? '#'}
244
- className="inline-flex items-center justify-center gap-2 bg-stone-50 text-stone-900 px-6 py-3 rounded-lg text-sm font-semibold hover:bg-stone-100 transition-colors cursor-pointer whitespace-nowrap"
245
- >
246
- <DynamicIcon className="shrink-0" name={primaryCta.iconName} size={16} />
247
- {primaryCta.label}
248
- </a>
249
- )}
250
- {secondaryCta?.label && (
251
- <a
252
- href={secondaryCta.href ?? '#'}
253
- className="inline-flex items-center justify-center gap-2 bg-stone-50/10 border border-white/20 text-stone-50 px-6 py-3 rounded-lg text-sm font-semibold hover:bg-stone-50/20 transition-colors cursor-pointer whitespace-nowrap"
254
- >
255
- <DynamicIcon className="shrink-0" name={secondaryCta.iconName} size={16} />
256
- {secondaryCta.label}
257
- </a>
258
- )}
259
- </div> */}
260
-
261
- {/* Dusted CTA Buttons */}
262
- {showButtons && buttonItems.length > 0 && (
263
- <motion.div
264
- className="dusted-hero-cta-col mt-8 relative flex col-start-2 col-span-2 gap-4"
265
- variants={{
266
- hidden: { opacity: 0, y: 20 },
267
- visible: { opacity: 1, y: 0 },
268
- }}
269
- transition={{ duration: 0.6, ease: "easeOut" }}
270
- >
271
- {buttonItems.map((button, index) => (
272
- <a
273
- key={index}
274
- href={(button as any).href ?? "#"}
275
- className={
276
- button.class ??
277
- "inline-flex items-center gap-1.5 bg-indigo-700 text-stone-50 text-xs font-semibold px-3 py-1.5 rounded-lg group-hover:bg-indigo-600 group-hover:scale-[1.05] group-hover:shadow-xl transition-all duration-150 whitespace-nowrap"
278
- }
279
- style={{ cornerShape: "squircle" } as React.CSSProperties}
280
- >
281
- {/* TODO: Reconnect ButtonDustedFluid if this project provides it. */}
282
- {button.label || "Button"}
283
- </a>
284
- ))}
285
- </motion.div>
286
- )}
287
-
288
- {/* Stats */}
289
- {statItems.length > 0 && (
290
- <div className="flex items-center gap-8 mt-24 pt-8 border-t border-white/10">
291
- {statItems.map((stat, index) => (
292
- <div key={stat.label ?? index}>
293
- <div className="text-4xl font-semibold text-stone-100/50 dark:text-stone-50">
294
- {stat.value}
295
- </div>
296
- <div className="text-xs text-stone-50/75 mt-0.5">
297
- {stat.label}
298
- </div>
299
- </div>
300
- ))}
301
- </div>
302
- )}
303
- </div>
304
- </div>
305
-
306
- {/* Scroll indicator */}
307
- {showScrollIndicator && (
308
- <div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2 animate-bounce">
309
- <div className="w-5 h-8 border border-white/30 rounded-full flex items-start justify-center pt-1.5">
310
- <div className="w-1 h-2 bg-stone-50/50 rounded-full" />
311
- </div>
312
- </div>
313
- )}
314
- </div>
315
- </>
316
- );
317
- };
1
+ 'use client'
2
+
3
+ import React, { useState } from 'react'
4
+ import Image from 'next/image'
5
+ import { motion } from 'framer-motion'
6
+ import { TextFade } from '@/components/ui/anims/fade'
7
+ import * as LucideIcons from 'lucide-react'
8
+
9
+ export type DDHeroProps = Record<string, any>
10
+ type Media = { url?: string | null; alt?: string | null; [key: string]: any }
11
+ type ButtonItem = { href?: string | null; class?: string | null; label?: string | null }
12
+ type StatItem = { value?: string | null; label?: string | null }
13
+
14
+ function isLocalAssetSrc(src?: string | null): src is string {
15
+ return typeof src === 'string' && src.startsWith('/api/media/file/')
16
+ }
17
+
18
+ function HeroPlaceholder() {
19
+ return (
20
+ <svg
21
+ aria-hidden="true"
22
+ className="h-full w-full"
23
+ viewBox="0 0 1600 900"
24
+ preserveAspectRatio="none"
25
+ >
26
+ <defs>
27
+ <linearGradient id="dd-hero-placeholder" x1="0" y1="0" x2="1" y2="1">
28
+ <stop offset="0%" stopColor="#09090b" />
29
+ <stop offset="45%" stopColor="#241244" />
30
+ <stop offset="100%" stopColor="#111827" />
31
+ </linearGradient>
32
+ <pattern id="dd-hero-grid" width="80" height="80" patternUnits="userSpaceOnUse">
33
+ <path
34
+ d="M 80 0 L 0 0 0 80"
35
+ fill="none"
36
+ stroke="#ffffff"
37
+ strokeOpacity="0.06"
38
+ strokeWidth="1"
39
+ />
40
+ </pattern>
41
+ </defs>
42
+ <rect width="1600" height="900" fill="url(#dd-hero-placeholder)" />
43
+ <rect width="1600" height="900" fill="url(#dd-hero-grid)" />
44
+ <path
45
+ d="M 1040 140 C 1260 220 1380 390 1320 570 C 1240 800 910 790 760 640 C 620 500 790 260 1040 140 Z"
46
+ fill="#ffffff"
47
+ fillOpacity="0.08"
48
+ />
49
+ <path
50
+ d="M 180 620 C 360 470 570 490 690 640 C 810 790 600 900 360 840 C 210 800 90 740 180 620 Z"
51
+ fill="#a3e635"
52
+ fillOpacity="0.08"
53
+ />
54
+ </svg>
55
+ )
56
+ }
57
+
58
+ function DynamicIcon({
59
+ name,
60
+ size = 16,
61
+ ...props
62
+ }: { name?: string | null } & Omit<LucideIcons.LucideProps, 'name'>) {
63
+ if (!name) return null
64
+ const Icon = (LucideIcons as unknown as Record<string, React.FC<LucideIcons.LucideProps>>)[name]
65
+ if (!Icon) return null
66
+ return <Icon size={size} {...props} />
67
+ }
68
+
69
+ export const DDHero: React.FC<DDHeroProps> = ({
70
+ badge,
71
+ headlineMain,
72
+ headlineAccent,
73
+ description,
74
+ // primaryCta,
75
+ // secondaryCta,
76
+ stats,
77
+ backgroundImage,
78
+ backgroundImageFallbackUrl,
79
+ sectionId,
80
+ showScrollIndicator,
81
+ customCSS,
82
+ showButtons = true,
83
+ buttons = [
84
+ { variant: 'v1', icon: 'Goal', label: 'Choose Lane' },
85
+ { variant: 'v1', icon: 'Feather', label: 'How it works' },
86
+ ],
87
+ }) => {
88
+ const bgMedia =
89
+ backgroundImage && typeof backgroundImage === 'object' ? (backgroundImage as Media) : null
90
+ const bgUrl = bgMedia?.url ?? backgroundImageFallbackUrl ?? undefined
91
+ const safeBgUrl = isLocalAssetSrc(bgUrl) ? bgUrl : null
92
+ const isVideo = Boolean(safeBgUrl && (bgMedia?.mimeType?.startsWith('video/') ?? false))
93
+ const buttonItems = buttons as ButtonItem[]
94
+ const statItems = (stats ?? []) as StatItem[]
95
+
96
+ const [bgLoaded, setBgLoaded] = useState(false)
97
+
98
+ // Get dynamic icon component
99
+ const ChipIcon = LucideIcons.Sparkles
100
+
101
+ return (
102
+ <>
103
+ {customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
104
+
105
+ <div
106
+ id={sectionId ?? undefined}
107
+ className="relative min-h-screen flex items-center overflow-hidden"
108
+ >
109
+ {/* Background Image */}
110
+ <div className="absolute inset-0">
111
+ {safeBgUrl ? (
112
+ <>
113
+ {!bgLoaded && <div className="absolute inset-0 bg-stone-900 animate-pulse" />}
114
+ {isVideo ? (
115
+ <video
116
+ src={safeBgUrl}
117
+ autoPlay
118
+ muted
119
+ loop
120
+ playsInline
121
+ preload="none"
122
+ className="w-full h-full object-cover object-top"
123
+ onLoadedData={() => setBgLoaded(true)}
124
+ />
125
+ ) : (
126
+ <Image
127
+ src={safeBgUrl}
128
+ alt="Studio hero background"
129
+ fill
130
+ priority
131
+ loading="eager"
132
+ className="object-cover object-top"
133
+ onLoad={() => setBgLoaded(true)}
134
+ />
135
+ )}
136
+ </>
137
+ ) : (
138
+ <HeroPlaceholder />
139
+ )}
140
+ <div className="absolute inset-0 bg-gradient-to-r from-stone-950/95 via-stone-950/80 to-stone-950/40" />
141
+ <div className="absolute inset-0 bg-linear-to-t from-stone-950/60 via-transparent to-transparent" />
142
+ </div>
143
+
144
+ {/* Content */}
145
+ <div className="relative z-10 w-full max-w-6xl lg:max-w-7xl mx-auto px-4 md:px-6 pt-28 pb-24">
146
+ #BlockName-DDHero
147
+ <div className="max-w-2xl">
148
+ {/* Badge */}
149
+ {/* {badge?.show && badge.text && (
150
+ <div className="inline-flex items-center gap-2 bg-stone-50/10 border border-white/20 rounded-full px-4 py-1.5 mb-8">
151
+ <span className="w-1.5 h-1.5 rounded-full bg-lime-400 animate-pulse" />
152
+ <span className="text-stone-50/80 text-xs font-medium tracking-wide">
153
+ {badge.text}
154
+ </span>
155
+ </div>
156
+ )} */}
157
+
158
+ {/* Badge - to convert to dynamic */}
159
+ {badge?.show && badge.text && (
160
+ <motion.div
161
+ variants={{
162
+ hidden: { opacity: 0, y: -20, filter: 'blur(6px)' },
163
+ visible: { opacity: 1, y: 0, filter: 'none' },
164
+ }}
165
+ transition={{ duration: 0.5, ease: 'easeOut' }}
166
+ >
167
+ <span className="inline-flex items-center gap-2 rounded-full border border-lime-500 bg-black/70 px-4 py-1.5 text-xs text-lime-100">
168
+ {/* TODO: Reconnect Badge/AnimatedShinyText if this project provides them. */}
169
+ <ChipIcon className="mr-2 h-3.5 w-3.5" />
170
+ {badge.text}
171
+ </span>
172
+ </motion.div>
173
+ )}
174
+
175
+ {/* Headline */}
176
+ {/* <h1 className="text-5xl md:text-6xl lg:text-7xl font-semibold text-stone-900 dark:text-stone-50 leading-[1.05] tracking-tight mb-6">
177
+ {headlineMain}
178
+ <br />
179
+ <span className="text-stone-400">{headlineAccent}</span>
180
+ </h1> */}
181
+
182
+ {/* Headline */}
183
+ <motion.div
184
+ className="relative mt-4 z-50 headline-main"
185
+ variants={{
186
+ hidden: { opacity: 0, y: 40 },
187
+ visible: { opacity: 1, y: 0 },
188
+ }}
189
+ viewport={{ once: false, amount: 0.3 }}
190
+ transition={{ duration: 0.6, ease: 'easeOut' }}
191
+ style={{ filter: 'drop-shadow(3px 3px 1px #151515)' }}
192
+ >
193
+ <span className="block text-6xl sm:text-6xl font-semibold text-left tracking-tight md:text-7xl text-purple-600 dark:text-violet-600 hyphens-auto">
194
+ {/* TODO: Reconnect SplittingText animation if this project provides it. */}
195
+ {headlineMain}
196
+ </span>
197
+ </motion.div>
198
+ <motion.div
199
+ className="relative z-50 headline-accent"
200
+ variants={{
201
+ hidden: { opacity: 0, y: 40 },
202
+ visible: { opacity: 1, y: 0 },
203
+ }}
204
+ viewport={{ once: false, amount: 0.3 }}
205
+ transition={{ duration: 0.6, ease: 'easeOut' }}
206
+ style={{ filter: 'drop-shadow(3px 3px 1px #151515)' }}
207
+ >
208
+ <span className="block text-6xl sm:text-6xl font-semibold text-left tracking-tight md:text-7xl text-purple-900 dark:text-violet-900 hyphens-auto">
209
+ {/* TODO: Reconnect SplittingText animation if this project provides it. */}
210
+ {headlineAccent}
211
+ </span>
212
+ </motion.div>
213
+
214
+ {/* Description */}
215
+ <TextFade direction="down" className="mt-10" delay={0.5}>
216
+ {description && (
217
+ <p className="text-stone-50/65 leading-relaxed mt-4 mb-0 max-w-xl">{description}</p>
218
+ )}
219
+ </TextFade>
220
+
221
+ {/* CTA Buttons */}
222
+ {/* <div className="flex flex-col sm:flex-row gap-3 mx-4">
223
+ {primaryCta?.label && (
224
+ <a
225
+ href={primaryCta.href ?? '#'}
226
+ className="inline-flex items-center justify-center gap-2 bg-stone-50 text-stone-900 px-6 py-3 rounded-lg text-sm font-semibold hover:bg-stone-100 transition-colors cursor-pointer whitespace-nowrap"
227
+ >
228
+ <DynamicIcon className="shrink-0" name={primaryCta.iconName} size={16} />
229
+ {primaryCta.label}
230
+ </a>
231
+ )}
232
+ {secondaryCta?.label && (
233
+ <a
234
+ href={secondaryCta.href ?? '#'}
235
+ className="inline-flex items-center justify-center gap-2 bg-stone-50/10 border border-white/20 text-stone-50 px-6 py-3 rounded-lg text-sm font-semibold hover:bg-stone-50/20 transition-colors cursor-pointer whitespace-nowrap"
236
+ >
237
+ <DynamicIcon className="shrink-0" name={secondaryCta.iconName} size={16} />
238
+ {secondaryCta.label}
239
+ </a>
240
+ )}
241
+ </div> */}
242
+
243
+ {/* Dusted CTA Buttons */}
244
+ {showButtons && buttonItems.length > 0 && (
245
+ <motion.div
246
+ className="dusted-hero-cta-col mt-8 relative flex col-start-2 col-span-2 gap-4"
247
+ variants={{
248
+ hidden: { opacity: 0, y: 20 },
249
+ visible: { opacity: 1, y: 0 },
250
+ }}
251
+ transition={{ duration: 0.6, ease: 'easeOut' }}
252
+ >
253
+ {buttonItems.map((button, index) => (
254
+ <a
255
+ key={index}
256
+ href={(button as any).href ?? '#'}
257
+ className={
258
+ button.class ??
259
+ 'inline-flex items-center gap-1.5 bg-indigo-700 text-stone-50 text-xs font-semibold px-3 py-1.5 rounded-lg group-hover:bg-indigo-600 group-hover:scale-[1.05] group-hover:shadow-xl transition-all duration-150 whitespace-nowrap'
260
+ }
261
+ style={{ cornerShape: 'squircle' } as React.CSSProperties}
262
+ >
263
+ {/* TODO: Reconnect ButtonDustedFluid if this project provides it. */}
264
+ {button.label || 'Button'}
265
+ </a>
266
+ ))}
267
+ </motion.div>
268
+ )}
269
+
270
+ {/* Stats */}
271
+ {statItems.length > 0 && (
272
+ <div className="flex items-center gap-8 mt-24 pt-8 border-t border-white/10">
273
+ {statItems.map((stat, index) => (
274
+ <div key={stat.label ?? index}>
275
+ <div className="text-4xl font-semibold text-stone-100/50 dark:text-stone-50">
276
+ {stat.value}
277
+ </div>
278
+ <div className="text-xs text-stone-50/75 mt-0.5">{stat.label}</div>
279
+ </div>
280
+ ))}
281
+ </div>
282
+ )}
283
+ </div>
284
+ </div>
285
+
286
+ {/* Scroll indicator */}
287
+ {showScrollIndicator && (
288
+ <div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2 animate-bounce">
289
+ <div className="w-5 h-8 border border-white/30 rounded-full flex items-start justify-center pt-1.5">
290
+ <div className="w-1 h-2 bg-stone-50/50 rounded-full" />
291
+ </div>
292
+ </div>
293
+ )}
294
+ </div>
295
+ </>
296
+ )
297
+ }