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
@@ -0,0 +1,244 @@
1
+ 'use client'
2
+ import { cn } from '@/utilities/ui'
3
+ import React, { Fragment } from 'react'
4
+
5
+ import { AnimatePresence } from 'framer-motion'
6
+ import { FadeOnScroll } from './utilities/FadeOnScroll'
7
+ import { extractCustomCSS } from './utilities/renderCustomCSS'
8
+ import ParallaxWrapper from './utilities/ParallaxWrapper'
9
+ import { blockComponents } from './blockComponents'
10
+
11
+ interface BlockProps {
12
+ sectionId?: string
13
+ disableInnerContainer?: boolean
14
+
15
+ scrollBehavior?: 'normal' | 'stickToTop' | 'stickToTopFade' | 'parallax'
16
+
17
+ sticky?: boolean
18
+ stickyTopOffset?: number
19
+ stickyZIndex?: number
20
+
21
+ fadeOnScroll?: boolean
22
+ useAbsoluteScroll?: boolean
23
+ fadeStartProgress?: number
24
+ fadeStartPixels?: number
25
+ fadeEndProgress?: number
26
+ fadeEndPixels?: number
27
+ fadeFinalOpacity?: number
28
+ fadeOffsetStart?: 'start start' | 'start center' | 'start end' | `start ${number}%`
29
+ fadeOffsetEnd?: 'start start' | 'start center' | 'start end' | `start ${number}%`
30
+
31
+ parallaxEnabled?: boolean
32
+ parallaxDistance?: number
33
+ parallaxDirection?: 'up' | 'down'
34
+ showScrollDebugMarkers?: boolean
35
+ gsapSelector?: string
36
+ pinTargetSelector?: string
37
+ pinSpacing?: number
38
+ scrollDuration?: number
39
+ scrollSpeed?: number
40
+
41
+ sectionStyle?: React.CSSProperties
42
+
43
+ initialYFadeValue?: number
44
+ isFadeOnce?: boolean
45
+ fadeStiffValue?: number
46
+ fadeDampValue?: number
47
+ exitYFadeValue?: number | string
48
+
49
+ [key: string]: any
50
+ }
51
+
52
+ const toNumber = (value: unknown, fallback: number): number => {
53
+ if (typeof value === 'number' && Number.isFinite(value)) return value
54
+
55
+ if (typeof value === 'string') {
56
+ const parsed = Number(value)
57
+ if (Number.isFinite(parsed)) return parsed
58
+ }
59
+
60
+ return fallback
61
+ }
62
+
63
+ export const RenderBlocks: React.FC<{
64
+ blocks: Array<BlockProps & { blockType: string }>
65
+ }> = (props) => {
66
+ const { blocks } = props
67
+
68
+ if (!blocks || !Array.isArray(blocks) || blocks.length === 0) return null
69
+
70
+ const customCSS = extractCustomCSS(blocks as any)
71
+
72
+ return (
73
+ <>
74
+ {customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
75
+
76
+ {blocks.map((block, index) => {
77
+ const { blockType } = block
78
+
79
+ if (!blockType || !(blockType in blockComponents)) return null
80
+
81
+ const Block = blockComponents[blockType]
82
+ if (!Block) return null
83
+
84
+ const blockProps = block as BlockProps
85
+
86
+ const key =
87
+ 'id' in (block as any) && (block as any).id
88
+ ? `block-${(block as any).id}-${blockType}`
89
+ : `block-${index}-${blockType}`
90
+
91
+ const sectionId = blockProps.sectionId
92
+ const scrollSpeed = blockProps.scrollSpeed
93
+
94
+ const scrollBehavior = blockProps.scrollBehavior || 'normal'
95
+
96
+ const isProjectsSliderSticky =
97
+ blockType === 'projectsSlider' && blockProps.stickyToTop === true
98
+
99
+ const isSticky =
100
+ scrollBehavior === 'stickToTop' ||
101
+ scrollBehavior === 'stickToTopFade' ||
102
+ blockProps.sticky === true
103
+
104
+ const isStickyFade =
105
+ scrollBehavior === 'stickToTopFade' ||
106
+ (blockProps.sticky === true && blockProps.fadeOnScroll === true)
107
+
108
+ const isParallax = scrollBehavior === 'parallax' || blockProps.parallaxEnabled === true
109
+
110
+ const stickyTopOffset = toNumber(blockProps.stickyTopOffset, 0)
111
+ const stickyZIndex = toNumber(blockProps.stickyZIndex, 5)
112
+
113
+ const sectionStyle =
114
+ blockProps.sectionStyle && typeof blockProps.sectionStyle === 'object'
115
+ ? (blockProps.sectionStyle as React.CSSProperties)
116
+ : undefined
117
+
118
+ const wrapperlessBlockTypes = new Set([
119
+ 'asideNav',
120
+ 'studioHorizontalScroll',
121
+ 'studioStackCards',
122
+ ])
123
+
124
+ const shouldRenderWrapperless =
125
+ wrapperlessBlockTypes.has(blockType) ||
126
+ (blockType === 'projectsSlider' && blockProps.stickyToTop === true)
127
+
128
+ if (shouldRenderWrapperless) {
129
+ return <Block suppressHydrationWarning key={key} {...blockProps} disableInnerContainer />
130
+ }
131
+
132
+ const blockElement = <Block {...blockProps} disableInnerContainer />
133
+ const isNineCanvasBlock = blockType === 'nineCanvasBlock'
134
+
135
+ const sharedDataProps = {
136
+ suppressHydrationWarning: true,
137
+ id: isNineCanvasBlock ? 'ninecanvas' : sectionId,
138
+ 'data-block-type': blockType,
139
+ 'data-ninecanvas-gesture-owner': blockType === 'DD-Carousel-A' ? true : undefined,
140
+ 'data-projects-slider-sticky': isProjectsSliderSticky ? 'true' : undefined,
141
+ 'data-scroll': scrollSpeed ? true : undefined,
142
+ 'data-scroll-speed': scrollSpeed ? String(scrollSpeed) : undefined,
143
+ }
144
+
145
+ if (isParallax) {
146
+ return (
147
+ <AnimatePresence key={key} mode="sync">
148
+ <section
149
+ {...sharedDataProps}
150
+ className={cn('m-0 renderblock-container relative overflow-visible', sectionId)}
151
+ style={sectionStyle}
152
+ >
153
+ <ParallaxWrapper
154
+ sectionId={sectionId}
155
+ animationConfig={{
156
+ scrollToSelector: blockProps.scrollToSelector,
157
+ triggerSelector: blockProps.gsapSelector,
158
+ pinTargetSelector: blockProps.pinTargetSelector,
159
+ pinSpacing: blockProps.pinSpacing,
160
+ scrollDuration: blockProps.scrollDuration,
161
+ parallaxDistance: blockProps.parallaxDistance ?? 120,
162
+ parallaxDirection: blockProps.parallaxDirection ?? 'up',
163
+ markers: blockProps.showScrollDebugMarkers,
164
+ }}
165
+ >
166
+ {blockElement}
167
+ </ParallaxWrapper>
168
+ </section>
169
+ </AnimatePresence>
170
+ )
171
+ }
172
+
173
+ if (isStickyFade) {
174
+ return (
175
+ <AnimatePresence key={key} mode="sync">
176
+ <section
177
+ {...sharedDataProps}
178
+ className={cn('m-0 renderblock-container sticky overflow-visible', sectionId)}
179
+ style={{
180
+ top: `${stickyTopOffset}px`,
181
+ zIndex: stickyZIndex,
182
+ ...sectionStyle,
183
+ }}
184
+ >
185
+ <FadeOnScroll
186
+ startFade={
187
+ blockProps.useAbsoluteScroll
188
+ ? (blockProps.fadeStartPixels ?? 1000)
189
+ : (blockProps.fadeStartProgress ?? 0)
190
+ }
191
+ endFade={
192
+ blockProps.useAbsoluteScroll
193
+ ? (blockProps.fadeEndPixels ?? 2000)
194
+ : (blockProps.fadeEndProgress ?? 0.75)
195
+ }
196
+ finalOpacity={blockProps.fadeFinalOpacity ?? 0}
197
+ offsetStart={blockProps.fadeOffsetStart ?? 'start 50%'}
198
+ offsetEnd={blockProps.fadeOffsetEnd ?? 'start 10%'}
199
+ useAbsoluteScroll={blockProps.useAbsoluteScroll ?? false}
200
+ >
201
+ {blockElement}
202
+ </FadeOnScroll>
203
+ </section>
204
+ </AnimatePresence>
205
+ )
206
+ }
207
+
208
+ if (isSticky) {
209
+ return (
210
+ <AnimatePresence key={key} mode="sync">
211
+ <section
212
+ {...sharedDataProps}
213
+ className={cn('m-0 renderblock-container sticky overflow-visible', sectionId)}
214
+ style={{
215
+ top: `${stickyTopOffset}px`,
216
+ zIndex: stickyZIndex,
217
+ ...sectionStyle,
218
+ }}
219
+ >
220
+ {blockElement}
221
+ </section>
222
+ </AnimatePresence>
223
+ )
224
+ }
225
+
226
+ return (
227
+ <AnimatePresence key={key} mode="sync">
228
+ <section
229
+ {...sharedDataProps}
230
+ className={cn(
231
+ 'm-0 renderblock-container relative overflow-visible',
232
+ isNineCanvasBlock && 'ninecanvas',
233
+ sectionId,
234
+ )}
235
+ style={sectionStyle}
236
+ >
237
+ {blockElement}
238
+ </section>
239
+ </AnimatePresence>
240
+ )
241
+ })}
242
+ </>
243
+ )
244
+ }
@@ -0,0 +1,6 @@
1
+ // Demo-only child registry. The original HZ registry is retained in source/.
2
+ import { Content } from '../Content/config'
3
+ import { DDCarouselA } from '../DD-Carousel-A/config'
4
+ import { DDProgress } from '../DD-Progress/config'
5
+ import { RawHtmlBlock } from '../rawHtml/config'
6
+ export const allowedCanvasBlocks = [Content, DDCarouselA, DDProgress, RawHtmlBlock]
@@ -0,0 +1,8 @@
1
+ import dynamic from 'next/dynamic'
2
+ import type { ComponentType } from 'react'
3
+ export const blockComponents: Record<string, ComponentType<any>> = {
4
+ content: dynamic(() => import('../Content/Component').then((m) => m.ContentBlock)),
5
+ 'DD-Carousel-A': dynamic(() => import('../DD-Carousel-A/Component').then((m) => m.DDCarouselA)),
6
+ 'DD-Progress': dynamic(() => import('../DD-Progress/Component').then((m) => m.DDProgress)),
7
+ rawHtml: dynamic(() => import('../rawHtml/Component').then((m) => m.RawHtmlBlock)),
8
+ }
@@ -0,0 +1,316 @@
1
+ 'use client'
2
+ import * as React from 'react'
3
+ import { getClientPerformanceMode } from '../utilities/webglCleanup'
4
+
5
+ export type CanvasPanel = {
6
+ id: string
7
+ x: number
8
+ y: number
9
+ label: string
10
+ content?: React.ReactNode
11
+ }
12
+
13
+ type CanvasGridProps = {
14
+ activeCanvas: CanvasPanel
15
+ canvases: CanvasPanel[]
16
+ onCanvasChange: (canvasId: string) => void
17
+ }
18
+
19
+ export function CanvasGrid({ activeCanvas, canvases, onCanvasChange }: CanvasGridProps) {
20
+ const trackRef = React.useRef<HTMLDivElement>(null)
21
+ const touchStartRef = React.useRef<{
22
+ x: number
23
+ y: number
24
+ lastY: number
25
+ target: EventTarget | null
26
+ canvasGestureAllowed: boolean
27
+ scrolled?: boolean
28
+ } | null>(null)
29
+ const activeContentRef = React.useRef<HTMLDivElement | null>(null)
30
+
31
+ React.useEffect(() => {
32
+ const el = activeContentRef.current
33
+ if (!el) return
34
+
35
+ const reducedPerformance = getClientPerformanceMode() === 'reduced'
36
+ if (reducedPerformance) {
37
+ const previousOverflowY = el.style.overflowY
38
+ el.style.overflowY = 'auto'
39
+ window.dispatchEvent(new CustomEvent('locomotive:resize'))
40
+ return () => {
41
+ el.style.overflowY = previousOverflowY
42
+ }
43
+ }
44
+
45
+ let cancelled = false
46
+ let scroll: { destroy: () => void } | undefined
47
+ let secondFrame = 0
48
+
49
+ const firstFrame = window.requestAnimationFrame(() => {
50
+ secondFrame = window.requestAnimationFrame(async () => {
51
+ const { default: LocomotiveScroll } = await import('locomotive-scroll')
52
+ if (cancelled) return
53
+
54
+ scroll = new LocomotiveScroll({
55
+ lenisOptions: { wrapper: el, content: el, eventsTarget: el, lerp: 0.08 },
56
+ })
57
+ })
58
+ })
59
+
60
+ return () => {
61
+ cancelled = true
62
+ window.cancelAnimationFrame(firstFrame)
63
+ window.cancelAnimationFrame(secondFrame)
64
+ scroll?.destroy()
65
+ }
66
+ }, [activeCanvas.id])
67
+
68
+ React.useEffect(() => {
69
+ const track = trackRef.current
70
+ if (!track) return
71
+
72
+ const getActivePanel = (target: EventTarget | null) => {
73
+ if (!(target instanceof Element)) return null
74
+
75
+ const panel = target.closest<HTMLElement>('.hzCanvasPanel--active')
76
+ return panel && track.contains(panel) ? panel : null
77
+ }
78
+
79
+ const getScrollableRegions = (panel: HTMLElement, target: EventTarget | null) => {
80
+ const regions: HTMLElement[] = []
81
+ let element =
82
+ target instanceof HTMLElement
83
+ ? target
84
+ : target instanceof Element
85
+ ? target.parentElement
86
+ : null
87
+
88
+ while (element && element !== panel) {
89
+ const { overflowY } = window.getComputedStyle(element)
90
+ const canOverflow = /^(auto|scroll|overlay)$/.test(overflowY)
91
+
92
+ if (canOverflow && element.scrollHeight > element.clientHeight) {
93
+ regions.push(element)
94
+ }
95
+
96
+ element = element.parentElement
97
+ }
98
+
99
+ const canvasContent = panel.querySelector<HTMLElement>('.hzCanvasContent')
100
+ if (canvasContent && !regions.includes(canvasContent)) regions.push(canvasContent)
101
+
102
+ return regions
103
+ }
104
+
105
+ const scrollElement = (element: HTMLElement, deltaY: number) => {
106
+ const maxScrollTop = element.scrollHeight - element.clientHeight
107
+ if (maxScrollTop <= 0) return false
108
+
109
+ const nextScrollTop = Math.min(Math.max(element.scrollTop + deltaY, 0), maxScrollTop)
110
+ if (nextScrollTop === element.scrollTop) return false
111
+
112
+ element.scrollTop = nextScrollTop
113
+ return true
114
+ }
115
+
116
+ const scrollFromTarget = (panel: HTMLElement, target: EventTarget | null, deltaY: number) =>
117
+ getScrollableRegions(panel, target).some((region) => scrollElement(region, deltaY))
118
+
119
+ const interactiveGestureSelector = [
120
+ '.carousel',
121
+ '.carouselList',
122
+ '.carouselNav',
123
+ '[class*="carousel-"]',
124
+ '[class*="Carousel"]',
125
+ '.embla',
126
+ '.embla__viewport',
127
+ '.embla__container',
128
+ '.swiper',
129
+ '.swiper-container',
130
+ '.swiper-wrapper',
131
+ '.swiper-slide',
132
+ '.ultimate-embla-carousel',
133
+ '.ultimate-embla-viewport',
134
+ '.ultimate-embla-container',
135
+ '[data-carousel-id]',
136
+ '[data-projects-slider-scroll-scene]',
137
+ '[data-ninecanvas-gesture-owner]',
138
+ 'button',
139
+ 'a',
140
+ 'input',
141
+ 'select',
142
+ 'textarea',
143
+ '[role="button"]',
144
+ '[role="slider"]',
145
+ '[contenteditable="true"]',
146
+ ].join(',')
147
+
148
+ const canUseCanvasGesture = (panel: HTMLElement, target: EventTarget | null) => {
149
+ if (!(target instanceof Element)) return false
150
+
151
+ const gestureOwner = target.closest(interactiveGestureSelector)
152
+ return !gestureOwner || !panel.contains(gestureOwner)
153
+ }
154
+
155
+ const getAdjacentCanvas = (deltaX: number, deltaY: number) => {
156
+ const absX = Math.abs(deltaX)
157
+ const absY = Math.abs(deltaY)
158
+ const xOffset = absX > absY ? (deltaX < 0 ? 1 : -1) : 0
159
+ const yOffset = absY >= absX ? (deltaY > 0 ? -1 : 1) : 0
160
+
161
+ return canvases.find(
162
+ (canvas) => canvas.x === activeCanvas.x + xOffset && canvas.y === activeCanvas.y + yOffset,
163
+ )
164
+ }
165
+
166
+ const changeCanvasFromTouch = (deltaX: number, deltaY: number) => {
167
+ const nextCanvas = getAdjacentCanvas(deltaX, deltaY)
168
+ if (!nextCanvas) return false
169
+
170
+ onCanvasChange(nextCanvas.id)
171
+ return true
172
+ }
173
+
174
+ const notifyCanvasScrollDown = (deltaY: number) => {
175
+ if (deltaY > 0) {
176
+ window.dispatchEvent(new CustomEvent('hz:canvas-scroll-down'))
177
+ }
178
+ }
179
+
180
+ const handleWheel = (event: WheelEvent) => {
181
+ const panel = getActivePanel(event.target)
182
+ if (!panel) return
183
+
184
+ const deltaMultiplier =
185
+ event.deltaMode === WheelEvent.DOM_DELTA_LINE
186
+ ? 16
187
+ : event.deltaMode === WheelEvent.DOM_DELTA_PAGE
188
+ ? panel.clientHeight
189
+ : 1
190
+
191
+ const deltaY = event.deltaY * deltaMultiplier
192
+
193
+ if (!scrollFromTarget(panel, event.target, deltaY)) return
194
+
195
+ notifyCanvasScrollDown(deltaY)
196
+
197
+ event.preventDefault()
198
+ event.stopPropagation()
199
+ }
200
+
201
+ const handleTouchStart = (event: TouchEvent) => {
202
+ const panel = getActivePanel(event.target)
203
+ const touch = event.touches[0]
204
+ if (!panel || !touch) return
205
+
206
+ touchStartRef.current = {
207
+ x: touch.clientX,
208
+ y: touch.clientY,
209
+ lastY: touch.clientY,
210
+ target: event.target,
211
+ canvasGestureAllowed: canUseCanvasGesture(panel, event.target),
212
+ }
213
+ }
214
+
215
+ const handleTouchMove = (event: TouchEvent) => {
216
+ const panel = getActivePanel(event.target)
217
+ const touch = event.touches[0]
218
+ const touchStart = touchStartRef.current
219
+ if (!panel || !touchStart || !touch) return
220
+
221
+ const deltaY = touchStart.lastY - touch.clientY
222
+ touchStart.lastY = touch.clientY
223
+
224
+ if (scrollFromTarget(panel, event.target, deltaY)) {
225
+ touchStart.scrolled = true
226
+ notifyCanvasScrollDown(deltaY)
227
+
228
+ event.preventDefault()
229
+ event.stopPropagation()
230
+ }
231
+ }
232
+
233
+ const handleTouchEnd = (event: TouchEvent) => {
234
+ const panel =
235
+ getActivePanel(event.target) ?? getActivePanel(touchStartRef.current?.target ?? null)
236
+ const touchStart = touchStartRef.current
237
+ touchStartRef.current = null
238
+
239
+ if (!panel || !touchStart || !touchStart.canvasGestureAllowed) return
240
+
241
+ const touch = event.changedTouches[0]
242
+ if (!touch) return
243
+
244
+ const deltaX = touch.clientX - touchStart.x
245
+ const deltaY = touchStart.y - touch.clientY
246
+ const absX = Math.abs(deltaX)
247
+ const absY = Math.abs(deltaY)
248
+ const isHorizontalSwipe = absX >= 56 && absX > absY * 1.25
249
+ const isVerticalSwipe = absY >= 56 && absY > absX * 1.25
250
+
251
+ if (!isHorizontalSwipe && !isVerticalSwipe) return
252
+ // A vertical gesture consumed by nested scrolling must not also change panels.
253
+ if (isVerticalSwipe && touchStart.scrolled) return
254
+
255
+ if (changeCanvasFromTouch(deltaX, deltaY)) {
256
+ event.preventDefault()
257
+ event.stopPropagation()
258
+ }
259
+ }
260
+
261
+ const handleTouchCancel = () => {
262
+ touchStartRef.current = null
263
+ }
264
+ track.addEventListener('wheel', handleWheel, { passive: false })
265
+ track.addEventListener('touchstart', handleTouchStart, { passive: true })
266
+ track.addEventListener('touchmove', handleTouchMove, { passive: false })
267
+ track.addEventListener('touchend', handleTouchEnd, { passive: false })
268
+ track.addEventListener('touchcancel', handleTouchCancel, { passive: true })
269
+
270
+ return () => {
271
+ track.removeEventListener('wheel', handleWheel)
272
+ track.removeEventListener('touchstart', handleTouchStart)
273
+ track.removeEventListener('touchmove', handleTouchMove)
274
+ track.removeEventListener('touchend', handleTouchEnd)
275
+ track.removeEventListener('touchcancel', handleTouchCancel)
276
+ }
277
+ }, [activeCanvas, canvases, onCanvasChange])
278
+
279
+ return (
280
+ <div ref={trackRef} className={`hzCanvasTrack hzCanvasTrack--${activeCanvas.id}`}>
281
+ {canvases.map((canvas) => {
282
+ const isActive = canvas.id === activeCanvas.id
283
+
284
+ return (
285
+ <section
286
+ aria-label={canvas.label}
287
+ aria-hidden={!isActive}
288
+ inert={!isActive}
289
+ className={[
290
+ 'hzCanvasPanel',
291
+ isActive ? 'hzCanvasPanel--active' : '',
292
+ `hzCanvasPanel--x-${canvas.x === -1 ? 'neg1' : canvas.x === 1 ? 'pos1' : '0'}`,
293
+ `hzCanvasPanel--y-${canvas.y === -1 ? 'neg1' : canvas.y === 1 ? 'pos1' : '0'}`,
294
+ ]
295
+ .filter(Boolean)
296
+ .join(' ')}
297
+ data-active={isActive ? 'true' : undefined}
298
+ key={canvas.id}
299
+ onClick={() => onCanvasChange(canvas.id)}
300
+ >
301
+ <div
302
+ ref={isActive ? activeContentRef : undefined}
303
+ className="hzCanvasContent"
304
+ data-canvas-content={canvas.id}
305
+ data-scroll-container={isActive ? true : undefined}
306
+ onClick={(event) => event.stopPropagation()}
307
+ >
308
+ {canvas.content ||
309
+ (isActive ? <p className="hzCanvasEmpty">{canvas.label}</p> : null)}
310
+ </div>
311
+ </section>
312
+ )
313
+ })}
314
+ </div>
315
+ )
316
+ }