blocks-dusted 0.1.10 → 0.1.11

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 (246) hide show
  1. package/README.md +35 -0
  2. package/package.json +1 -1
  3. package/src/utils/paths.js +13 -13
  4. package/templates/blocks/DD-Accordion/Component.tsx +229 -0
  5. package/templates/blocks/DD-Accordion/README.md +3 -0
  6. package/templates/blocks/DD-Accordion/config.ts +73 -0
  7. package/templates/blocks/DD-Accordion/manifest.json +75 -0
  8. package/templates/blocks/DD-AnimText/Component.tsx +788 -0
  9. package/templates/blocks/DD-AnimText/README.md +3 -0
  10. package/templates/blocks/DD-AnimText/config.ts +295 -0
  11. package/templates/blocks/DD-AnimText/manifest.json +85 -0
  12. package/templates/blocks/DD-AsideNav/Component.tsx +321 -0
  13. package/templates/blocks/DD-AsideNav/config.ts +204 -0
  14. package/templates/blocks/DD-AsideNav/manifest.json +80 -0
  15. package/templates/blocks/DD-Banner/Component.tsx +51 -0
  16. package/templates/blocks/DD-Banner/config.ts +57 -0
  17. package/templates/blocks/DD-Banner/manifest.json +72 -0
  18. package/templates/blocks/DD-BgLightning/Component.tsx +44 -0
  19. package/templates/blocks/DD-BgLightning/README.md +3 -0
  20. package/templates/blocks/DD-BgLightning/config.ts +97 -0
  21. package/templates/blocks/DD-BgLightning/manifest.json +79 -0
  22. package/templates/blocks/DD-BookCall/Component.tsx +10 -11
  23. package/templates/blocks/DD-BookCall/config.ts +238 -238
  24. package/templates/blocks/DD-ButtonPop/Component.tsx +117 -0
  25. package/templates/blocks/DD-ButtonPop/config.ts +105 -0
  26. package/templates/blocks/DD-ButtonPop/manifest.json +88 -0
  27. package/templates/blocks/DD-ButtonPop/svg-icons/Dust2.tsx +167 -0
  28. package/templates/blocks/DD-Card/Component.tsx +251 -0
  29. package/templates/blocks/DD-Card/README.md +4 -0
  30. package/templates/blocks/DD-Card/config.ts +154 -0
  31. package/templates/blocks/DD-Card/manifest.json +82 -0
  32. package/templates/blocks/DD-CardTemplate01/Component.tsx +5 -6
  33. package/templates/blocks/DD-CardTemplate01/config.ts +106 -106
  34. package/templates/blocks/DD-Carousel-A/Component.tsx +249 -249
  35. package/templates/blocks/DD-Carousel-B/Component.tsx +403 -403
  36. package/templates/blocks/DD-Chip/Component.tsx +250 -0
  37. package/templates/blocks/DD-Chip/EXAMPLES.md +313 -0
  38. package/templates/blocks/DD-Chip/README.md +202 -0
  39. package/templates/blocks/DD-Chip/config.ts +369 -0
  40. package/templates/blocks/DD-Chip/manifest.json +74 -0
  41. package/templates/blocks/DD-ComparisonTable/Component.tsx +256 -256
  42. package/templates/blocks/DD-Contact/Component.tsx +434 -434
  43. package/templates/blocks/DD-Contact/config.ts +22 -22
  44. package/templates/blocks/DD-Counter/Component.tsx +190 -0
  45. package/templates/blocks/DD-Counter/README.md +4 -0
  46. package/templates/blocks/DD-Counter/config.ts +291 -0
  47. package/templates/blocks/DD-Counter/manifest.json +74 -0
  48. package/templates/blocks/DD-CustomCSS/Component.tsx +8 -0
  49. package/templates/blocks/DD-CustomCSS/README.md +4 -0
  50. package/templates/blocks/DD-CustomCSS/addToLayout.md +5 -0
  51. package/templates/blocks/DD-CustomCSS/config.ts +22 -0
  52. package/templates/blocks/DD-CustomCSS/manifest.json +72 -0
  53. package/templates/blocks/DD-CustomCSS/renderCustomCSS.md +7 -0
  54. package/templates/blocks/DD-FeatCat/Component.tsx +302 -302
  55. package/templates/blocks/DD-FeatCat/config.ts +201 -201
  56. package/templates/blocks/DD-FeatStrip/Component.tsx +585 -585
  57. package/templates/blocks/DD-FeatStrip/config.ts +134 -134
  58. package/templates/blocks/DD-FluidBtn/ButtonsEyeCatching.tsx +628 -0
  59. package/templates/blocks/DD-FluidBtn/Component.tsx +43 -0
  60. package/templates/blocks/DD-FluidBtn/README.md +6 -0
  61. package/templates/blocks/DD-FluidBtn/config.ts +102 -0
  62. package/templates/blocks/DD-FluidBtn/manifest.json +86 -0
  63. package/templates/blocks/DD-GooeyInfo/Component.tsx +149 -0
  64. package/templates/blocks/DD-GooeyInfo/config.ts +135 -0
  65. package/templates/blocks/DD-GooeyInfo/manifest.json +72 -0
  66. package/templates/blocks/DD-Hero/Component.tsx +40 -17
  67. package/templates/blocks/DD-Hero/config.ts +267 -267
  68. package/templates/blocks/DD-HorizontalScroll/Component.tsx +4 -2
  69. package/templates/blocks/DD-HorizontalScroll/config.ts +231 -231
  70. package/templates/blocks/DD-Icon/Component.tsx +287 -0
  71. package/templates/blocks/DD-Icon/README.md +4 -0
  72. package/templates/blocks/DD-Icon/config.ts +258 -0
  73. package/templates/blocks/DD-Icon/hooks/revalidateBlockIds.ts +66 -0
  74. package/templates/blocks/DD-Icon/manifest.json +82 -0
  75. package/templates/blocks/DD-IconButton/Component.tsx +455 -0
  76. package/templates/blocks/DD-IconButton/README.md +326 -0
  77. package/templates/blocks/DD-IconButton/config.ts +239 -0
  78. package/templates/blocks/DD-IconButton/manifest.json +85 -0
  79. package/templates/blocks/DD-IdTag/Component.tsx +21 -0
  80. package/templates/blocks/DD-IdTag/README.md +4 -0
  81. package/templates/blocks/DD-IdTag/config.ts +41 -0
  82. package/templates/blocks/DD-IdTag/manifest.json +72 -0
  83. package/templates/blocks/DD-Iframe/Component.tsx +45 -0
  84. package/templates/blocks/DD-Iframe/README.md +4 -0
  85. package/templates/blocks/DD-Iframe/config.ts +47 -0
  86. package/templates/blocks/DD-Iframe/manifest.json +72 -0
  87. package/templates/blocks/DD-IframeMedia/Component.tsx +129 -0
  88. package/templates/blocks/DD-IframeMedia/README.md +4 -0
  89. package/templates/blocks/DD-IframeMedia/config.ts +79 -0
  90. package/templates/blocks/DD-IframeMedia/manifest.json +72 -0
  91. package/templates/blocks/DD-LineAnim/Component.tsx +36 -0
  92. package/templates/blocks/DD-LineAnim/README.md +4 -0
  93. package/templates/blocks/DD-LineAnim/config.ts +90 -0
  94. package/templates/blocks/DD-LineAnim/manifest.json +80 -0
  95. package/templates/blocks/DD-LinkGrid/Component.tsx +119 -0
  96. package/templates/blocks/DD-LinkGrid/README.md +4 -0
  97. package/templates/blocks/DD-LinkGrid/config.ts +51 -0
  98. package/templates/blocks/DD-LinkGrid/manifest.json +74 -0
  99. package/templates/blocks/DD-LogoMarq/Component.tsx +132 -0
  100. package/templates/blocks/DD-LogoMarq/README.md +4 -0
  101. package/templates/blocks/DD-LogoMarq/config.ts +264 -0
  102. package/templates/blocks/DD-LogoMarq/manifest.json +82 -0
  103. package/templates/blocks/DD-Lottie/Component.tsx +459 -0
  104. package/templates/blocks/DD-Lottie/README.md +4 -0
  105. package/templates/blocks/DD-Lottie/config.ts +379 -0
  106. package/templates/blocks/DD-Lottie/index.tsx +28 -0
  107. package/templates/blocks/DD-Lottie/manifest.json +83 -0
  108. package/templates/blocks/DD-MagicBento/Component.tsx +777 -0
  109. package/templates/blocks/DD-MagicBento/README.md +4 -0
  110. package/templates/blocks/DD-MagicBento/config.ts +264 -0
  111. package/templates/blocks/DD-MagicBento/manifest.json +74 -0
  112. package/templates/blocks/DD-MasonaryMedia/Component.tsx +547 -545
  113. package/templates/blocks/DD-MasonaryMedia/config.ts +2 -2
  114. package/templates/blocks/DD-MasonaryMedia/vendor.d.ts +2 -2
  115. package/templates/blocks/DD-Masonry/Component.tsx +645 -0
  116. package/templates/blocks/DD-Masonry/README.md +4 -0
  117. package/templates/blocks/DD-Masonry/config.ts +475 -0
  118. package/templates/blocks/DD-Masonry/manifest.json +82 -0
  119. package/templates/blocks/DD-Pricing/Component.tsx +372 -372
  120. package/templates/blocks/DD-Process/Component.tsx +149 -149
  121. package/templates/blocks/DD-Process/config.ts +150 -150
  122. package/templates/blocks/DD-Progress/Component.tsx +141 -0
  123. package/templates/blocks/DD-Progress/README.md +4 -0
  124. package/templates/blocks/DD-Progress/config.ts +175 -0
  125. package/templates/blocks/DD-Progress/manifest.json +74 -0
  126. package/templates/blocks/DD-ProjHeader/Component.tsx +91 -0
  127. package/templates/blocks/DD-ProjHeader/README.md +4 -0
  128. package/templates/blocks/DD-ProjHeader/config.ts +96 -0
  129. package/templates/blocks/DD-ProjHeader/manifest.json +72 -0
  130. package/templates/blocks/DD-ProjSlider/Component.tsx +123 -0
  131. package/templates/blocks/DD-ProjSlider/README.md +4 -0
  132. package/templates/blocks/DD-ProjSlider/config.ts +64 -0
  133. package/templates/blocks/DD-ProjSlider/manifest.json +72 -0
  134. package/templates/blocks/DD-ScrollText/Component.tsx +114 -0
  135. package/templates/blocks/DD-ScrollText/README.md +4 -0
  136. package/templates/blocks/DD-ScrollText/config.ts +58 -0
  137. package/templates/blocks/DD-ScrollText/manifest.json +72 -0
  138. package/templates/blocks/DD-Section/Component.tsx +266 -266
  139. package/templates/blocks/DD-Separator/Component.tsx +66 -0
  140. package/templates/blocks/DD-Separator/README.md +4 -0
  141. package/templates/blocks/DD-Separator/config.ts +58 -0
  142. package/templates/blocks/DD-Separator/manifest.json +72 -0
  143. package/templates/blocks/DD-Services/Component.tsx +176 -176
  144. package/templates/blocks/DD-Services/config.ts +149 -149
  145. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +307 -307
  146. package/templates/blocks/DD-Slider-A/Component.tsx +237 -237
  147. package/templates/blocks/DD-StackCards/Component.tsx +473 -473
  148. package/templates/blocks/DD-StackCards/config.ts +399 -399
  149. package/templates/blocks/DD-Team/Component.tsx +247 -247
  150. package/templates/blocks/DD-Team/config.ts +173 -173
  151. package/templates/blocks/DD-TeamCards/Component.tsx +251 -0
  152. package/templates/blocks/DD-TeamCards/README.md +4 -0
  153. package/templates/blocks/DD-TeamCards/config.ts +302 -0
  154. package/templates/blocks/DD-TeamCards/manifest.json +74 -0
  155. package/templates/blocks/DD-TechStack/Component.tsx +168 -162
  156. package/templates/blocks/DD-TechStack/config.ts +153 -153
  157. package/templates/blocks/DD-TestMarq/Component.tsx +132 -0
  158. package/templates/blocks/DD-TestMarq/README.md +4 -0
  159. package/templates/blocks/DD-TestMarq/RowLabel.tsx +9 -0
  160. package/templates/blocks/DD-TestMarq/config.ts +88 -0
  161. package/templates/blocks/DD-TestMarq/manifest.json +72 -0
  162. package/templates/blocks/DD-Testim/Component.tsx +247 -0
  163. package/templates/blocks/DD-Testim/README.md +4 -0
  164. package/templates/blocks/DD-Testim/config.ts +101 -0
  165. package/templates/blocks/DD-Testim/manifest.json +75 -0
  166. package/templates/blocks/DD-Testim2/Component.tsx +256 -0
  167. package/templates/blocks/DD-Testim2/README.md +4 -0
  168. package/templates/blocks/DD-Testim2/config.ts +100 -0
  169. package/templates/blocks/DD-Testim2/manifest.json +74 -0
  170. package/templates/blocks/DD-Testimonials/Component.tsx +147 -147
  171. package/templates/blocks/DD-Testimonials/config.ts +120 -120
  172. package/templates/blocks/DD-TextMarq/Component.tsx +157 -0
  173. package/templates/blocks/DD-TextMarq/README.md +4 -0
  174. package/templates/blocks/DD-TextMarq/config.ts +237 -0
  175. package/templates/blocks/DD-TextMarq/manifest.json +82 -0
  176. package/templates/blocks/DD-Thanks/Component.tsx +500 -0
  177. package/templates/blocks/DD-Thanks/README.md +4 -0
  178. package/templates/blocks/DD-Thanks/config.ts +224 -0
  179. package/templates/blocks/DD-Thanks/manifest.json +72 -0
  180. package/templates/blocks/DD-VertMarq/Component.tsx +154 -0
  181. package/templates/blocks/DD-VertMarq/README.md +4 -0
  182. package/templates/blocks/DD-VertMarq/config.ts +236 -0
  183. package/templates/blocks/DD-VertMarq/dist/index.css +6 -0
  184. package/templates/blocks/DD-VertMarq/dist/index.min.css +1 -0
  185. package/templates/blocks/DD-VertMarq/manifest.json +74 -0
  186. package/templates/blocks/DD-Work/Component.tsx +318 -315
  187. package/templates/blocks/DD-Work/config.ts +165 -165
  188. package/templates/blocks/DD-Work-B/Component.tsx +294 -294
  189. package/templates/blocks/DDHG-CTASection2/Component.tsx +185 -0
  190. package/templates/blocks/DDHG-CTASection2/config.ts +140 -0
  191. package/templates/blocks/DDHG-CTASection2/manifest.json +45 -0
  192. package/templates/blocks/DDHG-HighlightArc/Component.tsx +243 -0
  193. package/templates/blocks/DDHG-HighlightArc/config.ts +85 -0
  194. package/templates/blocks/DDHG-HighlightArc/manifest.json +46 -0
  195. package/templates/blocks/DDHG-LogoMarquee/Component.tsx +93 -0
  196. package/templates/blocks/DDHG-LogoMarquee/config.ts +66 -0
  197. package/templates/blocks/DDHG-LogoMarquee/manifest.json +39 -0
  198. package/templates/blocks/DDHG-NavCodeGrid/Component.tsx +213 -0
  199. package/templates/blocks/DDHG-NavCodeGrid/config.ts +64 -0
  200. package/templates/blocks/DDHG-NavCodeGrid/manifest.json +45 -0
  201. package/templates/blocks/DDHG-ProcessSec/Component.tsx +575 -0
  202. package/templates/blocks/DDHG-ProcessSec/config.ts +170 -0
  203. package/templates/blocks/DDHG-ProcessSec/manifest.json +56 -0
  204. package/templates/blocks/DDHG-ServiceDetails/Component.tsx +164 -0
  205. package/templates/blocks/DDHG-ServiceDetails/ServiceDetailCard.tsx +394 -0
  206. package/templates/blocks/DDHG-ServiceDetails/config.ts +77 -0
  207. package/templates/blocks/DDHG-ServiceDetails/manifest.json +50 -0
  208. package/templates/blocks/DDHG-ServiceHero/Component.tsx +234 -0
  209. package/templates/blocks/DDHG-ServiceHero/config.ts +49 -0
  210. package/templates/blocks/DDHG-ServiceHero/manifest.json +26 -0
  211. package/templates/blocks/DDHG-ServiceOverview/Component.tsx +277 -0
  212. package/templates/blocks/DDHG-ServiceOverview/config.ts +64 -0
  213. package/templates/blocks/DDHG-ServiceOverview/manifest.json +44 -0
  214. package/templates/blocks/DDHG-StatsFAQ/Component.tsx +389 -0
  215. package/templates/blocks/DDHG-StatsFAQ/config.ts +204 -0
  216. package/templates/blocks/DDHG-StatsFAQ/manifest.json +45 -0
  217. package/templates/blocks/DDHG-TeamCards/Component.tsx +131 -0
  218. package/templates/blocks/DDHG-TeamCards/config.ts +48 -0
  219. package/templates/blocks/DDHG-TeamCards/manifest.json +29 -0
  220. package/templates/shared/src/components/BgLightning/index.tsx +242 -0
  221. package/templates/shared/src/components/ui/ShareButton/index.tsx +80 -0
  222. package/templates/shared/src/components/ui/anims/FadeOnScroll.tsx +48 -0
  223. package/templates/shared/src/components/ui/anims/FadeUpOnInView.tsx +63 -0
  224. package/templates/shared/src/components/ui/anims/GlareHover.tsx +111 -0
  225. package/templates/shared/src/components/ui/anims/LineAnims.tsx +81 -0
  226. package/templates/shared/src/components/ui/anims/TitleAnims01.tsx +54 -0
  227. package/templates/shared/src/components/ui/anims/animation-container.tsx +29 -0
  228. package/templates/shared/src/components/ui/anims/blur-in.tsx +26 -0
  229. package/templates/shared/src/components/ui/anims/counter.tsx +53 -0
  230. package/templates/shared/src/components/ui/anims/fade.tsx +83 -0
  231. package/templates/shared/src/components/ui/anims/gradual-spacing.tsx +39 -0
  232. package/templates/shared/src/components/ui/anims/pull-up.tsx +93 -0
  233. package/templates/shared/src/components/ui/anims/reveal-text.tsx +76 -0
  234. package/templates/shared/src/components/ui/anims/rotate-words.tsx +43 -0
  235. package/templates/shared/src/components/ui/anims/shiny-text.tsx +58 -0
  236. package/templates/shared/src/components/ui/anims/shuffle-text.tsx +85 -0
  237. package/templates/shared/src/components/ui/anims/split-text.tsx +211 -0
  238. package/templates/shared/src/components/ui/anims/staggered-fade.tsx +133 -0
  239. package/templates/shared/src/components/ui/anims/text-scroll.tsx +111 -0
  240. package/templates/shared/src/components/ui/anims/typing-effect.tsx +112 -0
  241. package/templates/shared/src/components/ui/button-dusted-fluid/index.tsx +1521 -0
  242. package/templates/shared/src/components/ui/separator.tsx +85 -0
  243. package/templates/shared/src/hooks/useBodyScrollLock.ts +97 -0
  244. package/templates/shared/src/hooks/useClickOutside.ts +28 -0
  245. package/templates/shared/src/utilities/renderCustomCSS.ts +5 -0
  246. package/templates/shared/src/utilities/smoothScroll.ts +65 -0
