blocks-dusted 0.1.11 → 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 (161) hide show
  1. package/package.json +5 -2
  2. package/src/cli.js +39 -3
  3. package/src/commands/shop.js +59 -0
  4. package/src/commands/warehouse.js +73 -0
  5. package/src/installers/patchRenderBlocks.js +1 -1
  6. package/src/registry/validateTemplateManifest.js +2 -1
  7. package/templates/blocks/BannerDD790432/Component.tsx +63 -0
  8. package/templates/blocks/BannerDD790432/config.ts +54 -0
  9. package/templates/blocks/BannerDD790432/manifest.json +80 -0
  10. package/templates/blocks/Button47/Component.tsx +124 -0
  11. package/templates/blocks/Button47/config.ts +75 -0
  12. package/templates/blocks/Button47/link.ts +22 -0
  13. package/templates/blocks/Button47/manifest.json +105 -0
  14. package/templates/blocks/Button47/styles.module.scss +75 -0
  15. package/templates/blocks/DD-Accordion/Component.tsx +55 -36
  16. package/templates/blocks/DD-Accordion/config.ts +71 -59
  17. package/templates/blocks/DD-AnimText/Component.tsx +50 -32
  18. package/templates/blocks/DD-AnimText/config.ts +296 -295
  19. package/templates/blocks/DD-AsideNav/Component.tsx +323 -319
  20. package/templates/blocks/DD-Banner/Component.tsx +54 -51
  21. package/templates/blocks/DD-BookCall/Component.tsx +10 -9
  22. package/templates/blocks/DD-ButtonPop/Component.tsx +118 -117
  23. package/templates/blocks/DD-Card/Component.tsx +4 -4
  24. package/templates/blocks/DD-CardTemplate01/Component.tsx +5 -5
  25. package/templates/blocks/DD-CardsWithVariantV3/Component.tsx +472 -0
  26. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/index.tsx +203 -0
  27. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/styles.ts +87 -0
  28. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/useInjectStyle.ts +39 -0
  29. package/templates/blocks/DD-CardsWithVariantV3/config.ts +326 -0
  30. package/templates/blocks/DD-CardsWithVariantV3/manifest.json +110 -0
  31. package/templates/blocks/DD-Carousel-A/Component.tsx +251 -249
  32. package/templates/blocks/DD-Carousel-B/Component.tsx +405 -403
  33. package/templates/blocks/DD-Chip/Component.tsx +14 -14
  34. package/templates/blocks/DD-ComparisonTable/Component.tsx +255 -256
  35. package/templates/blocks/DD-Contact/Component.tsx +433 -434
  36. package/templates/blocks/DD-Counter/Component.tsx +18 -13
  37. package/templates/blocks/DD-DeliCard/Component.tsx +303 -0
  38. package/templates/blocks/DD-DeliCard/config.ts +186 -0
  39. package/templates/blocks/DD-DeliCard/manifest.json +94 -0
  40. package/templates/blocks/DD-FeatCat/Component.tsx +302 -302
  41. package/templates/blocks/DD-FeatProd/CircularGallery.tsx +1179 -0
  42. package/templates/blocks/DD-FeatProd/Component.tsx +484 -0
  43. package/templates/blocks/DD-FeatProd/config.ts +173 -0
  44. package/templates/blocks/DD-FeatProd/manifest.json +93 -0
  45. package/templates/blocks/DD-FeatServ/Component.tsx +607 -0
  46. package/templates/blocks/DD-FeatServ/config.ts +213 -0
  47. package/templates/blocks/DD-FeatServ/manifest.json +113 -0
  48. package/templates/blocks/DD-FeatStrip/Component.tsx +584 -585
  49. package/templates/blocks/DD-FluidBtn/ButtonsEyeCatching.tsx +627 -628
  50. package/templates/blocks/DD-FluidBtn/Component.tsx +46 -43
  51. package/templates/blocks/DD-GooeyInfo/Component.tsx +150 -149
  52. package/templates/blocks/DD-GooeyInfo/config.ts +136 -135
  53. package/templates/blocks/DD-Hero/Component.tsx +16 -40
  54. package/templates/blocks/DD-HeroPlas/Component.tsx +50 -0
  55. package/templates/blocks/DD-HeroPlas/ThreeScene.tsx +109 -0
  56. package/templates/blocks/DD-HeroPlas/config.ts +33 -0
  57. package/templates/blocks/DD-HeroPlas/manifest.json +88 -0
  58. package/templates/blocks/DD-HorizontalScroll/Component.tsx +4 -5
  59. package/templates/blocks/DD-Icon/Component.tsx +46 -81
  60. package/templates/blocks/DD-IconButton/Component.tsx +13 -20
  61. package/templates/blocks/DD-IdTag/Component.tsx +22 -21
  62. package/templates/blocks/DD-IframeMedia/Component.tsx +14 -10
  63. package/templates/blocks/DD-LineAnim/Component.tsx +42 -36
  64. package/templates/blocks/DD-LogoMarq/Component.tsx +38 -48
  65. package/templates/blocks/DD-Lottie/Component.tsx +11 -3
  66. package/templates/blocks/DD-MasonaryMedia/Component.tsx +544 -547
  67. package/templates/blocks/DD-MasonaryMedia/config.ts +389 -389
  68. package/templates/blocks/DD-Masonry/Component.tsx +654 -644
  69. package/templates/blocks/DD-Masonry/config.ts +475 -475
  70. package/templates/blocks/DD-MaterialSwatches/Component.tsx +85 -0
  71. package/templates/blocks/DD-MaterialSwatches/config.ts +232 -0
  72. package/templates/blocks/DD-MaterialSwatches/manifest.json +84 -0
  73. package/templates/blocks/DD-MaterialSwatches/material.ts +107 -0
  74. package/templates/blocks/DD-NineCanvas/Component.tsx +129 -0
  75. package/templates/blocks/DD-NineCanvas/RenderBlocks.tsx +244 -0
  76. package/templates/blocks/DD-NineCanvas/allowedCanvasBlocks.ts +6 -0
  77. package/templates/blocks/DD-NineCanvas/blockComponents.tsx +8 -0
  78. package/templates/blocks/DD-NineCanvas/canvas/CanvasGrid.tsx +316 -0
  79. package/templates/blocks/DD-NineCanvas/canvas/RadialNav.tsx +432 -0
  80. package/templates/blocks/DD-NineCanvas/canvas/canvas-panels.css +889 -0
  81. package/templates/blocks/DD-NineCanvas/canvas/icons/custom-icons.tsx +364 -0
  82. package/templates/blocks/DD-NineCanvas/config.ts +61 -0
  83. package/templates/blocks/DD-NineCanvas/manifest.json +165 -0
  84. package/templates/blocks/DD-NineCanvas/styles.scss +152 -0
  85. package/templates/blocks/DD-NineCanvas/tooltip/get-strict-context.tsx +26 -0
  86. package/templates/blocks/DD-NineCanvas/tooltip/primitives.tsx +178 -0
  87. package/templates/blocks/DD-NineCanvas/tooltip/tooltip.tsx +71 -0
  88. package/templates/blocks/DD-NineCanvas/tooltip/use-controlled-state.tsx +33 -0
  89. package/templates/blocks/DD-NineCanvas/utilities/FadeOnScroll.tsx +68 -0
  90. package/templates/blocks/DD-NineCanvas/utilities/ParallaxWrapper.tsx +157 -0
  91. package/templates/blocks/DD-NineCanvas/utilities/renderCustomCSS.ts +5 -0
  92. package/templates/blocks/DD-NineCanvas/utilities/webglCleanup.ts +27 -0
  93. package/templates/blocks/DD-PayloadAIAssistant/Component.tsx +78 -0
  94. package/templates/blocks/DD-PayloadAIAssistant/DustedBot.tsx +590 -0
  95. package/templates/blocks/DD-PayloadAIAssistant/config.ts +37 -0
  96. package/templates/blocks/DD-PayloadAIAssistant/demo.css +125 -0
  97. package/templates/blocks/DD-PayloadAIAssistant/manifest.json +93 -0
  98. package/templates/blocks/DD-PayloadAIAssistant/source-defaults.ts +17 -0
  99. package/templates/blocks/DD-PayloadAIAssistant/styles.ts +306 -0
  100. package/templates/blocks/DD-Pricing/Component.tsx +368 -372
  101. package/templates/blocks/DD-Pricing/config.ts +215 -215
  102. package/templates/blocks/DD-Process/Component.tsx +147 -149
  103. package/templates/blocks/DD-Progress/Component.tsx +156 -141
  104. package/templates/blocks/DD-ProjHeader/Component.tsx +93 -91
  105. package/templates/blocks/DD-ProjSlider/Component.tsx +1 -1
  106. package/templates/blocks/DD-ProjSlider/config.ts +2 -2
  107. package/templates/blocks/DD-Section/Component.tsx +265 -266
  108. package/templates/blocks/DD-Separator/Component.tsx +66 -66
  109. package/templates/blocks/DD-Services/Component.tsx +176 -176
  110. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +306 -307
  111. package/templates/blocks/DD-Slider-A/Component.tsx +237 -237
  112. package/templates/blocks/DD-StackCards/Component.tsx +474 -473
  113. package/templates/blocks/DD-Team/Component.tsx +247 -247
  114. package/templates/blocks/DD-TeamCards/Component.tsx +251 -251
  115. package/templates/blocks/DD-TechStack/Component.tsx +161 -168
  116. package/templates/blocks/DD-TestMarq/Component.tsx +158 -132
  117. package/templates/blocks/DD-Testim/Component.tsx +247 -247
  118. package/templates/blocks/DD-Testim2/Component.tsx +265 -256
  119. package/templates/blocks/DD-Testimonials/Component.tsx +139 -147
  120. package/templates/blocks/DD-TextMarq/Component.tsx +157 -157
  121. package/templates/blocks/DD-Thanks/Component.tsx +501 -500
  122. package/templates/blocks/DD-Work/Component.tsx +315 -315
  123. package/templates/blocks/DD-Work-B/Component.tsx +294 -294
  124. package/templates/blocks/DDHG-CTASection2/Component.tsx +183 -184
  125. package/templates/blocks/DDHG-HighlightArc/Component.tsx +246 -243
  126. package/templates/blocks/DDHG-LogoMarquee/Component.tsx +180 -93
  127. package/templates/blocks/DDHG-LogoMarquee/config.ts +154 -66
  128. package/templates/blocks/DDHG-NavCodeGrid/Component.tsx +225 -213
  129. package/templates/blocks/DDHG-ProcessSec/Component.tsx +38 -39
  130. package/templates/blocks/DDHG-ServiceDetails/Component.tsx +11 -11
  131. package/templates/blocks/DDHG-ServiceDetails/ServiceDetailCard.tsx +30 -47
  132. package/templates/blocks/DDHG-ServiceHero/Component.tsx +10 -10
  133. package/templates/blocks/DDHG-ServiceOverview/Component.tsx +23 -23
  134. package/templates/blocks/DDHG-StatsFAQ/Component.tsx +40 -40
  135. package/templates/blocks/DDHG-TeamCards/Component.tsx +140 -131
  136. package/templates/blocks/Form/Checkbox/index.tsx +45 -0
  137. package/templates/blocks/Form/Component.tsx +163 -0
  138. package/templates/blocks/Form/Country/index.tsx +65 -0
  139. package/templates/blocks/Form/Country/options.ts +982 -0
  140. package/templates/blocks/Form/Email/index.tsx +38 -0
  141. package/templates/blocks/Form/Error/index.tsx +15 -0
  142. package/templates/blocks/Form/Message/index.tsx +13 -0
  143. package/templates/blocks/Form/Number/index.tsx +36 -0
  144. package/templates/blocks/Form/Select/index.tsx +63 -0
  145. package/templates/blocks/Form/State/index.tsx +64 -0
  146. package/templates/blocks/Form/State/options.ts +52 -0
  147. package/templates/blocks/Form/Text/index.tsx +32 -0
  148. package/templates/blocks/Form/Textarea/index.tsx +40 -0
  149. package/templates/blocks/Form/Width/index.tsx +13 -0
  150. package/templates/blocks/Form/config.ts +51 -0
  151. package/templates/blocks/Form/fields.tsx +21 -0
  152. package/templates/blocks/Form/manifest.json +168 -0
  153. package/templates/blocks/MediaBlock/Component.tsx +67 -0
  154. package/templates/blocks/MediaBlock/config.ts +14 -0
  155. package/templates/blocks/MediaBlock/manifest.json +92 -0
  156. package/templates/blocks/TransitionsDustedShowcase/Component.tsx +9 -0
  157. package/templates/blocks/TransitionsDustedShowcase/config.ts +13 -0
  158. package/templates/blocks/TransitionsDustedShowcase/manifest.json +80 -0
  159. package/templates/blocks/rawHtml/Component.tsx +25 -0
  160. package/templates/blocks/rawHtml/config.ts +56 -0
  161. package/templates/blocks/rawHtml/manifest.json +80 -0
