blocks-dusted 0.1.12 → 0.1.13

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 (148) hide show
  1. package/package.json +1 -1
  2. package/src/installers/patchRenderBlocks.js +1 -1
  3. package/src/registry/validateTemplateManifest.js +2 -1
  4. package/templates/blocks/BannerDD790432/Component.tsx +63 -0
  5. package/templates/blocks/BannerDD790432/config.ts +54 -0
  6. package/templates/blocks/BannerDD790432/manifest.json +80 -0
  7. package/templates/blocks/Button47/Component.tsx +124 -0
  8. package/templates/blocks/Button47/config.ts +75 -0
  9. package/templates/blocks/Button47/link.ts +22 -0
  10. package/templates/blocks/Button47/manifest.json +105 -0
  11. package/templates/blocks/Button47/styles.module.scss +75 -0
  12. package/templates/blocks/DD-Accordion/Component.tsx +55 -36
  13. package/templates/blocks/DD-Accordion/config.ts +71 -59
  14. package/templates/blocks/DD-AnimText/Component.tsx +50 -32
  15. package/templates/blocks/DD-AnimText/config.ts +296 -295
  16. package/templates/blocks/DD-AsideNav/Component.tsx +323 -319
  17. package/templates/blocks/DD-Banner/Component.tsx +54 -51
  18. package/templates/blocks/DD-BookCall/Component.tsx +535 -535
  19. package/templates/blocks/DD-ButtonPop/Component.tsx +118 -117
  20. package/templates/blocks/DD-Card/Component.tsx +4 -4
  21. package/templates/blocks/DD-CardTemplate01/Component.tsx +139 -140
  22. package/templates/blocks/DD-CardsWithVariantV3/Component.tsx +472 -0
  23. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/index.tsx +203 -0
  24. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/styles.ts +87 -0
  25. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/useInjectStyle.ts +39 -0
  26. package/templates/blocks/DD-CardsWithVariantV3/config.ts +326 -0
  27. package/templates/blocks/DD-CardsWithVariantV3/manifest.json +110 -0
  28. package/templates/blocks/DD-Carousel-A/Component.tsx +251 -249
  29. package/templates/blocks/DD-Carousel-B/Component.tsx +405 -403
  30. package/templates/blocks/DD-ComparisonTable/Component.tsx +255 -256
  31. package/templates/blocks/DD-Contact/Component.tsx +433 -435
  32. package/templates/blocks/DD-Counter/Component.tsx +11 -6
  33. package/templates/blocks/DD-DeliCard/Component.tsx +303 -0
  34. package/templates/blocks/DD-DeliCard/config.ts +186 -0
  35. package/templates/blocks/DD-DeliCard/manifest.json +94 -0
  36. package/templates/blocks/DD-FeatCat/Component.tsx +302 -302
  37. package/templates/blocks/DD-FeatProd/CircularGallery.tsx +1179 -0
  38. package/templates/blocks/DD-FeatProd/Component.tsx +484 -0
  39. package/templates/blocks/DD-FeatProd/config.ts +173 -0
  40. package/templates/blocks/DD-FeatProd/manifest.json +93 -0
  41. package/templates/blocks/DD-FeatServ/Component.tsx +607 -0
  42. package/templates/blocks/DD-FeatServ/config.ts +213 -0
  43. package/templates/blocks/DD-FeatServ/manifest.json +113 -0
  44. package/templates/blocks/DD-FeatStrip/Component.tsx +584 -585
  45. package/templates/blocks/DD-FluidBtn/ButtonsEyeCatching.tsx +627 -628
  46. package/templates/blocks/DD-FluidBtn/Component.tsx +46 -43
  47. package/templates/blocks/DD-GooeyInfo/Component.tsx +150 -149
  48. package/templates/blocks/DD-GooeyInfo/config.ts +136 -135
  49. package/templates/blocks/DD-Hero/Component.tsx +296 -297
  50. package/templates/blocks/DD-HeroPlas/Component.tsx +50 -0
  51. package/templates/blocks/DD-HeroPlas/ThreeScene.tsx +109 -0
  52. package/templates/blocks/DD-HeroPlas/config.ts +33 -0
  53. package/templates/blocks/DD-HeroPlas/manifest.json +88 -0
  54. package/templates/blocks/DD-HorizontalScroll/Component.tsx +937 -936
  55. package/templates/blocks/DD-Icon/Component.tsx +46 -81
  56. package/templates/blocks/DD-IconButton/Component.tsx +13 -20
  57. package/templates/blocks/DD-IdTag/Component.tsx +22 -21
  58. package/templates/blocks/DD-LineAnim/Component.tsx +42 -36
  59. package/templates/blocks/DD-LogoMarq/Component.tsx +38 -48
  60. package/templates/blocks/DD-Lottie/Component.tsx +11 -3
  61. package/templates/blocks/DD-MasonaryMedia/Component.tsx +544 -545
  62. package/templates/blocks/DD-MasonaryMedia/config.ts +389 -389
  63. package/templates/blocks/DD-Masonry/Component.tsx +654 -644
  64. package/templates/blocks/DD-Masonry/config.ts +475 -475
  65. package/templates/blocks/DD-MaterialSwatches/Component.tsx +85 -0
  66. package/templates/blocks/DD-MaterialSwatches/config.ts +232 -0
  67. package/templates/blocks/DD-MaterialSwatches/manifest.json +84 -0
  68. package/templates/blocks/DD-MaterialSwatches/material.ts +107 -0
  69. package/templates/blocks/DD-NineCanvas/Component.tsx +129 -0
  70. package/templates/blocks/DD-NineCanvas/RenderBlocks.tsx +244 -0
  71. package/templates/blocks/DD-NineCanvas/allowedCanvasBlocks.ts +6 -0
  72. package/templates/blocks/DD-NineCanvas/blockComponents.tsx +8 -0
  73. package/templates/blocks/DD-NineCanvas/canvas/CanvasGrid.tsx +316 -0
  74. package/templates/blocks/DD-NineCanvas/canvas/RadialNav.tsx +432 -0
  75. package/templates/blocks/DD-NineCanvas/canvas/canvas-panels.css +889 -0
  76. package/templates/blocks/DD-NineCanvas/canvas/icons/custom-icons.tsx +364 -0
  77. package/templates/blocks/DD-NineCanvas/config.ts +61 -0
  78. package/templates/blocks/DD-NineCanvas/manifest.json +165 -0
  79. package/templates/blocks/DD-NineCanvas/styles.scss +152 -0
  80. package/templates/blocks/DD-NineCanvas/tooltip/get-strict-context.tsx +26 -0
  81. package/templates/blocks/DD-NineCanvas/tooltip/primitives.tsx +178 -0
  82. package/templates/blocks/DD-NineCanvas/tooltip/tooltip.tsx +71 -0
  83. package/templates/blocks/DD-NineCanvas/tooltip/use-controlled-state.tsx +33 -0
  84. package/templates/blocks/DD-NineCanvas/utilities/FadeOnScroll.tsx +68 -0
  85. package/templates/blocks/DD-NineCanvas/utilities/ParallaxWrapper.tsx +157 -0
  86. package/templates/blocks/DD-NineCanvas/utilities/renderCustomCSS.ts +5 -0
  87. package/templates/blocks/DD-NineCanvas/utilities/webglCleanup.ts +27 -0
  88. package/templates/blocks/DD-PayloadAIAssistant/Component.tsx +78 -0
  89. package/templates/blocks/DD-PayloadAIAssistant/DustedBot.tsx +590 -0
  90. package/templates/blocks/DD-PayloadAIAssistant/config.ts +37 -0
  91. package/templates/blocks/DD-PayloadAIAssistant/demo.css +125 -0
  92. package/templates/blocks/DD-PayloadAIAssistant/manifest.json +93 -0
  93. package/templates/blocks/DD-PayloadAIAssistant/source-defaults.ts +17 -0
  94. package/templates/blocks/DD-PayloadAIAssistant/styles.ts +306 -0
  95. package/templates/blocks/DD-Pricing/Component.tsx +2 -3
  96. package/templates/blocks/DD-Pricing/config.ts +215 -215
  97. package/templates/blocks/DD-Process/Component.tsx +2 -2
  98. package/templates/blocks/DD-ProjHeader/Component.tsx +93 -91
  99. package/templates/blocks/DD-ProjSlider/Component.tsx +1 -1
  100. package/templates/blocks/DD-Section/Component.tsx +265 -266
  101. package/templates/blocks/DD-Separator/Component.tsx +66 -66
  102. package/templates/blocks/DD-Services/Component.tsx +176 -176
  103. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +306 -307
  104. package/templates/blocks/DD-Slider-A/Component.tsx +2 -2
  105. package/templates/blocks/DD-StackCards/Component.tsx +172 -171
  106. package/templates/blocks/DD-Team/Component.tsx +247 -247
  107. package/templates/blocks/DD-TeamCards/Component.tsx +251 -251
  108. package/templates/blocks/DD-TechStack/Component.tsx +173 -174
  109. package/templates/blocks/DD-TestMarq/Component.tsx +158 -132
  110. package/templates/blocks/DD-Testim/Component.tsx +247 -247
  111. package/templates/blocks/DD-Testim2/Component.tsx +265 -256
  112. package/templates/blocks/DD-Testimonials/Component.tsx +139 -139
  113. package/templates/blocks/DD-Thanks/Component.tsx +1 -1
  114. package/templates/blocks/DD-Work/Component.tsx +318 -318
  115. package/templates/blocks/DD-Work-B/Component.tsx +294 -294
  116. package/templates/blocks/DDHG-CTASection2/Component.tsx +183 -184
  117. package/templates/blocks/DDHG-HighlightArc/Component.tsx +6 -3
  118. package/templates/blocks/DDHG-LogoMarquee/Component.tsx +45 -8
  119. package/templates/blocks/DDHG-NavCodeGrid/Component.tsx +1 -3
  120. package/templates/blocks/DDHG-ServiceDetails/ServiceDetailCard.tsx +9 -22
  121. package/templates/blocks/DDHG-ServiceHero/Component.tsx +10 -10
  122. package/templates/blocks/DDHG-TeamCards/Component.tsx +6 -35
  123. package/templates/blocks/Form/Checkbox/index.tsx +45 -0
  124. package/templates/blocks/Form/Component.tsx +163 -0
  125. package/templates/blocks/Form/Country/index.tsx +65 -0
  126. package/templates/blocks/Form/Country/options.ts +982 -0
  127. package/templates/blocks/Form/Email/index.tsx +38 -0
  128. package/templates/blocks/Form/Error/index.tsx +15 -0
  129. package/templates/blocks/Form/Message/index.tsx +13 -0
  130. package/templates/blocks/Form/Number/index.tsx +36 -0
  131. package/templates/blocks/Form/Select/index.tsx +63 -0
  132. package/templates/blocks/Form/State/index.tsx +64 -0
  133. package/templates/blocks/Form/State/options.ts +52 -0
  134. package/templates/blocks/Form/Text/index.tsx +32 -0
  135. package/templates/blocks/Form/Textarea/index.tsx +40 -0
  136. package/templates/blocks/Form/Width/index.tsx +13 -0
  137. package/templates/blocks/Form/config.ts +51 -0
  138. package/templates/blocks/Form/fields.tsx +21 -0
  139. package/templates/blocks/Form/manifest.json +168 -0
  140. package/templates/blocks/MediaBlock/Component.tsx +67 -0
  141. package/templates/blocks/MediaBlock/config.ts +14 -0
  142. package/templates/blocks/MediaBlock/manifest.json +92 -0
  143. package/templates/blocks/TransitionsDustedShowcase/Component.tsx +9 -0
  144. package/templates/blocks/TransitionsDustedShowcase/config.ts +13 -0
  145. package/templates/blocks/TransitionsDustedShowcase/manifest.json +80 -0
  146. package/templates/blocks/rawHtml/Component.tsx +25 -0
  147. package/templates/blocks/rawHtml/config.ts +56 -0
  148. package/templates/blocks/rawHtml/manifest.json +80 -0