@@ -1,545 +1,547 @@
1
- 'use client'
2
-
3
- import React, { useEffect, useMemo, useRef } from 'react'
4
- import Link from 'next/link'
5
- import { motion, type Variants } from 'framer-motion'
6
- import { cn } from '@/utilities/ui'
7
-
8
- type MediaLike = {
9
- url?: string | null
10
- alt?: string | null
11
- mimeType?: string | null
12
- }
13
-
14
- type ShowcaseItem = {
15
- id?: string | null
16
- title?: string | null
17
- category?: string | null
18
- framework?: string | null
19
- description?: string | null
20
- href?: string | null
21
- linkLabel?: string | null
22
- free?: boolean | null
23
- priceLabel?: string | null
24
- aspectRatio?: '16/9' | '3/2' | '1/1' | '2/3' | string | null
25
- image?: number | string | MediaLike | null
26
- hoverVideo?: number | string | MediaLike | null
27
- posterFallbackUrl?: string | null
28
- videoFallbackUrl?: string | null
29
- }
30
-
31
- type DDMasonaryMediaProps = {
32
- eyebrow?: string | null
33
- headlineMain?: string | null
34
- headlineAccent?: string | null
35
- subtext?: string | null
36
- items?: ShowcaseItem[] | null
37
- columnsDesktop?: number | null
38
- columnsTablet?: number | null
39
- gapClass?: string | null
40
- containerClassName?: string | null
41
- sectionClassName?: string | null
42
- sectionId?: string | null
43
- customCSS?: string | null
44
- showCta?: boolean | null
45
- ctaAlign?: 'left' | 'center' | 'right' | null
46
- ctaDisplayMode?: 'iconWithLabel' | 'iconOnly' | 'labelOnly' | null
47
- ctaIconPosition?: 'before' | 'after' | null
48
- ctaButtonText?: string | null
49
- ctaButtonStyle?: string | null
50
- ctaSize?: 'small' | 'medium' | 'large' | null
51
- ctaIcon?: string | null
52
- ctaLink?: any | null
53
- ctaContainerClass?: string | null
54
- }
55
-
56
- const getMediaUrl = (media?: number | string | MediaLike | null, fallback?: string | null) => {
57
- if (media && typeof media === 'object' && typeof media.url === 'string') return media.url
58
- if (typeof media === 'string') return media
59
- return fallback || ''
60
- }
61
-
62
- const getMediaAlt = (media?: number | string | MediaLike | null, fallback = '') => {
63
- if (media && typeof media === 'object' && typeof media.alt === 'string') return media.alt
64
- return fallback
65
- }
66
-
67
- const getMediaMimeType = (media?: number | string | MediaLike | null) => {
68
- if (media && typeof media === 'object' && typeof media.mimeType === 'string')
69
- return media.mimeType
70
- return ''
71
- }
72
-
73
- const getFileExtension = (url: string) => {
74
- const clean = url.split('?')[0]?.split('#')[0] || ''
75
- return clean.split('.').pop()?.toLowerCase() || ''
76
- }
77
-
78
- const isVideoMedia = (url: string, mimeType?: string | null) => {
79
- const ext = getFileExtension(url)
80
-
81
- return Boolean(
82
- mimeType?.startsWith('video/') ||
83
- ext === 'webm' ||
84
- ext === 'mp4' ||
85
- ext === 'mov' ||
86
- ext === 'm4v',
87
- )
88
- }
89
-
90
- const isWideCard = (ratio?: string | null) => ratio === '16/9' || ratio === '3/2'
91
- const isSquareCard = (ratio?: string | null) => ratio === '1/1'
92
-
93
- const getGapPx = (gapClass?: string | null) => {
94
- if (!gapClass) return 20
95
- if (gapClass.includes('gap-0')) return 0
96
- if (gapClass.includes('gap-1')) return 4
97
- if (gapClass.includes('gap-2')) return 8
98
- if (gapClass.includes('gap-3')) return 12
99
- if (gapClass.includes('gap-4')) return 16
100
- if (gapClass.includes('gap-5')) return 20
101
- if (gapClass.includes('gap-6')) return 24
102
- if (gapClass.includes('gap-8')) return 32
103
- if (gapClass.includes('gap-10')) return 40
104
- return 20
105
- }
106
-
107
- const parseAspectRatio = (ratio?: string | null) => {
108
- if (!ratio || !ratio.includes('/')) return '1 / 1'
109
-
110
- const [rawWidth, rawHeight] = ratio.split('/').map(Number)
111
- const width = rawWidth && !Number.isNaN(rawWidth) ? rawWidth : 1
112
- const height = rawHeight && !Number.isNaN(rawHeight) ? rawHeight : 1
113
-
114
- return `${width} / ${height}`
115
- }
116
-
117
- const containerVariants: Variants = {
118
- hidden: {},
119
- show: {
120
- transition: {
121
- staggerChildren: 0.08,
122
- delayChildren: 0.06,
123
- },
124
- },
125
- }
126
-
127
- const itemVariants: Variants = {
128
- hidden: {
129
- opacity: 0,
130
- y: 22,
131
- filter: 'blur(6px)',
132
- },
133
- show: {
134
- opacity: 1,
135
- y: 0,
136
- filter: 'none',
137
- transition: {
138
- duration: 0.45,
139
- ease: 'easeOut',
140
- },
141
- },
142
- }
143
-
144
- function StudioMasonaryCard({ item, index }: { item: ShowcaseItem; index: number }) {
145
- const videoRef = useRef<HTMLVideoElement | null>(null)
146
-
147
- const imageUrl = getMediaUrl(item.image, item.posterFallbackUrl)
148
- const imageAlt = getMediaAlt(item.image, item.title || 'Studio preview')
149
-
150
- const hoverUrl = getMediaUrl(item.hoverVideo, item.videoFallbackUrl)
151
- const hoverMimeType = getMediaMimeType(item.hoverVideo)
152
- const hoverIsVideo = isVideoMedia(hoverUrl, hoverMimeType)
153
- const hoverIsImage = Boolean(hoverUrl && !hoverIsVideo)
154
-
155
- const href = item.href || '#'
156
- const aspectRatio = parseAspectRatio(item.aspectRatio || '16/9')
157
- const wide = isWideCard(item.aspectRatio)
158
- const square = isSquareCard(item.aspectRatio)
159
- const badgeLabel = item.category || (item.free ? 'Free' : item.priceLabel || '')
160
-
161
- const handleMouseEnter = () => {
162
- const video = videoRef.current
163
- if (!video || !hoverIsVideo) return
164
-
165
- if (video.readyState === 0) video.load()
166
-
167
- video.currentTime = 0
168
- const playPromise = video.play()
169
-
170
- if (playPromise && typeof playPromise.catch === 'function') {
171
- playPromise.catch(() => undefined)
172
- }
173
- }
174
-
175
- const handleMouseLeave = () => {
176
- const video = videoRef.current
177
- if (!video) return
178
-
179
- video.pause()
180
- video.currentTime = 0
181
- }
182
-
183
- return (
184
- <article
185
- className={cn('studio-masonry-item card-item', wide && 'is-wide', square && 'is-square')}
186
- data-index={index}
187
- onMouseEnter={handleMouseEnter}
188
- onMouseLeave={handleMouseLeave}
189
- >
190
- <Link href={href} className="block h-full focus-visible:outline-none">
191
- <article
192
- data-theme="dark"
193
- className="masonary-article-card outline-2 outline-stone-500/50 outline-solid group relative h-full w-full overflow-clip rounded-[3rem]! border border-border bg-[#55526b85]! backdrop-blur-xl! shadow-2xl transition-all duration-300 hover:cursor-pointer hover:border-lime-500 hover:shadow-2xl"
194
- style={{ cornerShape: 'squircle', aspectRatio } as React.CSSProperties}
195
- >
196
- <div
197
- className="card-media relative w-full overflow-hidden rounded-[3rem]! border border-border bg-linear-to-br from-violet-950 to-indigo-950 shadow-2xl!"
198
- style={{ cornerShape: 'squircle', aspectRatio } as React.CSSProperties}
199
- >
200
- {imageUrl ? (
201
- <img
202
- src={imageUrl}
203
- alt={imageAlt}
204
- loading="lazy"
205
- decoding="async"
206
- className={cn(
207
- 'absolute inset-0 h-full w-full object-cover object-top rounded-[3rem]! transition-all duration-500 group-hover:scale-105',
208
- hoverUrl && 'group-hover:opacity-0',
209
- )}
210
- style={{ cornerShape: 'squircle' } as React.CSSProperties}
211
- />
212
- ) : (
213
- <div className="absolute inset-0 flex items-center justify-center bg-linear-to-br from-violet-950 to-indigo-950 text-xs text-stone-500">
214
- Preview image
215
- </div>
216
- )}
217
-
218
- {hoverUrl && hoverIsVideo && (
219
- <video
220
- ref={videoRef}
221
- src={hoverUrl}
222
- muted
223
- loop
224
- playsInline
225
- preload="metadata"
226
- className="absolute inset-0 h-full w-full object-cover object-top rounded-[3rem]! opacity-0 transition-all duration-500 group-hover:scale-105 group-hover:opacity-100"
227
- style={{ cornerShape: 'squircle' } as React.CSSProperties}
228
- />
229
- )}
230
-
231
- {hoverUrl && hoverIsImage && (
232
- <img
233
- src={hoverUrl}
234
- alt=""
235
- loading="lazy"
236
- decoding="async"
237
- className="absolute inset-0 h-full w-full object-cover object-top !rounded-4xl opacity-0 transition-all duration-500 group-hover:scale-105 group-hover:opacity-100"
238
- style={{ cornerShape: 'squircle' } as React.CSSProperties}
239
- />
240
- )}
241
-
242
- <div
243
- className="pointer-events-none absolute inset-0 z-10 transition-opacity duration-300 group-hover:opacity-90"
244
- style={{
245
- background:
246
- 'linear-gradient(180deg, rgba(0,0,0,0.62) 0%, rgba(0,0,0,0.08) 32%, rgba(0,0,0,0.12) 52%, rgba(0,0,0,0.88) 100%)',
247
- }}
248
- />
249
-
250
- <div className="pointer-events-none absolute inset-0 z-20 flex flex-col justify-between p-4 sm:p-4 md:p-4">
251
- {badgeLabel ? (
252
- <div className="flex items-start justify-start">
253
- <span
254
- className="inline-flex rounded-full border border-lime-500/80 bg-lime-500/15 px-2 py-1 text-[11.5px] font-medium tracking-widest text-stone-50 backdrop-blur-md sm:px-2 sm:py-1"
255
- style={{ boxShadow: '0 0 24px rgba(132,204,22,0.35)' }}
256
- >
257
- {badgeLabel}
258
- </span>
259
- </div>
260
- ) : (
261
- <span />
262
- )}
263
-
264
- {item.title && (
265
- <h3
266
- className="max-w-[86%] text-lg font-semibold tracking-[0.08em] text-stone-50"
267
- style={{ textShadow: '0 4px 16px rgba(0,0,0,0.9)' }}
268
- >
269
- {item.title}
270
- </h3>
271
- )}
272
- </div>
273
- </div>
274
- </article>
275
- </Link>
276
- </article>
277
- )
278
- }
279
-
280
- export const DDMasonaryMedia: React.FC<DDMasonaryMediaProps> = ({
281
- eyebrow = 'Featured systems',
282
- headlineMain = 'Components, blocks, and templates',
283
- headlineAccent = 'built to feel alive',
284
- subtext = 'A curated showcase of production-ready interface pieces, with static previews and motion demos on hover.',
285
- items = [],
286
- columnsDesktop = 12,
287
- columnsTablet = 2,
288
- gapClass = 'gap-5',
289
- containerClassName = 'max-w-7xl mx-auto px-4 md:px-6',
290
- sectionClassName = 'py-24',
291
- sectionId = 'studio-masonary-media',
292
- customCSS,
293
- showCta = false,
294
- ctaAlign = 'center',
295
- ctaDisplayMode = 'iconWithLabel',
296
- ctaIconPosition = 'before',
297
- ctaButtonText,
298
- ctaButtonStyle = 'primary',
299
- ctaSize = 'medium',
300
- ctaIcon,
301
- ctaLink,
302
- ctaContainerClass,
303
- }) => {
304
- const gridRef = useRef<HTMLDivElement | null>(null)
305
- const isotopeRef = useRef<any>(null)
306
-
307
- const safeSectionId = sectionId || 'studio-masonary-media'
308
- const gap = useMemo(() => getGapPx(gapClass), [gapClass])
309
- const desktopColumns = Math.min(Math.max(columnsDesktop || 6, 5), 6)
310
- const tabletColumns = Math.min(Math.max(columnsTablet || 2, 1), 3)
311
-
312
- useEffect(() => {
313
- if (!gridRef.current || !items || items.length === 0) return
314
-
315
- let destroyed = false
316
-
317
- const init = async () => {
318
- const Isotope = (await import('isotope-layout')).default
319
- const imagesLoaded = (await import('imagesloaded')).default
320
-
321
- if (!gridRef.current || destroyed) return
322
-
323
- const iso = new Isotope(gridRef.current, {
324
- itemSelector: '.studio-masonry-item',
325
- percentPosition: true,
326
- transitionDuration: '0.35s',
327
- masonry: {
328
- columnWidth: '.studio-masonry-sizer',
329
- gutter: '.studio-masonry-gutter',
330
- fitWidth: false,
331
- },
332
- })
333
-
334
- isotopeRef.current = iso
335
-
336
- imagesLoaded(gridRef.current, () => {
337
- if (!destroyed) iso.layout()
338
- })
339
-
340
- window.setTimeout(() => {
341
- if (!destroyed) iso.layout()
342
- }, 250)
343
- }
344
-
345
- init()
346
-
347
- const handleResize = () => {
348
- isotopeRef.current?.layout()
349
- }
350
-
351
- window.addEventListener('resize', handleResize)
352
-
353
- return () => {
354
- destroyed = true
355
- window.removeEventListener('resize', handleResize)
356
- isotopeRef.current?.destroy()
357
- isotopeRef.current = null
358
- }
359
- }, [items])
360
-
361
- useEffect(() => {
362
- isotopeRef.current?.reloadItems()
363
- isotopeRef.current?.layout()
364
- }, [items, columnsDesktop, columnsTablet, gap])
365
-
366
- if (!items || items.length === 0) return null
367
-
368
- return (
369
- <>
370
- <style
371
- dangerouslySetInnerHTML={{
372
- __html: `
373
- #${safeSectionId} .studio-masonry-grid {
374
- --studio-masonry-gap: ${gap}px;
375
- --studio-masonry-cols: 1;
376
- position: relative;
377
- width: 100%;
378
- }
379
-
380
- #${safeSectionId} .studio-masonry-sizer,
381
- #${safeSectionId} .studio-masonry-item {
382
- width: 100%;
383
- }
384
-
385
- #${safeSectionId} .studio-masonry-gutter {
386
- width: var(--studio-masonry-gap);
387
- }
388
-
389
- #${safeSectionId} .studio-masonry-item {
390
- margin-bottom: var(--studio-masonry-gap);
391
- will-change: transform;
392
- }
393
-
394
- @media (max-width: 639px) {
395
- #${safeSectionId} .studio-masonry-grid {
396
- --studio-masonry-cols: 2;
397
- }
398
-
399
- #${safeSectionId} .studio-masonry-sizer,
400
- #${safeSectionId} .studio-masonry-item.is-square {
401
- width: calc(
402
- (100% - ((var(--studio-masonry-cols) - 1) * var(--studio-masonry-gap))) /
403
- var(--studio-masonry-cols)
404
- );
405
- }
406
-
407
- #${safeSectionId} .studio-masonry-item:not(.is-square) {
408
- width: 100%;
409
- }
410
-
411
- #${safeSectionId} .studio-masonry-item.is-square .card-media {
412
- aspect-ratio: 1 / 1 !important;
413
- }
414
-
415
- #${safeSectionId} .studio-masonry-item:not(.is-square) .card-media {
416
- aspect-ratio: 16 / 9 !important;
417
- }
418
- }
419
-
420
- @media (min-width: 640px) {
421
- #${safeSectionId} .studio-masonry-grid {
422
- --studio-masonry-cols: ${tabletColumns};
423
- }
424
-
425
- #${safeSectionId} .studio-masonry-sizer,
426
- #${safeSectionId} .studio-masonry-item {
427
- width: calc(
428
- (100% - ((var(--studio-masonry-cols) - 1) * var(--studio-masonry-gap))) /
429
- var(--studio-masonry-cols)
430
- );
431
- }
432
-
433
- #${safeSectionId} .studio-masonry-item.is-wide {
434
- width: calc(
435
- (
436
- (100% - ((var(--studio-masonry-cols) - 1) * var(--studio-masonry-gap))) /
437
- var(--studio-masonry-cols)
438
- ) *
439
- 2 + var(--studio-masonry-gap)
440
- );
441
- }
442
- }
443
-
444
- @media (min-width: 1024px) {
445
- #${safeSectionId} .studio-masonry-grid {
446
- --studio-masonry-cols: ${desktopColumns};
447
- }
448
- }
449
-
450
- @media (min-width: 640px) and (max-width: 1023px) {
451
- #${safeSectionId} .studio-masonry-item.is-wide {
452
- width: 100%;
453
- }
454
- }
455
-
456
- #${safeSectionId} .card-media {
457
- min-height: 10rem;
458
- scale: 1;
459
- }
460
- `,
461
- }}
462
- />
463
-
464
- {customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
465
-
466
- <section id={safeSectionId} className={cn('studio-masonary-media', sectionClassName)}>
467
- <div className={cn(containerClassName)}>
468
- #BlockName-DDMasonaryMedia
469
- <motion.div
470
- variants={containerVariants}
471
- initial="hidden"
472
- whileInView="show"
473
- viewport={{ once: false, amount: 0.2 }}
474
- className="mb-14 flex flex-col justify-between gap-6 overflow-hidden lg:flex-row lg:items-end"
475
- >
476
- <motion.div variants={itemVariants}>
477
- {eyebrow && (
478
- <p className="top-caption mb-3 text-[11.5px] font-semibold uppercase tracking-widest text-stone-400">
479
- {eyebrow}
480
- </p>
481
- )}
482
-
483
- {(headlineMain || headlineAccent) && (
484
- <h2 className="text-3d text-4xl font-bold leading-tight text-stone-900 dark:text-stone-50 md:text-5xl">
485
- {headlineMain}
486
-
487
- {headlineAccent && (
488
- <>
489
- <br />
490
- <span className="text-stone-400">{headlineAccent}</span>
491
- </>
492
- )}
493
- </h2>
494
- )}
495
- </motion.div>
496
-
497
- {subtext && (
498
- <motion.p
499
- variants={itemVariants}
500
- className="max-w-sm text-base leading-relaxed text-stone-400 dark:text-stone-300 lg:text-right"
501
- >
502
- {subtext}
503
- </motion.p>
504
- )}
505
- </motion.div>
506
-
507
- <div ref={gridRef} className="studio-masonry-grid">
508
- <div className="studio-masonry-sizer" />
509
- <div className="studio-masonry-gutter" />
510
-
511
- {items.map((item, index) => (
512
- <StudioMasonaryCard
513
- key={item.id ?? `${item.title}-${index}`}
514
- item={item}
515
- index={index}
516
- />
517
- ))}
518
- </div>
519
- </div>
520
- {showCta && (
521
- <div
522
- className={cn(
523
- 'mt-10 flex',
524
- ctaAlign === 'left' && 'justify-start',
525
- ctaAlign === 'center' && 'justify-center',
526
- ctaAlign === 'right' && 'justify-end',
527
- ctaContainerClass,
528
- )}
529
- >
530
- <a
531
- href={(ctaLink as any)?.url ?? '#'}
532
- className="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"
533
- style={{ cornerShape: 'squircle' } as React.CSSProperties}
534
- >
535
- {/* TODO: Reconnect ButtonWithIcon if this project provides it. */}
536
- {ctaButtonText ?? (ctaLink as any)?.label ?? 'Learn more'}
537
- </a>
538
- </div>
539
- )}
540
- </section>
541
- </>
542
- )
543
- }
544
-
545
- export default DDMasonaryMedia
1
+ 'use client'
2
+
3
+ import React, { useEffect, useMemo, useRef } from 'react'
4
+ import Link from 'next/link'
5
+ import { motion, type Variants } from 'framer-motion'
6
+ import { cn } from '@/utilities/ui'
7
+
8
+ type MediaLike = {
9
+ url?: string | null
10
+ alt?: string | null
11
+ mimeType?: string | null
12
+ }
13
+
14
+ type ShowcaseItem = {
15
+ id?: string | null
16
+ title?: string | null
17
+ category?: string | null
18
+ framework?: string | null
19
+ description?: string | null
20
+ href?: string | null
21
+ linkLabel?: string | null
22
+ free?: boolean | null
23
+ priceLabel?: string | null
24
+ aspectRatio?: '16/9' | '3/2' | '1/1' | '2/3' | string | null
25
+ image?: number | string | MediaLike | null
26
+ hoverVideo?: number | string | MediaLike | null
27
+ posterFallbackUrl?: string | null
28
+ videoFallbackUrl?: string | null
29
+ }
30
+
31
+ type DDMasonaryMediaProps = {
32
+ eyebrow?: string | null
33
+ headlineMain?: string | null
34
+ headlineAccent?: string | null
35
+ subtext?: string | null
36
+ items?: ShowcaseItem[] | null
37
+ columnsDesktop?: number | null
38
+ columnsTablet?: number | null
39
+ gapClass?: string | null
40
+ containerClassName?: string | null
41
+ sectionClassName?: string | null
42
+ sectionId?: string | null
43
+ customCSS?: string | null
44
+ showCta?: boolean | null
45
+ ctaAlign?: 'left' | 'center' | 'right' | null
46
+ ctaDisplayMode?: 'iconWithLabel' | 'iconOnly' | 'labelOnly' | null
47
+ ctaIconPosition?: 'before' | 'after' | null
48
+ ctaButtonText?: string | null
49
+ ctaButtonStyle?: string | null
50
+ ctaSize?: 'small' | 'medium' | 'large' | null
51
+ ctaIcon?: string | null
52
+ ctaLink?: any | null
53
+ ctaContainerClass?: string | null
54
+ }
55
+
56
+ const getMediaUrl = (media?: number | string | MediaLike | null, fallback?: string | null) => {
57
+ if (media && typeof media === 'object' && typeof media.url === 'string') return media.url
58
+ if (typeof media === 'string') return media
59
+ return fallback || ''
60
+ }
61
+
62
+ const getMediaAlt = (media?: number | string | MediaLike | null, fallback = '') => {
63
+ if (media && typeof media === 'object' && typeof media.alt === 'string') return media.alt
64
+ return fallback
65
+ }
66
+
67
+ const getMediaMimeType = (media?: number | string | MediaLike | null) => {
68
+ if (media && typeof media === 'object' && typeof media.mimeType === 'string')
69
+ return media.mimeType
70
+ return ''
71
+ }
72
+
73
+ const getFileExtension = (url: string) => {
74
+ const clean = url.split('?')[0]?.split('#')[0] || ''
75
+ return clean.split('.').pop()?.toLowerCase() || ''
76
+ }
77
+
78
+ const isVideoMedia = (url: string, mimeType?: string | null) => {
79
+ const ext = getFileExtension(url)
80
+
81
+ return Boolean(
82
+ mimeType?.startsWith('video/') ||
83
+ ext === 'webm' ||
84
+ ext === 'mp4' ||
85
+ ext === 'mov' ||
86
+ ext === 'm4v',
87
+ )
88
+ }
89
+
90
+ const isWideCard = (ratio?: string | null) => ratio === '16/9' || ratio === '3/2'
91
+ const isSquareCard = (ratio?: string | null) => ratio === '1/1'
92
+
93
+ const getGapPx = (gapClass?: string | null) => {
94
+ if (!gapClass) return 20
95
+ if (gapClass.includes('gap-0')) return 0
96
+ if (gapClass.includes('gap-1')) return 4
97
+ if (gapClass.includes('gap-2')) return 8
98
+ if (gapClass.includes('gap-3')) return 12
99
+ if (gapClass.includes('gap-4')) return 16
100
+ if (gapClass.includes('gap-5')) return 20
101
+ if (gapClass.includes('gap-6')) return 24
102
+ if (gapClass.includes('gap-8')) return 32
103
+ if (gapClass.includes('gap-10')) return 40
104
+ return 20
105
+ }
106
+
107
+ const parseAspectRatio = (ratio?: string | null) => {
108
+ if (!ratio || !ratio.includes('/')) return '1 / 1'
109
+
110
+ const [rawWidth, rawHeight] = ratio.split('/').map(Number)
111
+ const width = rawWidth && !Number.isNaN(rawWidth) ? rawWidth : 1
112
+ const height = rawHeight && !Number.isNaN(rawHeight) ? rawHeight : 1
113
+
114
+ return `${width} / ${height}`
115
+ }
116
+
117
+ const containerVariants: Variants = {
118
+ hidden: {},
119
+ show: {
120
+ transition: {
121
+ staggerChildren: 0.08,
122
+ delayChildren: 0.06,
123
+ },
124
+ },
125
+ }
126
+
127
+ const itemVariants: Variants = {
128
+ hidden: {
129
+ opacity: 0,
130
+ y: 22,
131
+ filter: 'blur(6px)',
132
+ },
133
+ show: {
134
+ opacity: 1,
135
+ y: 0,
136
+ filter: 'none',
137
+ transition: {
138
+ duration: 0.45,
139
+ ease: 'easeOut',
140
+ },
141
+ },
142
+ }
143
+
144
+ function StudioMasonaryCard({ item, index }: { item: ShowcaseItem; index: number }) {
145
+ const videoRef = useRef<HTMLVideoElement | null>(null)
146
+
147
+ const imageUrl = getMediaUrl(item.image, item.posterFallbackUrl)
148
+ const imageAlt = getMediaAlt(item.image, item.title || 'Studio preview')
149
+
150
+ const hoverUrl = getMediaUrl(item.hoverVideo, item.videoFallbackUrl)
151
+ const hoverMimeType = getMediaMimeType(item.hoverVideo)
152
+ const hoverIsVideo = isVideoMedia(hoverUrl, hoverMimeType)
153
+ const hoverIsImage = Boolean(hoverUrl && !hoverIsVideo)
154
+
155
+ const href = item.href || '#'
156
+ const aspectRatio = parseAspectRatio(item.aspectRatio || '16/9')
157
+ const wide = isWideCard(item.aspectRatio)
158
+ const square = isSquareCard(item.aspectRatio)
159
+ const badgeLabel = item.category || (item.free ? 'Free' : item.priceLabel || '')
160
+
161
+ const handleMouseEnter = () => {
162
+ const video = videoRef.current
163
+ if (!video || !hoverIsVideo) return
164
+
165
+ if (video.readyState === 0) video.load()
166
+
167
+ video.currentTime = 0
168
+ const playPromise = video.play()
169
+
170
+ if (playPromise && typeof playPromise.catch === 'function') {
171
+ playPromise.catch(() => undefined)
172
+ }
173
+ }
174
+
175
+ const handleMouseLeave = () => {
176
+ const video = videoRef.current
177
+ if (!video) return
178
+
179
+ video.pause()
180
+ video.currentTime = 0
181
+ }
182
+
183
+ return (
184
+ <article
185
+ className={cn('studio-masonry-item card-item', wide && 'is-wide', square && 'is-square')}
186
+ data-index={index}
187
+ onMouseEnter={handleMouseEnter}
188
+ onMouseLeave={handleMouseLeave}
189
+ >
190
+ <Link href={href} className="block h-full focus-visible:outline-none">
191
+ <article
192
+ data-theme="dark"
193
+ className="masonary-article-card outline-2 outline-stone-500/50 outline-solid group relative h-full w-full overflow-clip !rounded-[3rem] border border-border bg-[#55526b85]! backdrop-blur-xl! shadow-2xl transition-all duration-300 hover:cursor-pointer hover:border-lime-500 hover:shadow-2xl [corner-shape:squircle]"
194
+ style={{ cornerShape: 'squircle', aspectRatio } as React.CSSProperties}
195
+ >
196
+ <div
197
+ className="card-media relative w-full overflow-hidden !rounded-[3rem] border border-border bg-linear-to-br from-violet-950 to-indigo-950 shadow-2xl! [corner-shape:squircle]"
198
+ style={{ cornerShape: 'squircle', aspectRatio } as React.CSSProperties}
199
+ >
200
+ {imageUrl ? (
201
+ <img
202
+ src={imageUrl}
203
+ alt={imageAlt}
204
+ loading="lazy"
205
+ decoding="async"
206
+ className={cn(
207
+ 'absolute inset-0 h-full w-full object-cover object-top !rounded-[3rem] transition-all duration-500 group-hover:scale-105',
208
+ hoverUrl && 'group-hover:opacity-0',
209
+ )}
210
+ style={{ cornerShape: 'squircle' } as React.CSSProperties}
211
+ />
212
+ ) : (
213
+ <div className="absolute inset-0 flex items-center justify-center bg-linear-to-br from-violet-950 to-indigo-950 text-xs text-stone-500">
214
+ Preview image
215
+ </div>
216
+ )}
217
+
218
+ {hoverUrl && hoverIsVideo && (
219
+ <video
220
+ ref={videoRef}
221
+ src={hoverUrl}
222
+ muted
223
+ loop
224
+ playsInline
225
+ preload="metadata"
226
+ className="absolute inset-0 h-full w-full object-cover object-top !rounded-[3rem] opacity-0 transition-all duration-500 group-hover:scale-105 group-hover:opacity-100"
227
+ style={{ cornerShape: 'squircle' } as React.CSSProperties}
228
+ />
229
+ )}
230
+
231
+ {hoverUrl && hoverIsImage && (
232
+ <img
233
+ src={hoverUrl}
234
+ alt=""
235
+ loading="lazy"
236
+ decoding="async"
237
+ className="absolute inset-0 h-full w-full object-cover object-top !rounded-[3rem] opacity-0 transition-all duration-500 group-hover:scale-105 group-hover:opacity-100"
238
+ style={{ cornerShape: 'squircle' } as React.CSSProperties}
239
+ />
240
+ )}
241
+
242
+ <div
243
+ className="pointer-events-none absolute inset-0 z-10 transition-opacity duration-300 group-hover:opacity-90"
244
+ style={{
245
+ background:
246
+ 'linear-gradient(180deg, rgba(0,0,0,0.62) 0%, rgba(0,0,0,0.08) 32%, rgba(0,0,0,0.12) 52%, rgba(0,0,0,0.88) 100%)',
247
+ }}
248
+ />
249
+
250
+ <div className="pointer-events-none absolute inset-0 z-20 flex flex-col justify-between p-4 sm:p-4 md:p-4">
251
+ {badgeLabel ? (
252
+ <div className="flex items-start justify-start">
253
+ <span
254
+ className="inline-flex rounded-full border border-lime-500/80 bg-lime-500/15 px-2 py-1 text-[11.5px] font-medium tracking-widest text-stone-50 backdrop-blur-md sm:px-2 sm:py-1"
255
+ style={{ boxShadow: '0 0 24px rgba(132,204,22,0.35)' }}
256
+ >
257
+ {badgeLabel}
258
+ </span>
259
+ </div>
260
+ ) : (
261
+ <span />
262
+ )}
263
+
264
+ {item.title && (
265
+ <h3
266
+ className="max-w-[86%] text-lg font-semibold tracking-[0.08em] text-stone-50"
267
+ style={{ textShadow: '0 4px 16px rgba(0,0,0,0.9)' }}
268
+ >
269
+ {item.title}
270
+ </h3>
271
+ )}
272
+ </div>
273
+ </div>
274
+ </article>
275
+ </Link>
276
+ </article>
277
+ )
278
+ }
279
+
280
+ export const DDMasonaryMedia: React.FC<DDMasonaryMediaProps> = ({
281
+ eyebrow = 'Featured systems',
282
+ headlineMain = 'Components, blocks, and templates',
283
+ headlineAccent = 'built to feel alive',
284
+ subtext = 'A curated showcase of production-ready interface pieces, with static previews and motion demos on hover.',
285
+ items = [],
286
+ columnsDesktop = 12,
287
+ columnsTablet = 2,
288
+ gapClass = 'gap-5',
289
+ containerClassName = 'w-full max-w-none px-4 md:px-8',
290
+ sectionClassName = 'py-24',
291
+ sectionId = 'studio-masonary-media',
292
+ customCSS,
293
+ showCta = false,
294
+ ctaAlign = 'center',
295
+ ctaDisplayMode = 'iconWithLabel',
296
+ ctaIconPosition = 'before',
297
+ ctaButtonText,
298
+ ctaButtonStyle = 'primary',
299
+ ctaSize = 'medium',
300
+ ctaIcon,
301
+ ctaLink,
302
+ ctaContainerClass,
303
+ }) => {
304
+ const gridRef = useRef<HTMLDivElement | null>(null)
305
+ const isotopeRef = useRef<any>(null)
306
+
307
+ const safeSectionId = sectionId || 'studio-masonary-media'
308
+ const gap = useMemo(() => getGapPx(gapClass), [gapClass])
309
+ const desktopColumns = Math.min(Math.max(columnsDesktop || 6, 5), 6)
310
+ const tabletColumns = Math.min(Math.max(columnsTablet || 2, 1), 3)
311
+
312
+ useEffect(() => {
313
+ if (!gridRef.current || !items || items.length === 0) return
314
+
315
+ let destroyed = false
316
+
317
+ const init = async () => {
318
+ const Isotope = (await import('isotope-layout')).default
319
+ const imagesLoaded = (await import('imagesloaded')).default
320
+
321
+ if (!gridRef.current || destroyed) return
322
+
323
+ const iso = new Isotope(gridRef.current, {
324
+ itemSelector: '.studio-masonry-item',
325
+ percentPosition: true,
326
+ transitionDuration: '0.35s',
327
+ masonry: {
328
+ columnWidth: '.studio-masonry-sizer',
329
+ gutter: '.studio-masonry-gutter',
330
+ fitWidth: false,
331
+ },
332
+ })
333
+
334
+ isotopeRef.current = iso
335
+
336
+ imagesLoaded(gridRef.current, () => {
337
+ if (!destroyed) iso.layout()
338
+ })
339
+
340
+ window.setTimeout(() => {
341
+ if (!destroyed) iso.layout()
342
+ }, 250)
343
+ }
344
+
345
+ init()
346
+
347
+ const handleResize = () => {
348
+ isotopeRef.current?.layout()
349
+ }
350
+
351
+ window.addEventListener('resize', handleResize)
352
+
353
+ return () => {
354
+ destroyed = true
355
+ window.removeEventListener('resize', handleResize)
356
+ isotopeRef.current?.destroy()
357
+ isotopeRef.current = null
358
+ }
359
+ }, [items])
360
+
361
+ useEffect(() => {
362
+ isotopeRef.current?.reloadItems()
363
+ isotopeRef.current?.layout()
364
+ }, [items, columnsDesktop, columnsTablet, gap])
365
+
366
+ if (!items || items.length === 0) return null
367
+
368
+ return (
369
+ <>
370
+ <style
371
+ dangerouslySetInnerHTML={{
372
+ __html: `
373
+ #${safeSectionId} .studio-masonry-grid {
374
+ --studio-masonry-gap: ${gap}px;
375
+ --studio-masonry-cols: 1;
376
+ position: relative;
377
+ width: 100%;
378
+ }
379
+
380
+ #${safeSectionId} .studio-masonry-sizer,
381
+ #${safeSectionId} .studio-masonry-item {
382
+ width: 100%;
383
+ }
384
+
385
+ #${safeSectionId} .studio-masonry-gutter {
386
+ width: var(--studio-masonry-gap);
387
+ }
388
+
389
+ #${safeSectionId} .studio-masonry-item {
390
+ margin-bottom: var(--studio-masonry-gap);
391
+ will-change: transform;
392
+ }
393
+
394
+ @media (max-width: 639px) {
395
+ #${safeSectionId} .studio-masonry-grid {
396
+ --studio-masonry-cols: 2;
397
+ }
398
+
399
+ #${safeSectionId} .studio-masonry-sizer,
400
+ #${safeSectionId} .studio-masonry-item.is-square {
401
+ width: calc(
402
+ (100% - ((var(--studio-masonry-cols) - 1) * var(--studio-masonry-gap))) /
403
+ var(--studio-masonry-cols)
404
+ );
405
+ }
406
+
407
+ #${safeSectionId} .studio-masonry-item:not(.is-square) {
408
+ width: 100%;
409
+ }
410
+
411
+ #${safeSectionId} .studio-masonry-item.is-square .card-media {
412
+ aspect-ratio: 1 / 1 !important;
413
+ }
414
+
415
+ #${safeSectionId} .studio-masonry-item:not(.is-square) .card-media {
416
+ aspect-ratio: 16 / 9 !important;
417
+ }
418
+ }
419
+
420
+ @media (min-width: 640px) {
421
+ #${safeSectionId} .studio-masonry-grid {
422
+ --studio-masonry-cols: ${tabletColumns};
423
+ }
424
+
425
+ #${safeSectionId} .studio-masonry-sizer,
426
+ #${safeSectionId} .studio-masonry-item {
427
+ width: calc(
428
+ (100% - ((var(--studio-masonry-cols) - 1) * var(--studio-masonry-gap))) /
429
+ var(--studio-masonry-cols)
430
+ );
431
+ }
432
+
433
+ #${safeSectionId} .studio-masonry-item.is-wide {
434
+ width: calc(
435
+ (
436
+ (100% - ((var(--studio-masonry-cols) - 1) * var(--studio-masonry-gap))) /
437
+ var(--studio-masonry-cols)
438
+ ) *
439
+ 2 + var(--studio-masonry-gap)
440
+ );
441
+ }
442
+ }
443
+
444
+ @media (min-width: 1024px) {
445
+ #${safeSectionId} .studio-masonry-grid {
446
+ --studio-masonry-cols: ${desktopColumns};
447
+ }
448
+ }
449
+
450
+ @media (min-width: 640px) and (max-width: 1023px) {
451
+ #${safeSectionId} .studio-masonry-item.is-wide {
452
+ width: 100%;
453
+ }
454
+ }
455
+
456
+ #${safeSectionId} .card-media {
457
+ min-height: 10rem;
458
+ scale: 1;
459
+ }
460
+ `,
461
+ }}
462
+ />
463
+
464
+ {customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
465
+
466
+ <section id={safeSectionId} className={cn('studio-masonary-media', sectionClassName)}>
467
+ <div className={cn(containerClassName)}>
468
+
469
+ <motion.div
470
+ variants={containerVariants}
471
+ initial="hidden"
472
+ whileInView="show"
473
+ viewport={{ once: false, amount: 0.2 }}
474
+ className="mb-14 flex flex-col justify-between gap-6 overflow-hidden lg:flex-row lg:items-end"
475
+ >
476
+ <motion.div variants={itemVariants}>
477
+ {eyebrow && (
478
+ <p className="top-caption mb-3 text-[11.5px] font-semibold uppercase tracking-widest text-stone-400">
479
+ {eyebrow}
480
+ </p>
481
+ )}
482
+
483
+ {(headlineMain || headlineAccent) && (
484
+ <h2 className="text-3d text-4xl font-bold leading-tight text-stone-900 dark:text-stone-50 md:text-5xl">
485
+ {headlineMain}
486
+
487
+ {headlineAccent && (
488
+ <>
489
+ <br />
490
+ <span className="text-stone-400">{headlineAccent}</span>
491
+ </>
492
+ )}
493
+ </h2>
494
+ )}
495
+ </motion.div>
496
+
497
+ {subtext && (
498
+ <motion.p
499
+ variants={itemVariants}
500
+ className="max-w-2xl text-base leading-relaxed text-stone-400 dark:text-stone-300 lg:max-w-xl lg:text-right"
501
+ >
502
+ {subtext}
503
+ </motion.p>
504
+ )}
505
+ </motion.div>
506
+
507
+ <div ref={gridRef} className="studio-masonry-grid">
508
+ <div className="studio-masonry-sizer" />
509
+ <div className="studio-masonry-gutter" />
510
+
511
+ {items.map((item, index) => (
512
+ <StudioMasonaryCard
513
+ key={item.id ?? `${item.title}-${index}`}
514
+ item={item}
515
+ index={index}
516
+ />
517
+ ))}
518
+ </div>
519
+ </div>
520
+ {showCta && (
521
+ <div
522
+ className={cn(
523
+ 'mt-10 flex',
524
+ ctaAlign === 'left' && 'justify-start',
525
+ ctaAlign === 'center' && 'justify-center',
526
+ ctaAlign === 'right' && 'justify-end',
527
+ ctaContainerClass,
528
+ )}
529
+ >
530
+ <a
531
+ href={(ctaLink as any)?.url ?? '#'}
532
+ className="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"
533
+ style={{ cornerShape: 'squircle' } as React.CSSProperties}
534
+ >
535
+ {/* TODO: Reconnect ButtonWithIcon if this project provides it. */}
536
+ {ctaButtonText ?? (ctaLink as any)?.label ?? 'Learn more'}
537
+ </a>
538
+ </div>
539
+ )}
540
+ </section>
541
+ </>
542
+ )
543
+ }
544
+
545
+ export default DDMasonaryMedia
546
+
547
+