@@ -0,0 +1,1179 @@
1
+ 'use client'
2
+
3
+ import { Camera, Mesh, Plane, Program, Renderer, Texture, Transform } from 'ogl'
4
+ import { useCallback, useEffect, useRef } from 'react'
5
+
6
+ type GL = Renderer['gl']
7
+ type ScrollDirection = 'right' | 'left'
8
+
9
+ interface GalleryScrollState {
10
+ current: number
11
+ target: number
12
+ direction: ScrollDirection
13
+ }
14
+
15
+ function debounce<T extends (...args: any[]) => void>(func: T, wait: number) {
16
+ let timeout: number
17
+
18
+ return function (this: any, ...args: Parameters<T>) {
19
+ window.clearTimeout(timeout)
20
+ timeout = window.setTimeout(() => func.apply(this, args), wait)
21
+ }
22
+ }
23
+
24
+ function lerp(p1: number, p2: number, t: number): number {
25
+ return p1 + (p2 - p1) * t
26
+ }
27
+
28
+ function autoBind(instance: any): void {
29
+ const proto = Object.getPrototypeOf(instance)
30
+
31
+ Object.getOwnPropertyNames(proto).forEach((key) => {
32
+ if (key !== 'constructor' && typeof instance[key] === 'function') {
33
+ instance[key] = instance[key].bind(instance)
34
+ }
35
+ })
36
+ }
37
+
38
+ const DEFAULT_FONT = 'bold 64px Poppins'
39
+ const DEFAULT_FONT_URL =
40
+ 'https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap'
41
+
42
+ const CHEVRON_VIEWBOX_WIDTH = 125
43
+ const CHEVRON_VIEWBOX_HEIGHT = 128
44
+
45
+ function deriveFontFamilyFromUrl(url: string): string {
46
+ const fileName = (url.split('/').pop() || 'custom-font').split('?')[0]
47
+ const base = fileName.replace(/\.(woff2?|ttf|otf|eot)$/i, '')
48
+
49
+ return base.replace(/[^a-zA-Z0-9-_ ]/g, '').trim() || 'CircularGalleryFont'
50
+ }
51
+
52
+ async function loadFontFromStylesheet(url: string): Promise<string> {
53
+ const response = await fetch(url)
54
+
55
+ if (!response.ok) {
56
+ throw new Error(`Failed to fetch font stylesheet (${response.status})`)
57
+ }
58
+
59
+ const cssText = await response.text()
60
+ const faceBlocks = cssText.match(/@font-face\s*{[^}]*}/g) || []
61
+
62
+ let family: string | null = null
63
+ const fontFaces: FontFace[] = []
64
+
65
+ for (const block of faceBlocks) {
66
+ const familyMatch = block.match(/font-family:\s*['"]?([^;'"]+)['"]?/)
67
+ const urlMatch = block.match(/url\(\s*['"]?([^'")]+)['"]?\s*\)/)
68
+
69
+ if (!familyMatch || !urlMatch) continue
70
+
71
+ family = familyMatch[1].trim()
72
+
73
+ const descriptors: FontFaceDescriptors = {}
74
+ const weightMatch = block.match(/font-weight:\s*([^;]+);/)
75
+ const styleMatch = block.match(/font-style:\s*([^;]+);/)
76
+ const rangeMatch = block.match(/unicode-range:\s*([^;]+);/)
77
+
78
+ if (weightMatch) descriptors.weight = weightMatch[1].trim()
79
+ if (styleMatch) descriptors.style = styleMatch[1].trim()
80
+ if (rangeMatch) descriptors.unicodeRange = rangeMatch[1].trim()
81
+
82
+ fontFaces.push(new FontFace(family, `url(${urlMatch[1]})`, descriptors))
83
+ }
84
+
85
+ if (!family) {
86
+ throw new Error('No @font-face rule found in the stylesheet')
87
+ }
88
+
89
+ await Promise.allSettled(
90
+ fontFaces.map(async (face) => {
91
+ await face.load()
92
+ document.fonts.add(face)
93
+ }),
94
+ )
95
+
96
+ return family
97
+ }
98
+
99
+ async function loadFontFromFile(url: string): Promise<string> {
100
+ const family = deriveFontFamilyFromUrl(url)
101
+ const fontFace = new FontFace(family, `url(${url})`)
102
+
103
+ await fontFace.load()
104
+ document.fonts.add(fontFace)
105
+
106
+ return family
107
+ }
108
+
109
+ async function loadCustomFont(fontUrl: string): Promise<string> {
110
+ const isStylesheet = fontUrl.includes('fonts.googleapis.com') || /\.css(\?.*)?$/i.test(fontUrl)
111
+
112
+ return isStylesheet ? loadFontFromStylesheet(fontUrl) : loadFontFromFile(fontUrl)
113
+ }
114
+
115
+ async function resolveFont(font: string, fontUrl?: string): Promise<string> {
116
+ const effectiveUrl = fontUrl || (font === DEFAULT_FONT ? DEFAULT_FONT_URL : null)
117
+
118
+ if (!effectiveUrl) {
119
+ if (document.fonts && document.fonts.load) {
120
+ try {
121
+ await document.fonts.load(font)
122
+ await document.fonts.ready
123
+ } catch {}
124
+ }
125
+
126
+ return font
127
+ }
128
+
129
+ try {
130
+ const family = await loadCustomFont(effectiveUrl)
131
+ const sizeMatch = font.match(/^\s*(.*?\d+px)/)
132
+ const prefix = sizeMatch ? sizeMatch[1].trim() : 'bold 64px'
133
+ const resolved = `${prefix} "${family}"`
134
+
135
+ if (document.fonts && document.fonts.load) {
136
+ try {
137
+ await document.fonts.load(resolved)
138
+ } catch {}
139
+ }
140
+
141
+ return resolved
142
+ } catch (error) {
143
+ console.error('CircularGallery: unable to load font from', fontUrl, error)
144
+ return font
145
+ }
146
+ }
147
+
148
+ function getFontSize(font: string): number {
149
+ const match = font.match(/(\d+)px/)
150
+ return match ? parseInt(match[1], 10) : 64
151
+ }
152
+
153
+ function createTextTexture(
154
+ gl: GL,
155
+ text: string,
156
+ font: string = 'bold 64px monospace',
157
+ color: string = '#292524',
158
+ ): { texture: Texture; width: number; height: number } {
159
+ const canvas = document.createElement('canvas')
160
+ const context = canvas.getContext('2d')
161
+
162
+ if (!context) throw new Error('Could not get 2d context')
163
+
164
+ const pixelRatio = Math.min(window.devicePixelRatio || 1, 2)
165
+ const fontSize = getFontSize(font)
166
+
167
+ context.font = font
168
+
169
+ const metrics = context.measureText(text)
170
+ const width = Math.ceil(metrics.width + 40)
171
+ const height = Math.ceil(fontSize * 1.4)
172
+
173
+ canvas.width = width * pixelRatio
174
+ canvas.height = height * pixelRatio
175
+ canvas.style.width = `${width}px`
176
+ canvas.style.height = `${height}px`
177
+
178
+ context.scale(pixelRatio, pixelRatio)
179
+ context.font = font
180
+ context.fillStyle = color
181
+ context.textBaseline = 'middle'
182
+ context.textAlign = 'center'
183
+ context.clearRect(0, 0, width, height)
184
+ context.fillText(text, width / 2, height / 2)
185
+
186
+ const texture = new Texture(gl, {
187
+ generateMipmaps: false,
188
+ })
189
+
190
+ texture.image = canvas
191
+
192
+ return {
193
+ texture,
194
+ width,
195
+ height,
196
+ }
197
+ }
198
+ function createTransparentPlaceholderCanvas(): HTMLCanvasElement {
199
+ const canvas = document.createElement('canvas')
200
+
201
+ canvas.width = 1
202
+ canvas.height = 1
203
+
204
+ return canvas
205
+ }
206
+
207
+ interface TitleProps {
208
+ gl: GL
209
+ plane: Mesh
210
+ renderer: Renderer
211
+ text: string
212
+ textColor?: string
213
+ font?: string
214
+ }
215
+
216
+ class Title {
217
+ gl: GL
218
+ plane: Mesh
219
+ renderer: Renderer
220
+ text: string
221
+ textColor: string
222
+ font: string
223
+ mesh!: Mesh
224
+ textureWidth: number = 1
225
+ textureHeight: number = 1
226
+
227
+ constructor({
228
+ gl,
229
+ plane,
230
+ renderer,
231
+ text,
232
+ textColor = '#57534e',
233
+ font = '64px sans-serif',
234
+ }: TitleProps) {
235
+ autoBind(this)
236
+
237
+ this.gl = gl
238
+ this.plane = plane
239
+ this.renderer = renderer
240
+ this.text = text
241
+ this.textColor = textColor
242
+ this.font = font
243
+
244
+ this.createMesh()
245
+ }
246
+
247
+ createMesh() {
248
+ const { texture, width, height } = createTextTexture(
249
+ this.gl,
250
+ this.text,
251
+ this.font,
252
+ this.textColor,
253
+ )
254
+
255
+ this.textureWidth = width
256
+ this.textureHeight = height
257
+
258
+ const geometry = new Plane(this.gl)
259
+
260
+ const program = new Program(this.gl, {
261
+ vertex: `
262
+ attribute vec3 position;
263
+ attribute vec2 uv;
264
+
265
+ uniform mat4 modelViewMatrix;
266
+ uniform mat4 projectionMatrix;
267
+
268
+ varying vec2 vUv;
269
+
270
+ void main() {
271
+ vUv = uv;
272
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
273
+ }
274
+ `,
275
+ fragment: `
276
+ precision highp float;
277
+
278
+ uniform sampler2D tMap;
279
+
280
+ varying vec2 vUv;
281
+
282
+ void main() {
283
+ vec4 color = texture2D(tMap, vUv);
284
+
285
+ if (color.a < 0.1) {
286
+ discard;
287
+ }
288
+
289
+ gl_FragColor = color;
290
+ }
291
+ `,
292
+ uniforms: {
293
+ tMap: {
294
+ value: texture,
295
+ },
296
+ },
297
+ transparent: true,
298
+ })
299
+
300
+ this.mesh = new Mesh(this.gl, {
301
+ geometry,
302
+ program,
303
+ })
304
+
305
+ this.mesh.setParent(this.plane)
306
+ this.onResize()
307
+ }
308
+
309
+ onResize() {
310
+ const aspect = this.textureWidth / this.textureHeight
311
+ const desiredHeight = this.plane.scale.y * 0.15
312
+ const desiredWidth = desiredHeight * aspect
313
+
314
+ this.mesh.scale.set(desiredWidth / this.plane.scale.x, desiredHeight / this.plane.scale.y, 1)
315
+
316
+ const localTextHeight = desiredHeight / this.plane.scale.y
317
+
318
+ this.mesh.position.y = -0.5 - localTextHeight * 0.5 - 0.5 / this.plane.scale.y
319
+ }
320
+ }
321
+
322
+ interface ScreenSize {
323
+ width: number
324
+ height: number
325
+ }
326
+
327
+ interface Viewport {
328
+ width: number
329
+ height: number
330
+ }
331
+
332
+ interface MediaProps {
333
+ geometry: Plane
334
+ gl: GL
335
+ image: string
336
+ index: number
337
+ length: number
338
+ renderer: Renderer
339
+ scene: Transform
340
+ screen: ScreenSize
341
+ text: string
342
+ viewport: Viewport
343
+ bend: number
344
+ textColor: string
345
+ borderRadius?: number
346
+ font?: string
347
+ }
348
+
349
+ class Media {
350
+ extra: number = 0
351
+ geometry: Plane
352
+ gl: GL
353
+ image: string
354
+ index: number
355
+ length: number
356
+ renderer: Renderer
357
+ scene: Transform
358
+ screen: ScreenSize
359
+ text: string
360
+ viewport: Viewport
361
+ bend: number
362
+ textColor: string
363
+ borderRadius: number
364
+ font?: string
365
+ program!: Program
366
+ plane!: Mesh
367
+ title!: Title
368
+ scale!: number
369
+ padding!: number
370
+ width!: number
371
+ widthTotal!: number
372
+ x!: number
373
+ speed: number = 0
374
+ isBefore: boolean = false
375
+ isAfter: boolean = false
376
+
377
+ constructor({
378
+ geometry,
379
+ gl,
380
+ image,
381
+ index,
382
+ length,
383
+ renderer,
384
+ scene,
385
+ screen,
386
+ text,
387
+ viewport,
388
+ bend,
389
+ textColor,
390
+ borderRadius = 0,
391
+ font,
392
+ }: MediaProps) {
393
+ this.geometry = geometry
394
+ this.gl = gl
395
+ this.image = image
396
+ this.index = index
397
+ this.length = length
398
+ this.renderer = renderer
399
+ this.scene = scene
400
+ this.screen = screen
401
+ this.text = text
402
+ this.viewport = viewport
403
+ this.bend = bend
404
+ this.textColor = textColor
405
+ this.borderRadius = borderRadius
406
+ this.font = font
407
+
408
+ this.createShader()
409
+ this.createMesh()
410
+ this.createTitle()
411
+ this.onResize()
412
+ this.plane.scale.y = (this.viewport.height * (700 * this.scale)) / this.screen.height
413
+ this.plane.scale.x = (this.viewport.width * (1000 * this.scale)) / this.screen.width
414
+ this.title?.onResize()
415
+ }
416
+
417
+ createShader() {
418
+ const texture = new Texture(this.gl, {
419
+ image: createTransparentPlaceholderCanvas(),
420
+ generateMipmaps: true,
421
+ })
422
+
423
+ this.program = new Program(this.gl, {
424
+ depthTest: false,
425
+ depthWrite: false,
426
+ vertex: `
427
+ precision highp float;
428
+
429
+ attribute vec3 position;
430
+ attribute vec2 uv;
431
+
432
+ uniform mat4 modelViewMatrix;
433
+ uniform mat4 projectionMatrix;
434
+
435
+ varying vec2 vUv;
436
+
437
+ void main() {
438
+ vUv = uv;
439
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
440
+ }
441
+ `,
442
+ fragment: `
443
+ precision highp float;
444
+
445
+ uniform vec2 uImageSizes;
446
+ uniform vec2 uPlaneSizes;
447
+ uniform sampler2D tMap;
448
+ uniform float uBorderRadius;
449
+
450
+ varying vec2 vUv;
451
+
452
+ float roundedBoxSDF(vec2 p, vec2 b, float r) {
453
+ vec2 d = abs(p) - b;
454
+
455
+ return length(max(d, vec2(0.0))) +
456
+ min(max(d.x, d.y), 0.0) -
457
+ r;
458
+ }
459
+
460
+ void main() {
461
+ vec2 ratio = vec2(
462
+ min(
463
+ (uPlaneSizes.x / uPlaneSizes.y) /
464
+ (uImageSizes.x / uImageSizes.y),
465
+ 1.0
466
+ ),
467
+ min(
468
+ (uPlaneSizes.y / uPlaneSizes.x) /
469
+ (uImageSizes.y / uImageSizes.x),
470
+ 1.0
471
+ )
472
+ );
473
+
474
+ vec2 uv = vec2(
475
+ vUv.x * ratio.x + (1.0 - ratio.x) * 0.5,
476
+ vUv.y * ratio.y + (1.0 - ratio.y) * 0.5
477
+ );
478
+
479
+ vec4 color = texture2D(tMap, uv);
480
+
481
+ float d = roundedBoxSDF(
482
+ vUv - 0.5,
483
+ vec2(0.5 - uBorderRadius),
484
+ uBorderRadius
485
+ );
486
+
487
+ float edgeSmooth = 0.02;
488
+ float alpha = 1.0 - smoothstep(-edgeSmooth, edgeSmooth, d);
489
+
490
+ gl_FragColor = vec4(color.rgb, alpha);
491
+ }
492
+ `,
493
+ uniforms: {
494
+ tMap: {
495
+ value: texture,
496
+ },
497
+ uPlaneSizes: {
498
+ value: [0, 0],
499
+ },
500
+ uImageSizes: {
501
+ value: [0, 0],
502
+ },
503
+ uBorderRadius: {
504
+ value: this.borderRadius,
505
+ },
506
+ },
507
+ transparent: true,
508
+ })
509
+
510
+ const img = new Image()
511
+
512
+ img.crossOrigin = 'anonymous'
513
+ img.src = this.image
514
+
515
+ img.onload = () => {
516
+ texture.image = img
517
+
518
+ this.program.uniforms.uImageSizes.value = [img.naturalWidth, img.naturalHeight]
519
+ }
520
+ }
521
+
522
+ createMesh() {
523
+ this.plane = new Mesh(this.gl, {
524
+ geometry: this.geometry,
525
+ program: this.program,
526
+ })
527
+
528
+ this.plane.setParent(this.scene)
529
+ }
530
+
531
+ createTitle() {
532
+ this.title = new Title({
533
+ gl: this.gl,
534
+ plane: this.plane,
535
+ renderer: this.renderer,
536
+ text: this.text,
537
+ textColor: this.textColor,
538
+ font: this.font,
539
+ })
540
+ }
541
+
542
+ update(
543
+ scroll: {
544
+ current: number
545
+ last: number
546
+ },
547
+ direction: ScrollDirection,
548
+ ) {
549
+ this.plane.position.x = this.x - scroll.current - this.extra
550
+
551
+ const x = this.plane.position.x
552
+ const halfViewportWidth = this.viewport.width / 2
553
+
554
+ if (this.bend === 0) {
555
+ this.plane.position.y = 0
556
+ this.plane.rotation.z = 0
557
+ } else {
558
+ const absoluteBend = Math.abs(this.bend)
559
+ const radius =
560
+ (halfViewportWidth * halfViewportWidth + absoluteBend * absoluteBend) / (2 * absoluteBend)
561
+
562
+ const effectiveX = Math.min(Math.abs(x), halfViewportWidth)
563
+ const arc = radius - Math.sqrt(radius * radius - effectiveX * effectiveX)
564
+
565
+ if (this.bend > 0) {
566
+ this.plane.position.y = -arc
567
+ this.plane.rotation.z = -Math.sign(x) * Math.asin(effectiveX / radius)
568
+ } else {
569
+ this.plane.position.y = arc
570
+ this.plane.rotation.z = Math.sign(x) * Math.asin(effectiveX / radius)
571
+ }
572
+ }
573
+
574
+ this.speed = scroll.current - scroll.last
575
+
576
+ const planeOffset = this.plane.scale.x / 2
577
+ const viewportOffset = this.viewport.width / 2
578
+
579
+ this.isBefore = this.plane.position.x + planeOffset < -viewportOffset
580
+
581
+ this.isAfter = this.plane.position.x - planeOffset > viewportOffset
582
+
583
+ if (direction === 'right' && this.isBefore) {
584
+ this.extra -= this.widthTotal
585
+ this.isBefore = false
586
+ this.isAfter = false
587
+ }
588
+
589
+ if (direction === 'left' && this.isAfter) {
590
+ this.extra += this.widthTotal
591
+ this.isBefore = false
592
+ this.isAfter = false
593
+ }
594
+ }
595
+
596
+ onResize({
597
+ screen,
598
+ viewport,
599
+ }: {
600
+ screen?: ScreenSize
601
+ viewport?: Viewport
602
+ } = {}) {
603
+ if (screen) {
604
+ this.screen = screen
605
+ }
606
+
607
+ if (viewport) {
608
+ this.viewport = viewport
609
+
610
+ if (this.plane.program.uniforms.uViewportSizes) {
611
+ this.plane.program.uniforms.uViewportSizes.value = [
612
+ this.viewport.width,
613
+ this.viewport.height,
614
+ ]
615
+ }
616
+ }
617
+
618
+ this.scale = this.screen.height / 1500
619
+
620
+ this.plane.scale.y = (this.viewport.height * (700 * this.scale)) / this.screen.height
621
+
622
+ this.plane.scale.x = (this.viewport.width * (1000 * this.scale)) / this.screen.width
623
+
624
+ this.plane.program.uniforms.uPlaneSizes.value = [this.plane.scale.x, this.plane.scale.y]
625
+
626
+ this.padding = 2
627
+ this.width = this.plane.scale.x + this.padding
628
+ this.widthTotal = this.width * this.length
629
+ this.x = this.width * this.index
630
+ }
631
+ }
632
+
633
+ interface AppConfig {
634
+ items?: {
635
+ image: string
636
+ text: string
637
+ }[]
638
+ bend?: number
639
+ textColor?: string
640
+ borderRadius?: number
641
+ font?: string
642
+ scrollSpeed?: number
643
+ scrollEase?: number
644
+ onItemClick?: (index: number) => void
645
+ onScrollChange?: (state: GalleryScrollState) => void
646
+ }
647
+
648
+ class App {
649
+ container: HTMLElement
650
+ scrollSpeed: number
651
+ scroll: {
652
+ ease: number
653
+ current: number
654
+ target: number
655
+ last: number
656
+ position?: number
657
+ }
658
+ onCheckDebounce: (...args: any[]) => void
659
+ renderer!: Renderer
660
+ gl!: GL
661
+ camera!: Camera
662
+ scene!: Transform
663
+ planeGeometry!: Plane
664
+ medias: Media[] = []
665
+ mediasImages: {
666
+ image: string
667
+ text: string
668
+ }[] = []
669
+ screen!: {
670
+ width: number
671
+ height: number
672
+ }
673
+ viewport!: {
674
+ width: number
675
+ height: number
676
+ }
677
+ raf: number = 0
678
+ boundOnResize!: () => void
679
+ boundOnWheel!: (e: Event) => void
680
+ boundOnTouchDown!: (e: MouseEvent | TouchEvent) => void
681
+ boundOnTouchMove!: (e: MouseEvent | TouchEvent) => void
682
+ boundOnTouchUp!: (e: MouseEvent | TouchEvent) => void
683
+ isDown: boolean = false
684
+ start: number = 0
685
+ dragDistance: number = 0
686
+ onItemClick?: (index: number) => void
687
+ onScrollChange?: (state: GalleryScrollState) => void
688
+ originalLength: number = 0
689
+
690
+ constructor(
691
+ container: HTMLElement,
692
+ {
693
+ items,
694
+ bend = 1,
695
+ textColor = '#eee',
696
+ borderRadius = 0,
697
+ font = 'semibold 64px Poppins',
698
+ scrollSpeed = 2,
699
+ scrollEase = 0.05,
700
+ onItemClick,
701
+ onScrollChange,
702
+ }: AppConfig,
703
+ ) {
704
+ this.container = container
705
+ this.scrollSpeed = scrollSpeed
706
+ this.scroll = {
707
+ ease: scrollEase,
708
+ current: 0,
709
+ target: 0,
710
+ last: 0,
711
+ }
712
+
713
+ this.onCheckDebounce = debounce(this.onCheck.bind(this), 200)
714
+ this.onItemClick = onItemClick
715
+ this.onScrollChange = onScrollChange
716
+
717
+ this.createRenderer()
718
+ this.createCamera()
719
+ this.createScene()
720
+ this.onResize()
721
+ this.createGeometry()
722
+ this.createMedias(items, bend, textColor, borderRadius, font)
723
+ this.update()
724
+ this.addEventListeners()
725
+ }
726
+
727
+ createRenderer() {
728
+ this.renderer = new Renderer({
729
+ alpha: true,
730
+ antialias: true,
731
+ dpr: Math.min(window.devicePixelRatio || 1, 2),
732
+ })
733
+
734
+ this.gl = this.renderer.gl
735
+ this.gl.clearColor(0, 0, 0, 0)
736
+
737
+ this.container.appendChild(this.renderer.gl.canvas as HTMLCanvasElement)
738
+ }
739
+
740
+ createCamera() {
741
+ this.camera = new Camera(this.gl)
742
+ this.camera.fov = 45
743
+ this.camera.position.z = 20
744
+ }
745
+
746
+ createScene() {
747
+ this.scene = new Transform()
748
+ }
749
+
750
+ createGeometry() {
751
+ this.planeGeometry = new Plane(this.gl, {
752
+ heightSegments: 50,
753
+ widthSegments: 100,
754
+ })
755
+ }
756
+
757
+ createMedias(
758
+ items:
759
+ | {
760
+ image: string
761
+ text: string
762
+ }[]
763
+ | undefined,
764
+ bend: number,
765
+ textColor: string,
766
+ borderRadius: number,
767
+ font: string,
768
+ ) {
769
+ const galleryItems = items && items.length ? items : []
770
+
771
+ this.originalLength = galleryItems.length
772
+ this.mediasImages = galleryItems.concat(galleryItems)
773
+
774
+ this.medias = this.mediasImages.map(
775
+ (data, index) =>
776
+ new Media({
777
+ geometry: this.planeGeometry,
778
+ gl: this.gl,
779
+ image: data.image,
780
+ index,
781
+ length: this.mediasImages.length,
782
+ renderer: this.renderer,
783
+ scene: this.scene,
784
+ screen: this.screen,
785
+ text: data.text,
786
+ viewport: this.viewport,
787
+ bend,
788
+ textColor,
789
+ borderRadius,
790
+ font,
791
+ }),
792
+ )
793
+ }
794
+
795
+ onTouchDown(e: MouseEvent | TouchEvent) {
796
+ this.isDown = true
797
+ this.scroll.position = this.scroll.current
798
+ this.start = 'touches' in e ? e.touches[0].clientX : e.clientX
799
+ }
800
+
801
+ onTouchMove(e: MouseEvent | TouchEvent) {
802
+ if (!this.isDown) return
803
+
804
+ const x = 'touches' in e ? e.touches[0].clientX : e.clientX
805
+
806
+ this.dragDistance = Math.abs(x - this.start)
807
+
808
+ const distance = (this.start - x) * (this.scrollSpeed * 0.025)
809
+
810
+ this.scroll.target = (this.scroll.position ?? 0) + distance
811
+ }
812
+
813
+ onTouchUp(e: MouseEvent | TouchEvent) {
814
+ if (!this.isDown) return
815
+
816
+ const wasTap = this.dragDistance < 5
817
+
818
+ this.isDown = false
819
+ this.dragDistance = 0
820
+
821
+ // A click should only select the card. Snapping here changes the OGL
822
+ // scroll target at the same moment the parent carousel updates.
823
+ if (!wasTap) {
824
+ this.onCheck()
825
+ }
826
+
827
+ if (wasTap && this.onItemClick && this.medias.length > 0) {
828
+ const pointerX =
829
+ 'changedTouches' in e ? e.changedTouches[0].clientX : (e as MouseEvent).clientX
830
+
831
+ const rect = this.container.getBoundingClientRect()
832
+ const normalizedX = (pointerX - rect.left) / this.screen.width - 0.5
833
+
834
+ const worldX = normalizedX * this.viewport.width
835
+
836
+ let closest = this.medias[0]
837
+ let closestDistance = Math.abs(closest.plane.position.x - worldX)
838
+
839
+ for (const media of this.medias) {
840
+ const distance = Math.abs(media.plane.position.x - worldX)
841
+
842
+ if (distance < closestDistance) {
843
+ closestDistance = distance
844
+ closest = media
845
+ }
846
+ }
847
+
848
+ const index = this.originalLength > 0 ? closest.index % this.originalLength : closest.index
849
+
850
+ this.onItemClick(index)
851
+ }
852
+ }
853
+
854
+ onWheel(e: Event) {
855
+ const wheelEvent = e as WheelEvent
856
+ const delta = wheelEvent.deltaY || (wheelEvent as any).wheelDelta || (wheelEvent as any).detail
857
+
858
+ this.scroll.target += (delta > 0 ? this.scrollSpeed : -this.scrollSpeed) * 0.2
859
+
860
+ this.onCheckDebounce()
861
+ }
862
+
863
+ onCheck() {
864
+ if (!this.medias || !this.medias[0]) return
865
+
866
+ const width = this.medias[0].width
867
+ const itemIndex = Math.round(Math.abs(this.scroll.target) / width)
868
+
869
+ const item = width * itemIndex
870
+
871
+ this.scroll.target = this.scroll.target < 0 ? -item : item
872
+ }
873
+
874
+ onResize() {
875
+ this.screen = {
876
+ width: Math.max(1, this.container.clientWidth),
877
+ height: Math.max(1, this.container.clientHeight),
878
+ }
879
+
880
+ this.renderer.setSize(this.screen.width, this.screen.height)
881
+
882
+ this.camera.perspective({
883
+ aspect: this.screen.width / this.screen.height,
884
+ })
885
+
886
+ const fov = (this.camera.fov * Math.PI) / 180
887
+ const height = 2 * Math.tan(fov / 2) * this.camera.position.z
888
+
889
+ const width = height * this.camera.aspect
890
+
891
+ this.viewport = {
892
+ width,
893
+ height,
894
+ }
895
+
896
+ if (this.medias) {
897
+ this.medias.forEach((media) =>
898
+ media.onResize({
899
+ screen: this.screen,
900
+ viewport: this.viewport,
901
+ }),
902
+ )
903
+ }
904
+ }
905
+
906
+ update() {
907
+ this.scroll.current = lerp(this.scroll.current, this.scroll.target, this.scroll.ease)
908
+
909
+ const direction: ScrollDirection = this.scroll.current > this.scroll.last ? 'right' : 'left'
910
+
911
+ if (this.medias) {
912
+ this.medias.forEach((media) => media.update(this.scroll, direction))
913
+ }
914
+
915
+ this.onScrollChange?.({
916
+ current: this.scroll.current,
917
+ target: this.scroll.target,
918
+ direction,
919
+ })
920
+
921
+ this.renderer.render({
922
+ scene: this.scene,
923
+ camera: this.camera,
924
+ })
925
+
926
+ this.scroll.last = this.scroll.current
927
+
928
+ this.raf = window.requestAnimationFrame(this.update.bind(this))
929
+ }
930
+
931
+ addEventListeners() {
932
+ this.boundOnResize = this.onResize.bind(this)
933
+ this.boundOnWheel = this.onWheel.bind(this)
934
+ this.boundOnTouchDown = this.onTouchDown.bind(this)
935
+ this.boundOnTouchMove = this.onTouchMove.bind(this)
936
+ this.boundOnTouchUp = this.onTouchUp.bind(this)
937
+
938
+ window.addEventListener('resize', this.boundOnResize)
939
+
940
+ window.addEventListener('mousewheel', this.boundOnWheel)
941
+
942
+ window.addEventListener('wheel', this.boundOnWheel)
943
+
944
+ window.addEventListener('mousedown', this.boundOnTouchDown)
945
+
946
+ window.addEventListener('mousemove', this.boundOnTouchMove)
947
+
948
+ window.addEventListener('mouseup', this.boundOnTouchUp)
949
+
950
+ window.addEventListener('touchstart', this.boundOnTouchDown)
951
+
952
+ window.addEventListener('touchmove', this.boundOnTouchMove)
953
+
954
+ window.addEventListener('touchend', this.boundOnTouchUp)
955
+ }
956
+
957
+ destroy() {
958
+ window.cancelAnimationFrame(this.raf)
959
+
960
+ window.removeEventListener('resize', this.boundOnResize)
961
+
962
+ window.removeEventListener('mousewheel', this.boundOnWheel)
963
+
964
+ window.removeEventListener('wheel', this.boundOnWheel)
965
+
966
+ window.removeEventListener('mousedown', this.boundOnTouchDown)
967
+
968
+ window.removeEventListener('mousemove', this.boundOnTouchMove)
969
+
970
+ window.removeEventListener('mouseup', this.boundOnTouchUp)
971
+
972
+ window.removeEventListener('touchstart', this.boundOnTouchDown)
973
+
974
+ window.removeEventListener('touchmove', this.boundOnTouchMove)
975
+
976
+ window.removeEventListener('touchend', this.boundOnTouchUp)
977
+
978
+ if (this.renderer?.gl?.canvas.parentNode) {
979
+ this.renderer.gl.canvas.parentNode.removeChild(this.renderer.gl.canvas as HTMLCanvasElement)
980
+ }
981
+ }
982
+ }
983
+
984
+ export interface CircularGalleryProps {
985
+ items?: {
986
+ image: string
987
+ text: string
988
+ }[]
989
+ bend?: number
990
+ textColor?: string
991
+ borderRadius?: number
992
+ font?: string
993
+ fontUrl?: string
994
+ scrollSpeed?: number
995
+ scrollEase?: number
996
+ onItemClick?: (index: number) => void
997
+ showScrollIndicator?: boolean
998
+ indicatorColor?: string
999
+ indicatorCount?: number
1000
+ }
1001
+
1002
+ export default function CircularGallery({
1003
+ items,
1004
+ bend = 3,
1005
+ textColor = '#eee',
1006
+ borderRadius = 0.05,
1007
+ font = 'bold 64px Poppins',
1008
+ fontUrl,
1009
+ scrollSpeed = 2,
1010
+ scrollEase = 0.05,
1011
+ onItemClick,
1012
+ showScrollIndicator = true,
1013
+ indicatorColor = '#d6d3d150',
1014
+ indicatorCount = 14,
1015
+ }: CircularGalleryProps) {
1016
+ const containerRef = useRef<HTMLDivElement>(null)
1017
+ const indicatorRef = useRef<HTMLDivElement>(null)
1018
+ const onItemClickRef = useRef(onItemClick)
1019
+ const itemsRef = useRef(items)
1020
+
1021
+ const chevronRefs = useRef<Array<SVGSVGElement | null>>([])
1022
+
1023
+ // Parent carousel state changes must not recreate the WebGL renderer.
1024
+ onItemClickRef.current = onItemClick
1025
+ itemsRef.current = items
1026
+
1027
+ const itemsSignature = JSON.stringify(items ?? [])
1028
+
1029
+ const updateChevronIndicator = useCallback(
1030
+ ({ current }: GalleryScrollState) => {
1031
+ const indicator = indicatorRef.current
1032
+
1033
+ if (!indicator || !showScrollIndicator) return
1034
+
1035
+ const width = indicator.clientWidth
1036
+
1037
+ if (!width || indicatorCount < 1) return
1038
+
1039
+ const isMobile = width < 768
1040
+ const chevronWidth = isMobile ? 84 : 125
1041
+ const spacing = isMobile ? 108 : 158
1042
+ const curveDepth = isMobile ? 25 : 44
1043
+ const totalWidth = spacing * indicatorCount
1044
+
1045
+ /*
1046
+ * The gallery cards move left as scroll.current increases.
1047
+ * Negating current moves the right-facing chevrons to the right.
1048
+ */
1049
+ const movement = -current * (isMobile ? 34 : 48)
1050
+
1051
+ chevronRefs.current.forEach((chevron, index) => {
1052
+ if (!chevron) return
1053
+
1054
+ const initialX = index * spacing - spacing
1055
+
1056
+ const wrappedX =
1057
+ ((((initialX + movement + spacing) % totalWidth) + totalWidth) % totalWidth) - spacing
1058
+
1059
+ const centreX = wrappedX + chevronWidth / 2
1060
+
1061
+ const normalizedX = (centreX - width / 2) / (width / 2)
1062
+
1063
+ const clampedX = Math.max(-1.25, Math.min(1.25, normalizedX))
1064
+
1065
+ const y = Math.pow(Math.abs(clampedX), 1.7) * curveDepth
1066
+
1067
+ const rotation = clampedX * 5
1068
+
1069
+ const scale = 1 - Math.min(0.12, Math.abs(clampedX) * 0.08)
1070
+
1071
+ const opacity = 1 - Math.min(0.58, Math.abs(clampedX) * 0.38)
1072
+
1073
+ chevron.style.transform = [
1074
+ `translate3d(${wrappedX}px, ${y}px, 0)`,
1075
+ `rotate(${rotation}deg)`,
1076
+ `scale(${scale})`,
1077
+ ].join(' ')
1078
+
1079
+ chevron.style.opacity = String(opacity)
1080
+ })
1081
+ },
1082
+ [indicatorCount, showScrollIndicator],
1083
+ )
1084
+
1085
+ useEffect(() => {
1086
+ if (!containerRef.current) return
1087
+
1088
+ let app: App | undefined
1089
+ let isMounted = true
1090
+
1091
+ resolveFont(font, fontUrl).then((resolvedFont) => {
1092
+ if (!isMounted || !containerRef.current) {
1093
+ return
1094
+ }
1095
+
1096
+ app = new App(containerRef.current, {
1097
+ items: itemsRef.current,
1098
+ bend,
1099
+ textColor,
1100
+ borderRadius,
1101
+ font: resolvedFont,
1102
+ scrollSpeed,
1103
+ scrollEase,
1104
+ onItemClick: (index) => {
1105
+ onItemClickRef.current?.(index)
1106
+ },
1107
+ onScrollChange: updateChevronIndicator,
1108
+ })
1109
+ })
1110
+
1111
+ return () => {
1112
+ isMounted = false
1113
+
1114
+ if (app) {
1115
+ app.destroy()
1116
+ }
1117
+ }
1118
+ }, [
1119
+ itemsSignature,
1120
+ bend,
1121
+ textColor,
1122
+ borderRadius,
1123
+ font,
1124
+ fontUrl,
1125
+ scrollSpeed,
1126
+ scrollEase,
1127
+ updateChevronIndicator,
1128
+ ])
1129
+
1130
+ return (
1131
+ <div className="circular-gallery-block relative h-full w-full overflow-hidden">
1132
+ {showScrollIndicator && (
1133
+ <div
1134
+ ref={indicatorRef}
1135
+ aria-hidden="true"
1136
+ className="CircularGallery-showScrollIndicator pointer-events-none absolute inset-x-0 top-16 mb-32 z-0 h-32 overflow-hidden md:h-64"
1137
+ style={{
1138
+ maskImage:
1139
+ 'linear-gradient(90deg, transparent 0%, black 6%, black 94%, transparent 100%)',
1140
+ WebkitMaskImage:
1141
+ 'linear-gradient(90deg, transparent 0%, black 6%, black 94%, transparent 100%)',
1142
+ }}
1143
+ >
1144
+ {Array.from({
1145
+ length: indicatorCount,
1146
+ }).map((_, index) => (
1147
+ <svg
1148
+ key={index}
1149
+ ref={(element) => {
1150
+ chevronRefs.current[index] = element
1151
+ }}
1152
+ width={CHEVRON_VIEWBOX_WIDTH}
1153
+ height={CHEVRON_VIEWBOX_HEIGHT}
1154
+ viewBox={`0 0 ${CHEVRON_VIEWBOX_WIDTH} ${CHEVRON_VIEWBOX_HEIGHT}`}
1155
+ fill="none"
1156
+ xmlns="http://www.w3.org/2000/svg"
1157
+ className="absolute left-0 top-0 h-auto w-[84px] will-change-transform md:w-[125px]"
1158
+ style={{
1159
+ opacity: 0,
1160
+ filter: 'drop-shadow(0 18px 14px rgb(0 0 0 / 0.08))',
1161
+ transformOrigin: 'center',
1162
+ }}
1163
+ >
1164
+ <path
1165
+ d="M0 0H73.7638L124.139 64L73.7638 128H0L50.3753 64L0 0Z"
1166
+ fill={indicatorColor}
1167
+ />
1168
+ </svg>
1169
+ ))}
1170
+ </div>
1171
+ )}
1172
+
1173
+ <div
1174
+ ref={containerRef}
1175
+ className="circular-gallery h-full w-full scale-100 cursor-grab overflow-hidden active:cursor-grabbing"
1176
+ />
1177
+ </div>
1178
+ )
1179
+ }