@@ -1,645 +1,655 @@
1
- /* eslint-disable react-hooks/exhaustive-deps */
2
- 'use client'
3
-
4
- import React, { useEffect, useLayoutEffect, useMemo, useRef, useState, useCallback } from 'react'
5
- import { gsap } from 'gsap'
6
- import { ScrollTrigger } from 'gsap/ScrollTrigger'
7
- import { cn } from '@/utilities/ui'
8
- import FadeUpOnInView from '@/components/ui/anims/FadeUpOnInView'
9
-
10
- if (typeof window !== 'undefined') {
11
- gsap.registerPlugin(ScrollTrigger)
12
- }
13
-
14
- const useMedia = (queries: string[], values: number[], defaultValue: number): number => {
15
- const [value, setValue] = useState(() => {
16
- if (typeof window !== 'undefined') {
17
- return values[queries.findIndex((q) => window.matchMedia(q).matches)] ?? defaultValue
18
- }
19
-
20
- return values[0] ?? defaultValue
21
- })
22
-
23
- useEffect(() => {
24
- if (typeof window === 'undefined') return
25
-
26
- const handler = () => {
27
- setValue(values[queries.findIndex((q) => window.matchMedia(q).matches)] ?? defaultValue)
28
- }
29
-
30
- const mediaQueryLists = queries.map((q) => window.matchMedia(q))
31
- mediaQueryLists.forEach((mql) => mql.addEventListener('change', handler))
32
-
33
- return () => mediaQueryLists.forEach((mql) => mql.removeEventListener('change', handler))
34
- }, [queries, values, defaultValue])
35
-
36
- return value
37
- }
38
-
39
- const useMeasure = <T extends HTMLElement>() => {
40
- const ref = useRef<T | null>(null)
41
- const [size, setSize] = useState({ width: 0, height: 0 })
42
-
43
- useLayoutEffect(() => {
44
- if (!ref.current) return
45
-
46
- const ro = new ResizeObserver((entries) => {
47
- if (entries[0]) {
48
- const { width, height } = entries[0].contentRect
49
- setSize({ width, height })
50
- }
51
- })
52
-
53
- ro.observe(ref.current)
54
-
55
- return () => ro.disconnect()
56
- }, [])
57
-
58
- return [ref, size] as const
59
- }
60
-
61
- interface MasonryItem {
62
- id: string
63
- media: { id?: string; url?: string; alt?: string; mimeType?: string }
64
- linkType?: 'none' | 'reference' | 'custom'
65
- reference?: {
66
- value: string | { slug: string }
67
- relationTo: string
68
- }
69
- url?: string
70
- newTab?: boolean
71
- aspectRatio: string
72
- showTextOverlay?: boolean
73
- textOverlay?: string
74
- }
75
-
76
- interface MasonryMediaBlockProps {
77
- items: MasonryItem[]
78
- layout: {
79
- columns: {
80
- desktop: number
81
- laptop: number
82
- tablet: number
83
- mobile: number
84
- }
85
- gap: number
86
- }
87
- animation: {
88
- enableAnimation?: boolean
89
- animateFrom: 'bottom' | 'top' | 'left' | 'right' | 'center' | 'random'
90
- ease: string
91
- duration: number
92
- stagger: number
93
- repeatOnScroll?: boolean
94
- }
95
- interaction: {
96
- scaleOnHover: boolean
97
- hoverScale: number
98
- blurToFocus: boolean
99
- colorShiftOnHover: boolean
100
- customHoverColors?: {
101
- fromColor: string
102
- toColor: string
103
- }
104
- }
105
- advanced?: {
106
- sectionId?: string
107
- customClassName?: string
108
- enableParallax?: boolean
109
- parallaxSpeed?: number
110
- }
111
- }
112
-
113
- const MediaContent: React.FC<{ item: MasonryItem }> = React.memo(
114
- ({ item }) => {
115
- return (
116
- <div
117
- className="w-full h-full overflow-hidden masonry-media-container"
118
- style={
119
- {
120
- aspectRatio: item.aspectRatio || '1/1',
121
- '--masonry-aspect-ratio': item.aspectRatio || '1/1'
122
- } as React.CSSProperties
123
- }
124
- >
125
- <style jsx>{`
126
- .masonry-media-container :global(.media-block),
127
- .masonry-media-container :global(.media-block img),
128
- .masonry-media-container :global(.media-block video),
129
- .masonry-media-container :global(.media-block picture),
130
- .masonry-media-container :global(.media-block picture img) {
131
- width: 100% !important;
132
- height: 100% !important;
133
- object-fit: cover !important;
134
- aspect-ratio: var(--masonry-aspect-ratio) !important;
135
- margin: 0;
136
- border-radius: 4px;
137
- }
138
-
139
- .masonry-media-container :global(.parallax-section),
140
- .masonry-media-container :global(.parallax-content) {
141
- width: 100% !important;
142
- height: 100% !important;
143
- }
144
- `}</style>
145
- {item.media?.mimeType?.startsWith('video/') ? (
146
- <video className="h-full w-full object-cover" src={item.media.url} muted playsInline loop />
1
+ /* eslint-disable react-hooks/exhaustive-deps */
2
+ 'use client'
3
+
4
+ import React, { useEffect, useLayoutEffect, useMemo, useRef, useState, useCallback } from 'react'
5
+ import { gsap } from 'gsap'
6
+ import { ScrollTrigger } from 'gsap/ScrollTrigger'
7
+ import { cn } from '@/utilities/ui'
8
+ import FadeUpOnInView from '@/components/ui/anims/FadeUpOnInView'
9
+
10
+ if (typeof window !== 'undefined') {
11
+ gsap.registerPlugin(ScrollTrigger)
12
+ }
13
+
14
+ const useMedia = (queries: string[], values: number[], defaultValue: number): number => {
15
+ const [value, setValue] = useState(() => {
16
+ if (typeof window !== 'undefined') {
17
+ return values[queries.findIndex((q) => window.matchMedia(q).matches)] ?? defaultValue
18
+ }
19
+
20
+ return values[0] ?? defaultValue
21
+ })
22
+
23
+ useEffect(() => {
24
+ if (typeof window === 'undefined') return
25
+
26
+ const handler = () => {
27
+ setValue(values[queries.findIndex((q) => window.matchMedia(q).matches)] ?? defaultValue)
28
+ }
29
+
30
+ const mediaQueryLists = queries.map((q) => window.matchMedia(q))
31
+ mediaQueryLists.forEach((mql) => mql.addEventListener('change', handler))
32
+
33
+ return () => mediaQueryLists.forEach((mql) => mql.removeEventListener('change', handler))
34
+ }, [queries, values, defaultValue])
35
+
36
+ return value
37
+ }
38
+
39
+ const useMeasure = <T extends HTMLElement>() => {
40
+ const ref = useRef<T | null>(null)
41
+ const [size, setSize] = useState({ width: 0, height: 0 })
42
+
43
+ useLayoutEffect(() => {
44
+ if (!ref.current) return
45
+
46
+ const ro = new ResizeObserver((entries) => {
47
+ if (entries[0]) {
48
+ const { width, height } = entries[0].contentRect
49
+ setSize({ width, height })
50
+ }
51
+ })
52
+
53
+ ro.observe(ref.current)
54
+
55
+ return () => ro.disconnect()
56
+ }, [])
57
+
58
+ return [ref, size] as const
59
+ }
60
+
61
+ interface MasonryItem {
62
+ id: string
63
+ media: { id?: string; url?: string; alt?: string; mimeType?: string }
64
+ linkType?: 'none' | 'reference' | 'custom'
65
+ reference?: {
66
+ value: string | { slug: string }
67
+ relationTo: string
68
+ }
69
+ url?: string
70
+ newTab?: boolean
71
+ aspectRatio: string
72
+ showTextOverlay?: boolean
73
+ textOverlay?: string
74
+ }
75
+
76
+ interface MasonryMediaBlockProps {
77
+ items: MasonryItem[]
78
+ layout: {
79
+ columns: {
80
+ desktop: number
81
+ laptop: number
82
+ tablet: number
83
+ mobile: number
84
+ }
85
+ gap: number
86
+ }
87
+ animation: {
88
+ enableAnimation?: boolean
89
+ animateFrom: 'bottom' | 'top' | 'left' | 'right' | 'center' | 'random'
90
+ ease: string
91
+ duration: number
92
+ stagger: number
93
+ repeatOnScroll?: boolean
94
+ }
95
+ interaction: {
96
+ scaleOnHover: boolean
97
+ hoverScale: number
98
+ blurToFocus: boolean
99
+ colorShiftOnHover: boolean
100
+ customHoverColors?: {
101
+ fromColor: string
102
+ toColor: string
103
+ }
104
+ }
105
+ advanced?: {
106
+ sectionId?: string
107
+ customClassName?: string
108
+ enableParallax?: boolean
109
+ parallaxSpeed?: number
110
+ }
111
+ }
112
+
113
+ const MediaContent: React.FC<{ item: MasonryItem }> = React.memo(
114
+ ({ item }) => {
115
+ return (
116
+ <div
117
+ className="w-full h-full overflow-hidden masonry-media-container"
118
+ style={
119
+ {
120
+ aspectRatio: item.aspectRatio || '1/1',
121
+ '--masonry-aspect-ratio': item.aspectRatio || '1/1',
122
+ } as React.CSSProperties
123
+ }
124
+ >
125
+ <style jsx>{`
126
+ .masonry-media-container :global(.media-block),
127
+ .masonry-media-container :global(.media-block img),
128
+ .masonry-media-container :global(.media-block video),
129
+ .masonry-media-container :global(.media-block picture),
130
+ .masonry-media-container :global(.media-block picture img) {
131
+ width: 100% !important;
132
+ height: 100% !important;
133
+ object-fit: cover !important;
134
+ aspect-ratio: var(--masonry-aspect-ratio) !important;
135
+ margin: 0;
136
+ border-radius: 4px;
137
+ }
138
+
139
+ .masonry-media-container :global(.parallax-section),
140
+ .masonry-media-container :global(.parallax-content) {
141
+ width: 100% !important;
142
+ height: 100% !important;
143
+ }
144
+ `}</style>
145
+ {item.media?.mimeType?.startsWith('video/') ? (
146
+ <video
147
+ className="h-full w-full object-cover"
148
+ src={item.media.url}
149
+ muted
150
+ playsInline
151
+ loop
152
+ />
147
153
  ) : (
148
- <img className="h-full w-full object-cover" src={item.media?.url || ''} alt={item.media?.alt || ''} />
149
- )}
150
- </div>
151
- )
152
- },
153
- (prev, next) =>
154
- prev.item.media?.id === next.item.media?.id && prev.item.aspectRatio === next.item.aspectRatio
155
- )
156
-
157
- export const DDMasonry: React.FC<MasonryMediaBlockProps> = ({
158
- items,
159
- layout,
160
- animation,
161
- interaction,
162
- advanced
163
- }) => {
164
- const triggersRef = useRef<ScrollTrigger[]>([])
165
-
166
- const getLinkUrl = (item: MasonryItem): string | null => {
167
- if (!item.linkType || item.linkType === 'none') return null
168
-
169
- if (item.linkType === 'custom') {
170
- return item.url || null
171
- }
172
-
173
- if (item.linkType === 'reference' && item.reference) {
174
- const refValue = item.reference.value
175
-
176
- const getCollectionPath = (relationTo: string) => {
177
- if (relationTo === 'posts') return 'thoughts'
178
- if (relationTo === 'pages') return ''
179
- return relationTo
180
- }
181
-
182
- const prefix = getCollectionPath(item.reference.relationTo)
183
- const toUrl = (slug: string) => (prefix ? `/${prefix}/${slug}` : `/${slug}`)
184
-
185
- if (typeof refValue === 'string') {
186
- return toUrl(refValue)
187
- }
188
-
189
- if (refValue && typeof refValue === 'object' && 'slug' in refValue) {
190
- return toUrl(String(refValue.slug))
191
- }
192
- }
193
-
194
- return null
195
- }
196
-
197
- const handleItemClick = (item: MasonryItem) => {
198
- const url = getLinkUrl(item)
199
- if (!url) return
200
-
201
- if (item.newTab) {
202
- window.open(url, '_blank', 'noopener,noreferrer')
203
- return
204
- }
205
-
206
- window.location.href = url
207
- }
208
-
209
- const blockId = useMemo(
210
- () => advanced?.sectionId || `masonry-${Math.random().toString(36).slice(2, 11)}`,
211
- [advanced?.sectionId]
212
- )
213
-
214
- const columns = useMedia(
215
- ['(min-width:1440px)', '(min-width:968px)', '(min-width:640px)', '(min-width:420px)'],
216
- [
217
- layout?.columns?.desktop || 5,
218
- layout?.columns?.laptop || 4,
219
- layout?.columns?.tablet || 3,
220
- layout?.columns?.mobile || 2
221
- ],
222
- 1
223
- )
224
-
225
- const [containerRef, { width }] = useMeasure<HTMLDivElement>()
226
- const [mediaReady, setMediaReady] = useState(false)
227
- const [containerHeight, setContainerHeight] = useState(0)
228
- const animatedItems = useRef(new Set<string>())
229
-
230
- const validItems = useMemo(() => {
231
- return (
232
- items?.map((item, index) => ({
233
- ...item,
234
- id: item.id || `item-${index}`
235
- })) || []
236
- )
237
- }, [items])
238
-
239
- const getAspectRatioHeight = (ratio: string, width: number): number => {
240
- if (!ratio || !ratio.includes('/')) return width
241
-
242
- const [w, h] = ratio.split('/').map(Number)
243
- const validW = w && !isNaN(w) ? w : 1
244
- const validH = h && !isNaN(h) ? h : 1
245
-
246
- return (width * validH) / validW
247
- }
248
-
249
- const preloadMedia = async (itemsToPreload: MasonryItem[]): Promise<void> => {
250
- await Promise.all(
251
- itemsToPreload.map((item) => {
252
- if (item.media?.url && item.media.mimeType?.startsWith('image/')) {
253
- return new Promise<void>((resolve) => {
254
- const img = new Image()
255
- img.src = item.media.url || ''
256
- img.onload = img.onerror = () => resolve()
257
- })
258
- }
259
-
260
- return Promise.resolve()
261
- })
262
- )
263
- }
264
-
265
- const getInitialPosition = useCallback(
266
- (item: any) => {
267
- const containerRect = containerRef.current?.getBoundingClientRect()
268
- if (!containerRect) return { x: item.x, y: item.y }
269
-
270
- let direction = animation?.animateFrom || 'bottom'
271
-
272
- if (direction === 'random') {
273
- const dirs = ['top', 'bottom', 'left', 'right']
274
- direction = dirs[Math.floor(Math.random() * dirs.length)] as typeof direction
275
- }
276
-
277
- switch (direction) {
278
- case 'top':
279
- return { x: item.x, y: -200 }
280
- case 'bottom':
281
- return { x: item.x, y: window.innerHeight + 200 }
282
- case 'left':
283
- return { x: -200, y: item.y }
284
- case 'right':
285
- return { x: window.innerWidth + 200, y: item.y }
286
- case 'center':
287
- return {
288
- x: containerRect.width / 2 - item.w / 2,
289
- y: containerRect.height / 2 - item.h / 2
290
- }
291
- default:
292
- return { x: item.x, y: item.y + 100 }
293
- }
294
- },
295
- [containerRef, animation?.animateFrom]
296
- )
297
-
298
- useEffect(() => {
299
- if (items && items.length > 0) {
300
- const timeout = window.setTimeout(() => setMediaReady(true), 3000)
301
-
302
- preloadMedia(items)
303
- .then(() => setMediaReady(true))
304
- .finally(() => window.clearTimeout(timeout))
305
- } else {
306
- setMediaReady(true)
307
- }
308
- }, [items])
309
-
310
- const grid = useMemo(() => {
311
- if (!width || !validItems.length) return { items: [], height: 0 }
312
-
313
- const colHeights = new Array(columns).fill(0)
314
- const gap = layout?.gap || 16
315
- const totalGaps = (columns - 1) * gap
316
- const columnWidth = (width - totalGaps) / columns
317
-
318
- const gridItems = validItems.map((item) => {
319
- const aspectRatio = item.aspectRatio || '1/1'
320
- const [w, h] = aspectRatio.split('/').map(Number)
321
- const validW = w && !isNaN(w) ? w : 1
322
- const validH = h && !isNaN(h) ? h : 1
323
- const ratio = validW / validH
324
-
325
- let colSpan = 1
326
-
327
- if (ratio >= 2) {
328
- colSpan = Math.min(2, columns)
329
- } else if (ratio >= 1.5) {
330
- colSpan = Math.min(2, columns)
331
- }
332
-
333
- let bestCol = 0
334
- let minHeight = Infinity
335
-
336
- for (let col = 0; col <= columns - colSpan; col++) {
337
- const maxHeightInSpan = Math.max(...colHeights.slice(col, col + colSpan))
338
-
339
- if (maxHeightInSpan < minHeight) {
340
- minHeight = maxHeightInSpan
341
- bestCol = col
342
- }
343
- }
344
-
345
- const itemWidth = columnWidth * colSpan + gap * (colSpan - 1)
346
- const x = bestCol * (columnWidth + gap)
347
- const height = getAspectRatioHeight(aspectRatio, itemWidth)
348
- const y = Math.max(...colHeights.slice(bestCol, bestCol + colSpan))
349
-
350
- for (let i = bestCol; i < bestCol + colSpan; i++) {
351
- colHeights[i] = y + height + gap
352
- }
353
-
354
- return { ...item, x, y, w: itemWidth, h: height, colSpan }
355
- })
356
-
357
- const totalHeight = Math.max(...colHeights)
358
-
359
- return { items: gridItems, height: totalHeight }
360
- }, [columns, validItems, width, layout?.gap])
361
-
362
- useEffect(() => {
363
- setContainerHeight(grid.height)
364
- }, [grid.height])
365
-
366
- const animateItemIntoView = useCallback(
367
- (element: HTMLElement, item: any, index: number, delay: number = 0) => {
368
- const start = getInitialPosition(item)
369
- const deltaX = start.x - item.x
370
- const deltaY = start.y - item.y
371
-
372
- gsap.fromTo(
373
- element,
374
- {
375
- opacity: 0,
376
- x: deltaX,
377
- y: deltaY,
378
- ...(interaction?.blurToFocus && { filter: 'blur(10px)' })
379
- },
380
- {
381
- opacity: 1,
382
- x: 0,
383
- y: 0,
384
- ...(interaction?.blurToFocus && { filter: 'none' }),
385
- duration: animation?.duration || 0.8,
386
- ease: animation?.ease && animation.ease !== '0' ? animation.ease : 'power3.out',
387
- delay: delay + index * (animation?.stagger || 0.05),
388
- overwrite: 'auto'
389
- }
390
- )
391
- },
392
- [getInitialPosition, animation, interaction?.blurToFocus]
393
- )
394
-
395
- const hasMounted = useRef(false)
396
-
397
- useLayoutEffect(() => {
398
- if (!mediaReady || !items || items.length === 0) return
399
-
400
- const blockEl = containerRef.current
401
- if (!blockEl) return
402
-
403
- triggersRef.current.forEach((t) => t.kill())
404
- triggersRef.current = []
405
-
406
- gsap.killTweensOf(`[data-block-id="${blockId}"] [data-key]`)
407
-
408
- if (animation?.enableAnimation === false) {
409
- grid.items.forEach((item) => {
410
- const selector = `[data-key="${item.id}"]`
411
- const element = blockEl.querySelector(selector) as HTMLElement | null
412
- if (!element) return
413
-
414
- gsap.set(element, {
415
- opacity: 1,
416
- x: 0,
417
- y: 0,
418
- left: item.x,
419
- top: item.y,
420
- width: item.w,
421
- height: item.h,
422
- filter: 'none'
423
- })
424
- })
425
-
426
- return
427
- }
428
-
429
- grid.items.forEach((item, index) => {
430
- const selector = `[data-key="${item.id}"]`
431
- const element = blockEl.querySelector(selector) as HTMLElement | null
432
- if (!element) return
433
-
434
- const animProps = { left: item.x, top: item.y, width: item.w, height: item.h }
435
-
436
- if (!hasMounted.current) {
437
- gsap.set(element, {
438
- opacity: 1,
439
- x: 0,
440
- y: 0,
441
- width: item.w,
442
- height: item.h,
443
- filter: 'none'
444
- })
445
-
446
- const trigger = ScrollTrigger.create({
447
- id: `masonry-item-${blockId}-${item.id}`,
448
- trigger: element,
449
- start: 'top 85%',
450
- end: 'bottom 15%',
451
- toggleActions: animation?.repeatOnScroll
452
- ? 'play none none reverse'
453
- : 'play none none none',
454
- onEnter: () => {
455
- if (!animatedItems.current.has(item.id) || animation?.repeatOnScroll) {
456
- animateItemIntoView(element, item, index)
457
-
458
- if (!animation?.repeatOnScroll) {
459
- animatedItems.current.add(item.id)
460
- }
461
- }
462
- },
463
- onLeave: () => {
464
- if (animation?.repeatOnScroll) {
465
- gsap.to(element, {
466
- opacity: 0.3,
467
- duration: 0.3,
468
- ease: 'power2.out'
469
- })
470
- }
471
- },
472
- onEnterBack: () => {
473
- if (animation?.repeatOnScroll) {
474
- gsap.to(element, {
475
- opacity: 1,
476
- duration: 0.3,
477
- ease: 'power2.out'
478
- })
479
- }
480
- },
481
- onLeaveBack: () => {
482
- if (animation?.repeatOnScroll) {
483
- gsap.to(element, {
484
- opacity: 0.3,
485
- duration: 0.3,
486
- ease: 'power2.out'
487
- })
488
- }
489
- }
490
- })
491
-
492
- triggersRef.current.push(trigger)
493
- } else {
494
- gsap.to(element, {
495
- ...animProps,
496
- x: 0,
497
- y: 0,
498
- duration: animation?.duration || 0.6,
499
- ease: animation?.ease && animation.ease !== '0' ? animation.ease : 'power3.out',
500
- overwrite: 'auto'
501
- })
502
- }
503
- })
504
-
505
- hasMounted.current = true
506
-
507
- return () => {
508
- triggersRef.current.forEach((t) => t.kill())
509
- triggersRef.current = []
510
- }
511
- }, [grid, mediaReady, animation, interaction?.blurToFocus, items, animateItemIntoView])
512
-
513
- useEffect(() => {
514
- return () => {
515
- ScrollTrigger.getAll().forEach((trigger) => {
516
- if (trigger.vars.id?.startsWith(`masonry-item-${blockId}-`)) {
517
- trigger.kill()
518
- }
519
- })
520
-
521
- gsap.killTweensOf(`[data-block-id="${blockId}"] [data-key]`)
522
- }
523
- }, [blockId])
524
-
525
- const handleMouseEnter = (_itemId: string, element: HTMLElement) => {
526
- if (interaction?.scaleOnHover) {
527
- gsap.to(element, {
528
- scale: interaction.hoverScale || 0.95,
529
- duration: 0.3,
530
- ease: 'power2.out',
531
- filter: 'brightness(0.75)'
532
- })
533
- }
534
-
535
- if (interaction?.colorShiftOnHover) {
536
- const overlay = element.querySelector('.color-overlay') as HTMLElement
537
- if (overlay) gsap.to(overlay, { opacity: 0.3, duration: 0.3 })
538
- }
539
- }
540
-
541
- const handleMouseLeave = (_itemId: string, element: HTMLElement) => {
542
- if (interaction?.scaleOnHover) {
543
- gsap.to(element, {
544
- scale: 1,
545
- duration: 0.3,
546
- ease: 'power2.out',
547
- filter: 'none'
548
- })
549
- }
550
-
551
- if (interaction?.colorShiftOnHover) {
552
- const overlay = element.querySelector('.color-overlay') as HTMLElement
553
- if (overlay) gsap.to(overlay, { opacity: 0, duration: 0.3 })
554
- }
555
- }
556
-
557
- if (!items || items.length === 0) {
558
- return <div className="p-4">No items to display</div>
559
- }
560
-
561
- return (
562
- <div
563
- id={advanced?.sectionId}
564
- data-block-id={blockId}
565
- className={cn('masonary-media-block relative w-full', advanced?.customClassName)}
566
- ref={containerRef}
567
- style={{
568
- height: containerHeight > 0 ? `${containerHeight}px` : 'auto',
569
- minHeight: containerHeight > 0 ? `${containerHeight}px` : 'auto'
570
- }}
571
- {...(advanced?.enableParallax && {
572
- 'data-scroll': true,
573
- 'data-scroll-speed': advanced.parallaxSpeed || 0.5
574
- })}
575
- >
576
- {!mediaReady &&
577
- grid.items.map((item) => (
578
- <div
579
- key={`skeleton-${item.id}`}
580
- className="absolute rounded-md overflow-hidden border border-border"
581
- style={{
582
- left: item.x,
583
- top: item.y,
584
- width: item.w,
585
- height: item.h
586
- }}
587
- >
588
- <div className="shimmer-effect w-full h-full bg-zinc-200/10 dark:bg-zinc-800/10" />
589
- </div>
590
- ))}
591
-
592
- {mediaReady &&
593
- grid.items.map((item) => {
594
- const hasLink = getLinkUrl(item) !== null
595
-
596
- return (
597
- <div
598
- key={item.id}
599
- data-key={item.id}
600
- className={cn(
601
- 'absolute box-content rounded-md overflow-hidden border border-border shadow-2xl',
602
- hasLink && 'cursor-pointer'
603
- )}
604
- style={{
605
- left: item.x,
606
- top: item.y,
607
- width: item.w,
608
- height: item.h,
609
- opacity: 1,
610
- willChange: 'transform, opacity, width, height'
611
- }}
612
- onClick={() => handleItemClick(item)}
613
- onMouseEnter={(e) => handleMouseEnter(item.id, e.currentTarget)}
614
- onMouseLeave={(e) => handleMouseLeave(item.id, e.currentTarget)}
615
- >
616
- <div className="group relative w-full h-full">
617
- <MediaContent item={item} />
618
-
619
- {item.showTextOverlay && item.textOverlay && (
620
- <span
621
- className="masonary-card-text-overlay absolute inset-0 z-10 m-auto flex h-full w-full items-center justify-center p-4 px-2 text-center text-sm text-white [text-shadow:2px_2px_2px_black] shadow-[0_0_0px_black] transition-all duration-300 hover:scale-110 hover:brightness-110 group-hover:[&_img]:brightness-[0.15]"
622
- >
623
- {item.textOverlay}
624
- </span>
625
- )}
626
-
627
- {interaction?.colorShiftOnHover && (
628
- <FadeUpOnInView delay={0.1} className="absolute inset-0">
629
- <div
630
- className="color-overlay absolute inset-0 rounded-sm opacity-0 pointer-events-none"
631
- style={{
632
- background: `linear-gradient(to top right, ${
633
- interaction.customHoverColors?.fromColor || 'rgba(236, 72, 153, 0.5)'
634
- }, ${interaction.customHoverColors?.toColor || 'rgba(14, 165, 233, 0.5)'})`
635
- }}
636
- />
637
- </FadeUpOnInView>
638
- )}
639
- </div>
640
- </div>
641
- )
642
- })}
643
- </div>
644
- )
645
- }
154
+ <img
155
+ className="h-full w-full object-cover"
156
+ src={item.media?.url || ''}
157
+ alt={item.media?.alt || ''}
158
+ />
159
+ )}
160
+ </div>
161
+ )
162
+ },
163
+ (prev, next) =>
164
+ prev.item.media?.id === next.item.media?.id && prev.item.aspectRatio === next.item.aspectRatio,
165
+ )
166
+
167
+ export const DDMasonry: React.FC<MasonryMediaBlockProps> = ({
168
+ items,
169
+ layout,
170
+ animation,
171
+ interaction,
172
+ advanced,
173
+ }) => {
174
+ const triggersRef = useRef<ScrollTrigger[]>([])
175
+
176
+ const getLinkUrl = (item: MasonryItem): string | null => {
177
+ if (!item.linkType || item.linkType === 'none') return null
178
+
179
+ if (item.linkType === 'custom') {
180
+ return item.url || null
181
+ }
182
+
183
+ if (item.linkType === 'reference' && item.reference) {
184
+ const refValue = item.reference.value
185
+
186
+ const getCollectionPath = (relationTo: string) => {
187
+ if (relationTo === 'pages') return ''
188
+ return relationTo
189
+ }
190
+
191
+ const prefix = getCollectionPath(item.reference.relationTo)
192
+ const toUrl = (slug: string) => (prefix ? `/${prefix}/${slug}` : `/${slug}`)
193
+
194
+ if (typeof refValue === 'string') {
195
+ return toUrl(refValue)
196
+ }
197
+
198
+ if (refValue && typeof refValue === 'object' && 'slug' in refValue) {
199
+ return toUrl(String(refValue.slug))
200
+ }
201
+ }
202
+
203
+ return null
204
+ }
205
+
206
+ const handleItemClick = (item: MasonryItem) => {
207
+ const url = getLinkUrl(item)
208
+ if (!url) return
209
+
210
+ if (item.newTab) {
211
+ window.open(url, '_blank', 'noopener,noreferrer')
212
+ return
213
+ }
214
+
215
+ window.location.href = url
216
+ }
217
+
218
+ const blockId = useMemo(
219
+ () => advanced?.sectionId || `masonry-${Math.random().toString(36).slice(2, 11)}`,
220
+ [advanced?.sectionId],
221
+ )
222
+
223
+ const columns = useMedia(
224
+ ['(min-width:1440px)', '(min-width:968px)', '(min-width:640px)', '(min-width:420px)'],
225
+ [
226
+ layout?.columns?.desktop || 5,
227
+ layout?.columns?.laptop || 4,
228
+ layout?.columns?.tablet || 3,
229
+ layout?.columns?.mobile || 2,
230
+ ],
231
+ 1,
232
+ )
233
+
234
+ const [containerRef, { width }] = useMeasure<HTMLDivElement>()
235
+ const [mediaReady, setMediaReady] = useState(false)
236
+ const [containerHeight, setContainerHeight] = useState(0)
237
+ const animatedItems = useRef(new Set<string>())
238
+
239
+ const validItems = useMemo(() => {
240
+ return (
241
+ items?.map((item, index) => ({
242
+ ...item,
243
+ id: item.id || `item-${index}`,
244
+ })) || []
245
+ )
246
+ }, [items])
247
+
248
+ const getAspectRatioHeight = (ratio: string, width: number): number => {
249
+ if (!ratio || !ratio.includes('/')) return width
250
+
251
+ const [w, h] = ratio.split('/').map(Number)
252
+ const validW = w && !isNaN(w) ? w : 1
253
+ const validH = h && !isNaN(h) ? h : 1
254
+
255
+ return (width * validH) / validW
256
+ }
257
+
258
+ const preloadMedia = async (itemsToPreload: MasonryItem[]): Promise<void> => {
259
+ await Promise.all(
260
+ itemsToPreload.map((item) => {
261
+ if (item.media?.url && item.media.mimeType?.startsWith('image/')) {
262
+ return new Promise<void>((resolve) => {
263
+ const img = new Image()
264
+ img.src = item.media.url || ''
265
+ img.onload = img.onerror = () => resolve()
266
+ })
267
+ }
268
+
269
+ return Promise.resolve()
270
+ }),
271
+ )
272
+ }
273
+
274
+ const getInitialPosition = useCallback(
275
+ (item: any) => {
276
+ const containerRect = containerRef.current?.getBoundingClientRect()
277
+ if (!containerRect) return { x: item.x, y: item.y }
278
+
279
+ let direction = animation?.animateFrom || 'bottom'
280
+
281
+ if (direction === 'random') {
282
+ const dirs = ['top', 'bottom', 'left', 'right']
283
+ direction = dirs[Math.floor(Math.random() * dirs.length)] as typeof direction
284
+ }
285
+
286
+ switch (direction) {
287
+ case 'top':
288
+ return { x: item.x, y: -200 }
289
+ case 'bottom':
290
+ return { x: item.x, y: window.innerHeight + 200 }
291
+ case 'left':
292
+ return { x: -200, y: item.y }
293
+ case 'right':
294
+ return { x: window.innerWidth + 200, y: item.y }
295
+ case 'center':
296
+ return {
297
+ x: containerRect.width / 2 - item.w / 2,
298
+ y: containerRect.height / 2 - item.h / 2,
299
+ }
300
+ default:
301
+ return { x: item.x, y: item.y + 100 }
302
+ }
303
+ },
304
+ [containerRef, animation?.animateFrom],
305
+ )
306
+
307
+ useEffect(() => {
308
+ if (items && items.length > 0) {
309
+ const timeout = window.setTimeout(() => setMediaReady(true), 3000)
310
+
311
+ preloadMedia(items)
312
+ .then(() => setMediaReady(true))
313
+ .finally(() => window.clearTimeout(timeout))
314
+ } else {
315
+ setMediaReady(true)
316
+ }
317
+ }, [items])
318
+
319
+ const grid = useMemo(() => {
320
+ if (!width || !validItems.length) return { items: [], height: 0 }
321
+
322
+ const colHeights = new Array(columns).fill(0)
323
+ const gap = layout?.gap || 16
324
+ const totalGaps = (columns - 1) * gap
325
+ const columnWidth = (width - totalGaps) / columns
326
+
327
+ const gridItems = validItems.map((item) => {
328
+ const aspectRatio = item.aspectRatio || '1/1'
329
+ const [w, h] = aspectRatio.split('/').map(Number)
330
+ const validW = w && !isNaN(w) ? w : 1
331
+ const validH = h && !isNaN(h) ? h : 1
332
+ const ratio = validW / validH
333
+
334
+ let colSpan = 1
335
+
336
+ if (ratio >= 2) {
337
+ colSpan = Math.min(2, columns)
338
+ } else if (ratio >= 1.5) {
339
+ colSpan = Math.min(2, columns)
340
+ }
341
+
342
+ let bestCol = 0
343
+ let minHeight = Infinity
344
+
345
+ for (let col = 0; col <= columns - colSpan; col++) {
346
+ const maxHeightInSpan = Math.max(...colHeights.slice(col, col + colSpan))
347
+
348
+ if (maxHeightInSpan < minHeight) {
349
+ minHeight = maxHeightInSpan
350
+ bestCol = col
351
+ }
352
+ }
353
+
354
+ const itemWidth = columnWidth * colSpan + gap * (colSpan - 1)
355
+ const x = bestCol * (columnWidth + gap)
356
+ const height = getAspectRatioHeight(aspectRatio, itemWidth)
357
+ const y = Math.max(...colHeights.slice(bestCol, bestCol + colSpan))
358
+
359
+ for (let i = bestCol; i < bestCol + colSpan; i++) {
360
+ colHeights[i] = y + height + gap
361
+ }
362
+
363
+ return { ...item, x, y, w: itemWidth, h: height, colSpan }
364
+ })
365
+
366
+ const totalHeight = Math.max(...colHeights)
367
+
368
+ return { items: gridItems, height: totalHeight }
369
+ }, [columns, validItems, width, layout?.gap])
370
+
371
+ useEffect(() => {
372
+ setContainerHeight(grid.height)
373
+ }, [grid.height])
374
+
375
+ const animateItemIntoView = useCallback(
376
+ (element: HTMLElement, item: any, index: number, delay: number = 0) => {
377
+ const start = getInitialPosition(item)
378
+ const deltaX = start.x - item.x
379
+ const deltaY = start.y - item.y
380
+
381
+ gsap.fromTo(
382
+ element,
383
+ {
384
+ opacity: 0,
385
+ x: deltaX,
386
+ y: deltaY,
387
+ ...(interaction?.blurToFocus && { filter: 'blur(10px)' }),
388
+ },
389
+ {
390
+ opacity: 1,
391
+ x: 0,
392
+ y: 0,
393
+ ...(interaction?.blurToFocus && { filter: 'none' }),
394
+ duration: animation?.duration || 0.8,
395
+ ease: animation?.ease && animation.ease !== '0' ? animation.ease : 'power3.out',
396
+ delay: delay + index * (animation?.stagger || 0.05),
397
+ overwrite: 'auto',
398
+ },
399
+ )
400
+ },
401
+ [getInitialPosition, animation, interaction?.blurToFocus],
402
+ )
403
+
404
+ const hasMounted = useRef(false)
405
+
406
+ useLayoutEffect(() => {
407
+ if (!mediaReady || !items || items.length === 0) return
408
+
409
+ const blockEl = containerRef.current
410
+ if (!blockEl) return
411
+
412
+ triggersRef.current.forEach((t) => t.kill())
413
+ triggersRef.current = []
414
+
415
+ gsap.killTweensOf(`[data-block-id="${blockId}"] [data-key]`)
416
+
417
+ if (animation?.enableAnimation === false) {
418
+ grid.items.forEach((item) => {
419
+ const selector = `[data-key="${item.id}"]`
420
+ const element = blockEl.querySelector(selector) as HTMLElement | null
421
+ if (!element) return
422
+
423
+ gsap.set(element, {
424
+ opacity: 1,
425
+ x: 0,
426
+ y: 0,
427
+ left: item.x,
428
+ top: item.y,
429
+ width: item.w,
430
+ height: item.h,
431
+ filter: 'none',
432
+ })
433
+ })
434
+
435
+ return
436
+ }
437
+
438
+ grid.items.forEach((item, index) => {
439
+ const selector = `[data-key="${item.id}"]`
440
+ const element = blockEl.querySelector(selector) as HTMLElement | null
441
+ if (!element) return
442
+
443
+ const animProps = { left: item.x, top: item.y, width: item.w, height: item.h }
444
+
445
+ if (!hasMounted.current) {
446
+ gsap.set(element, {
447
+ opacity: 1,
448
+ x: 0,
449
+ y: 0,
450
+ width: item.w,
451
+ height: item.h,
452
+ filter: 'none',
453
+ })
454
+
455
+ const trigger = ScrollTrigger.create({
456
+ id: `masonry-item-${blockId}-${item.id}`,
457
+ trigger: element,
458
+ start: 'top 85%',
459
+ end: 'bottom 15%',
460
+ toggleActions: animation?.repeatOnScroll
461
+ ? 'play none none reverse'
462
+ : 'play none none none',
463
+ onEnter: () => {
464
+ if (!animatedItems.current.has(item.id) || animation?.repeatOnScroll) {
465
+ animateItemIntoView(element, item, index)
466
+
467
+ if (!animation?.repeatOnScroll) {
468
+ animatedItems.current.add(item.id)
469
+ }
470
+ }
471
+ },
472
+ onLeave: () => {
473
+ if (animation?.repeatOnScroll) {
474
+ gsap.to(element, {
475
+ opacity: 0.3,
476
+ duration: 0.3,
477
+ ease: 'power2.out',
478
+ })
479
+ }
480
+ },
481
+ onEnterBack: () => {
482
+ if (animation?.repeatOnScroll) {
483
+ gsap.to(element, {
484
+ opacity: 1,
485
+ duration: 0.3,
486
+ ease: 'power2.out',
487
+ })
488
+ }
489
+ },
490
+ onLeaveBack: () => {
491
+ if (animation?.repeatOnScroll) {
492
+ gsap.to(element, {
493
+ opacity: 0.3,
494
+ duration: 0.3,
495
+ ease: 'power2.out',
496
+ })
497
+ }
498
+ },
499
+ })
500
+
501
+ triggersRef.current.push(trigger)
502
+ } else {
503
+ gsap.to(element, {
504
+ ...animProps,
505
+ x: 0,
506
+ y: 0,
507
+ duration: animation?.duration || 0.6,
508
+ ease: animation?.ease && animation.ease !== '0' ? animation.ease : 'power3.out',
509
+ overwrite: 'auto',
510
+ })
511
+ }
512
+ })
513
+
514
+ hasMounted.current = true
515
+
516
+ return () => {
517
+ triggersRef.current.forEach((t) => t.kill())
518
+ triggersRef.current = []
519
+ }
520
+ }, [grid, mediaReady, animation, interaction?.blurToFocus, items, animateItemIntoView])
521
+
522
+ useEffect(() => {
523
+ return () => {
524
+ ScrollTrigger.getAll().forEach((trigger) => {
525
+ if (trigger.vars.id?.startsWith(`masonry-item-${blockId}-`)) {
526
+ trigger.kill()
527
+ }
528
+ })
529
+
530
+ gsap.killTweensOf(`[data-block-id="${blockId}"] [data-key]`)
531
+ }
532
+ }, [blockId])
533
+
534
+ const handleMouseEnter = (_itemId: string, element: HTMLElement) => {
535
+ if (interaction?.scaleOnHover) {
536
+ gsap.to(element, {
537
+ scale: interaction.hoverScale || 0.95,
538
+ duration: 0.3,
539
+ ease: 'power2.out',
540
+ filter: 'brightness(0.75)',
541
+ })
542
+ }
543
+
544
+ if (interaction?.colorShiftOnHover) {
545
+ const overlay = element.querySelector('.color-overlay') as HTMLElement
546
+ if (overlay) gsap.to(overlay, { opacity: 0.3, duration: 0.3 })
547
+ }
548
+ }
549
+
550
+ const handleMouseLeave = (_itemId: string, element: HTMLElement) => {
551
+ if (interaction?.scaleOnHover) {
552
+ gsap.to(element, {
553
+ scale: 1,
554
+ duration: 0.3,
555
+ ease: 'power2.out',
556
+ filter: 'none',
557
+ })
558
+ }
559
+
560
+ if (interaction?.colorShiftOnHover) {
561
+ const overlay = element.querySelector('.color-overlay') as HTMLElement
562
+ if (overlay) gsap.to(overlay, { opacity: 0, duration: 0.3 })
563
+ }
564
+ }
565
+
566
+ if (!items || items.length === 0) {
567
+ return <div className="p-4">No items to display</div>
568
+ }
569
+
570
+ return (
571
+ <div
572
+ id={advanced?.sectionId}
573
+ data-block-id={blockId}
574
+ className={cn(
575
+ 'masonary-media-block relative min-w-full w-full',
576
+ advanced?.customClassName,
577
+ )}
578
+ ref={containerRef}
579
+ style={{
580
+ height: containerHeight > 0 ? `${containerHeight}px` : 'auto',
581
+ minHeight: containerHeight > 0 ? `${containerHeight}px` : 'auto',
582
+ }}
583
+ {...(advanced?.enableParallax && {
584
+ 'data-scroll': true,
585
+ 'data-scroll-speed': advanced.parallaxSpeed || 0.5,
586
+ })}
587
+ >
588
+ {!mediaReady &&
589
+ grid.items.map((item) => (
590
+ <div
591
+ key={`skeleton-${item.id}`}
592
+ className="absolute rounded-md overflow-hidden border border-border"
593
+ style={{
594
+ left: item.x,
595
+ top: item.y,
596
+ width: item.w,
597
+ height: item.h,
598
+ }}
599
+ >
600
+ <div className="shimmer-effect w-full h-full bg-zinc-200/10 dark:bg-zinc-800/10" />
601
+ </div>
602
+ ))}
603
+
604
+ {mediaReady &&
605
+ grid.items.map((item) => {
606
+ const hasLink = getLinkUrl(item) !== null
607
+
608
+ return (
609
+ <div
610
+ key={item.id}
611
+ data-key={item.id}
612
+ className={cn(
613
+ 'absolute box-content rounded-md overflow-hidden border border-border shadow-2xl',
614
+ hasLink && 'cursor-pointer',
615
+ )}
616
+ style={{
617
+ left: item.x,
618
+ top: item.y,
619
+ width: item.w,
620
+ height: item.h,
621
+ opacity: 1,
622
+ willChange: 'transform, opacity, width, height',
623
+ }}
624
+ onClick={() => handleItemClick(item)}
625
+ onMouseEnter={(e) => handleMouseEnter(item.id, e.currentTarget)}
626
+ onMouseLeave={(e) => handleMouseLeave(item.id, e.currentTarget)}
627
+ >
628
+ <div className="group relative w-full h-full">
629
+ <MediaContent item={item} />
630
+
631
+ {item.showTextOverlay && item.textOverlay && (
632
+ <span className="masonary-card-text-overlay absolute inset-0 z-10 m-auto flex h-full w-full items-center justify-center p-4 px-2 text-center text-sm text-white [text-shadow:2px_2px_2px_black] shadow-[0_0_0px_black] transition-all duration-300 hover:scale-110 hover:brightness-110 group-hover:[&_img]:brightness-[0.15]">
633
+ {item.textOverlay}
634
+ </span>
635
+ )}
636
+
637
+ {interaction?.colorShiftOnHover && (
638
+ <FadeUpOnInView delay={0.1} className="absolute inset-0">
639
+ <div
640
+ className="color-overlay absolute inset-0 rounded-sm opacity-0 pointer-events-none"
641
+ style={{
642
+ background: `linear-gradient(to top right, ${
643
+ interaction.customHoverColors?.fromColor || 'rgba(236, 72, 153, 0.5)'
644
+ }, ${interaction.customHoverColors?.toColor || 'rgba(14, 165, 233, 0.5)'})`,
645
+ }}
646
+ />
647
+ </FadeUpOnInView>
648
+ )}
649
+ </div>
650
+ </div>
651
+ )
652
+ })}
653
+ </div>
654
+ )
655
+ }