@hortonstudio/main 1.2.15 → 1.2.19

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.
@@ -0,0 +1,40 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "mcp__webflow__pages_get_content",
5
+ "WebFetch(domain:compassfacilities.webflow.io)",
6
+ "mcp__webflow__collections_list",
7
+ "mcp__webflow__collections_get",
8
+ "mcp__webflow__sites_list",
9
+ "mcp__webflow__site_applied_scripts_list",
10
+ "mcp__webflow__site_registered_scripts_list",
11
+ "mcp__webflow__add_inline_site_script",
12
+ "Bash(mv:*)",
13
+ "WebFetch(domain:www.compassfacilities.com)",
14
+ "mcp__webflow__pages_list",
15
+ "Bash(npm publish:*)",
16
+ "Bash(npm version:*)",
17
+ "mcp__ide__getDiagnostics",
18
+ "Bash(git init:*)",
19
+ "Bash(git branch:*)",
20
+ "Bash(mkdir:*)",
21
+ "Bash(rm:*)",
22
+ "Bash(git add:*)",
23
+ "Bash(git commit:*)",
24
+ "Bash(git remote add:*)",
25
+ "Bash(git push:*)",
26
+ "Bash(find:*)",
27
+ "WebFetch(domain:github.com)",
28
+ "WebFetch(domain:raw.githubusercontent.com)",
29
+ "WebFetch(domain:www.jsdelivr.com)",
30
+ "WebFetch(domain:cdn.jsdelivr.net)",
31
+ "Bash(npm install)",
32
+ "Bash(npm run build:*)",
33
+ "Bash(ls:*)",
34
+ "Bash(cp:*)",
35
+ "Bash(git remote remove:*)",
36
+ "Bash(tree:*)"
37
+ ],
38
+ "deny": []
39
+ }
40
+ }
package/CLAUDE.md ADDED
@@ -0,0 +1,45 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ This is `@hortonstudio/main` - an animation and utility library for client websites, primarily designed for Webflow integration. The library uses a modular ES6 system with dynamic loading based on HTML data attributes.
8
+
9
+ ## Architecture
10
+
11
+ **Main API**: `window.hsmain` (configurable via `API_NAME` constant in index.js)
12
+
13
+ **Module Categories**:
14
+ - Animation modules: `data-hs-anim-*` attributes trigger loading
15
+ - Utility modules: `data-hs-util-*` attributes trigger loading
16
+ - Auto-init modules: Always loaded automatically
17
+
18
+ **Dependencies**: Requires GSAP with ScrollTrigger and SplitText plugins
19
+
20
+ **Integration**: Designed for Webflow with `Webflow.ready()` callback system
21
+
22
+ ## Module Loading System
23
+
24
+ Modules are loaded via script tag attributes:
25
+ ```html
26
+ <script src="index.js" data-hs-main data-hs-anim-hero data-hs-util-toc></script>
27
+ ```
28
+
29
+ Each module exports an `init()` function returning `{ result: 'module-name initialized' }`.
30
+
31
+ ## Key Animation Patterns
32
+
33
+ **Hero animations** (`hero.js`): Orchestrated timeline with navigation reveals, split text headings, and staggered element appearances. Uses data attributes like `data-hs-hero="heading"` and `data-hs-split="word|line|char"`.
34
+
35
+ **Text animations** (`text.js`): Scroll-triggered animations using CSS classes `.a-word-split`, `.a-line-split`, `.a-char-split`, `.a-appear` on parent elements, targeting first child for animation.
36
+
37
+ **Configuration**: All modules expose config objects via `window.hsmain.moduleAnimations.config` with `updateConfig()` methods for runtime modification.
38
+
39
+ ## Important Implementation Details
40
+
41
+ - All animations wait for `document.fonts.ready` before initialization
42
+ - Split text instances are automatically cleaned up after animations complete
43
+ - CSS utility classes like `.u-overflow-clip` are dynamically added/removed for animation masking
44
+ - The library handles Webflow DOM changes by calling `Webflow.ready()` after module loading
45
+ - Navigation accessibility is temporarily disabled during hero animations then restored
@@ -0,0 +1,613 @@
1
+ const API_NAME = 'hsmain';
2
+
3
+ // Check for reduced motion preference
4
+ const prefersReducedMotion = () => {
5
+ return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
6
+ };
7
+
8
+ // Animation timing (in seconds)
9
+ const timing = {
10
+ announce: 0,
11
+ nav: 0.1,
12
+ navLogo: 0.3,
13
+ navList: 0.35,
14
+ navMenu: 0.35,
15
+ navButton: 0.5,
16
+ tag: 0.1,
17
+ heading: 0.15,
18
+ subheading: 0.25,
19
+ button: 0.35,
20
+ image: 0.5,
21
+ appear: 0.6
22
+ };
23
+
24
+ // Hero Animations Module
25
+ let heroTimeline = null;
26
+ let headingSplits = [];
27
+ let subheadingSplits = [];
28
+ let heroTimeout = null; // Track the setTimeout
29
+
30
+ const config = {
31
+ global: {
32
+ animationDelay: 0.2
33
+ },
34
+ headingSplit: {
35
+ duration: 1.5,
36
+ stagger: 0.1,
37
+ yPercent: 110,
38
+ ease: "power4.out"
39
+ },
40
+ subheadingSplit: {
41
+ duration: 1.5,
42
+ stagger: 0.1,
43
+ yPercent: 110,
44
+ ease: "power4.out"
45
+ },
46
+ appear: {
47
+ y: 50,
48
+ duration: 1.5,
49
+ ease: "power3.out"
50
+ },
51
+ navStagger: {
52
+ duration: 1.5,
53
+ stagger: 0.1,
54
+ ease: "power3.out"
55
+ },
56
+ nav: {
57
+ duration: 1,
58
+ ease: "power3.out"
59
+ }
60
+ };
61
+
62
+ function updateConfig(newConfig) {
63
+ function deepMerge(target, source) {
64
+ for (const key in source) {
65
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
66
+ target[key] = target[key] || {};
67
+ deepMerge(target[key], source[key]);
68
+ } else {
69
+ target[key] = source[key];
70
+ }
71
+ }
72
+ return target;
73
+ }
74
+
75
+ deepMerge(config, newConfig);
76
+ }
77
+
78
+ function killHeroAnimations() {
79
+ if (heroTimeout) {
80
+ clearTimeout(heroTimeout);
81
+ heroTimeout = null;
82
+ }
83
+
84
+ if (heroTimeline) {
85
+ heroTimeline.kill();
86
+ heroTimeline = null;
87
+ }
88
+
89
+ headingSplits.forEach(split => {
90
+ if (split && split.revert) {
91
+ split.revert();
92
+ }
93
+ });
94
+ headingSplits = [];
95
+
96
+ subheadingSplits.forEach(split => {
97
+ if (split && split.revert) {
98
+ split.revert();
99
+ }
100
+ });
101
+ subheadingSplits = [];
102
+
103
+ // Restore page-wide tabbing if animation is killed
104
+ const allFocusableElements = document.querySelectorAll('[data-original-tabindex]');
105
+ allFocusableElements.forEach(el => {
106
+ el.style.pointerEvents = '';
107
+ const originalTabindex = el.getAttribute('data-original-tabindex');
108
+ if (originalTabindex === '0') {
109
+ el.removeAttribute('tabindex');
110
+ } else {
111
+ el.setAttribute('tabindex', originalTabindex);
112
+ }
113
+ el.removeAttribute('data-original-tabindex');
114
+ });
115
+ // Restore nav pointer events if animation is killed
116
+ const navElement = document.querySelector('[data-hs-hero="nav"]');
117
+ if (navElement) {
118
+ navElement.style.pointerEvents = '';
119
+ }
120
+ }
121
+
122
+ function startHeroAnimations() {
123
+ killHeroAnimations();
124
+ init();
125
+ }
126
+
127
+ function showHeroElementsWithoutAnimation() {
128
+ // Simply show all hero elements without any animation or split text
129
+ const allHeroElements = [
130
+ ...document.querySelectorAll('[data-hs-hero="announce"]'),
131
+ ...document.querySelectorAll('[data-hs-hero="nav"]'),
132
+ ...document.querySelectorAll('[data-hs-hero="nav-menu"]'),
133
+ ...document.querySelectorAll('[data-hs-hero="nav-logo"]'),
134
+ ...document.querySelectorAll('[data-hs-hero="nav-button"] > *:first-child'),
135
+ ...document.querySelectorAll('[data-hs-hero="nav-list"] > * > *:first-child'),
136
+ ...document.querySelectorAll('[data-hs-hero="heading"] > *:first-child'),
137
+ ...document.querySelectorAll('[data-hs-hero="subheading"] > *:first-child'),
138
+ ...document.querySelectorAll('[data-hs-hero="tag"] > *:first-child'),
139
+ ...document.querySelectorAll('[data-hs-hero="button"] > *'),
140
+ ...document.querySelectorAll('[data-hs-hero="image"]'),
141
+ ...document.querySelectorAll('[data-hs-hero="appear"]')
142
+ ];
143
+
144
+ allHeroElements.forEach(element => {
145
+ if (element) {
146
+ gsap.set(element, {
147
+ autoAlpha: 1,
148
+ opacity: 1,
149
+ y: 0,
150
+ yPercent: 0
151
+ });
152
+ // Remove any pointer-events restrictions
153
+ element.style.pointerEvents = '';
154
+ }
155
+ });
156
+
157
+ // Restore page-wide tabbing
158
+ const allFocusableElements = document.querySelectorAll('[data-original-tabindex]');
159
+ allFocusableElements.forEach(el => {
160
+ el.style.pointerEvents = '';
161
+ const originalTabindex = el.getAttribute('data-original-tabindex');
162
+ if (originalTabindex === '0') {
163
+ el.removeAttribute('tabindex');
164
+ } else {
165
+ el.setAttribute('tabindex', originalTabindex);
166
+ }
167
+ el.removeAttribute('data-original-tabindex');
168
+ });
169
+ }
170
+
171
+ export async function init() {
172
+ if (typeof window.gsap === "undefined") {
173
+ console.error('GSAP not found - hero animations disabled');
174
+ return;
175
+ }
176
+
177
+ // Check if there's a persistent config stored globally
178
+ const api = window[API_NAME] || {};
179
+ if (api.heroAnimations?.config && api.heroAnimations.config !== config) {
180
+ // Merge persistent config into current config
181
+ updateConfig(api.heroAnimations.config);
182
+ }
183
+
184
+ if (prefersReducedMotion()) {
185
+ // For reduced motion, just show elements without animation
186
+ showHeroElementsWithoutAnimation();
187
+
188
+ // Still expose the API for consistency
189
+ window[API_NAME] = window[API_NAME] || {};
190
+ window[API_NAME].heroAnimations = {
191
+ config: config,
192
+ updateConfig: (newConfig) => {
193
+ updateConfig(newConfig);
194
+ // Store config reference for persistence across restarts
195
+ api.heroAnimations.config = config;
196
+ },
197
+ start: startHeroAnimations,
198
+ kill: killHeroAnimations,
199
+ restart: () => {
200
+ killHeroAnimations();
201
+ startHeroAnimations();
202
+ }
203
+ };
204
+
205
+ return { result: 'anim-hero initialized (reduced motion)' };
206
+ }
207
+
208
+ gsap.registerPlugin(ScrollTrigger, SplitText);
209
+
210
+ // Element selection
211
+ const announceElements = document.querySelectorAll('[data-hs-hero="announce"]');
212
+ const navElement = document.querySelector('[data-hs-hero="nav"]');
213
+ const navMenuElements = document.querySelectorAll('[data-hs-hero="nav-menu"]');
214
+ const navLogoElements = document.querySelectorAll('[data-hs-hero="nav-logo"]');
215
+ const imageElements = document.querySelectorAll('[data-hs-hero="image"]');
216
+ const appearElements = document.querySelectorAll('[data-hs-hero="appear"]');
217
+
218
+ // Check if nav has advanced config
219
+ const hasAdvancedNav = navElement && navElement.hasAttribute('data-hs-heroconfig') && navElement.getAttribute('data-hs-heroconfig') === 'advanced';
220
+
221
+ // First child elements - only select if advanced nav is enabled
222
+ const navButton = [];
223
+ if (hasAdvancedNav) {
224
+ const navButtonParents = document.querySelectorAll('[data-hs-hero="nav-button"]');
225
+ navButtonParents.forEach(el => {
226
+ if (el.firstElementChild) navButton.push(el.firstElementChild);
227
+ });
228
+ }
229
+
230
+ const subheading = [];
231
+ const subheadingAppearElements = [];
232
+ const subheadingElements = document.querySelectorAll('[data-hs-hero="subheading"]');
233
+ const subheadingSplitElements = [];
234
+
235
+ subheadingElements.forEach(el => {
236
+ if (el.firstElementChild) {
237
+ // Get the heroconfig attribute to determine animation type (default to appear)
238
+ const heroConfig = el.getAttribute('data-hs-heroconfig') || 'appear';
239
+
240
+ if (heroConfig === 'appear') {
241
+ subheadingAppearElements.push(el.firstElementChild);
242
+ } else {
243
+ subheading.push(el.firstElementChild);
244
+ subheadingSplitElements.push(el);
245
+ }
246
+ }
247
+ });
248
+
249
+ const heading = [];
250
+ const headingAppearElements = [];
251
+ const headingElements = document.querySelectorAll('[data-hs-hero="heading"]');
252
+ const headingSplitElements = [];
253
+
254
+ headingElements.forEach(el => {
255
+ if (el.firstElementChild) {
256
+ // Get the heroconfig attribute to determine animation type
257
+ const heroConfig = el.getAttribute('data-hs-heroconfig') || 'word'; // default to word if not specified
258
+
259
+ if (heroConfig === 'appear') {
260
+ headingAppearElements.push(el.firstElementChild);
261
+ } else {
262
+ heading.push(el.firstElementChild);
263
+ headingSplitElements.push(el);
264
+ }
265
+ }
266
+ });
267
+
268
+ const tag = [];
269
+ const tagParents = document.querySelectorAll('[data-hs-hero="tag"]');
270
+ tagParents.forEach(el => {
271
+ if (el.firstElementChild) tag.push(el.firstElementChild);
272
+ });
273
+
274
+ // All children elements
275
+ const buttonAllChildren = [];
276
+ const buttonParents = document.querySelectorAll('[data-hs-hero="button"]');
277
+ buttonParents.forEach(el => {
278
+ const children = Array.from(el.children);
279
+ buttonAllChildren.push(...children);
280
+ });
281
+
282
+ const navListAllChildren = [];
283
+ if (hasAdvancedNav) {
284
+ const navListParents = document.querySelectorAll('[data-hs-hero="nav-list"]');
285
+ navListParents.forEach(el => {
286
+ const children = Array.from(el.children);
287
+
288
+ // Add overflow clip class to each child and collect their first child for animation
289
+ children.forEach(child => {
290
+ child.classList.add('u-overflow-clip');
291
+ if (child.firstElementChild) {
292
+ navListAllChildren.push(child.firstElementChild);
293
+ }
294
+ });
295
+ });
296
+ }
297
+
298
+ // Initial states
299
+ if (announceElements.length > 0) gsap.set(announceElements, { opacity: 0, y: -50 });
300
+ if (navElement) {
301
+ gsap.set(navElement, { opacity: 0, y: -50 });
302
+ // Disable nav pointer events until animation completes
303
+ navElement.style.pointerEvents = 'none';
304
+ }
305
+ if (hasAdvancedNav && navListAllChildren.length > 0) gsap.set(navListAllChildren, { opacity: 0, yPercent: 110 });
306
+ if (hasAdvancedNav && navMenuElements.length > 0) gsap.set(navMenuElements, { opacity: 0 });
307
+ if (hasAdvancedNav && navButton.length > 0) gsap.set(navButton, { opacity: 0 });
308
+ if (hasAdvancedNav && navLogoElements.length > 0) gsap.set(navLogoElements, { opacity: 0 });
309
+ if (subheadingAppearElements.length > 0) gsap.set(subheadingAppearElements, { y: config.appear.y, opacity: 0 });
310
+ if (tag.length > 0) gsap.set(tag, { y: config.appear.y, opacity: 0 });
311
+ if (buttonAllChildren.length > 0) gsap.set(buttonAllChildren, { y: config.appear.y, opacity: 0 });
312
+ if (imageElements.length > 0) gsap.set(imageElements, { opacity: 0 });
313
+ if (appearElements.length > 0) gsap.set(appearElements, { y: config.appear.y, opacity: 0 });
314
+ if (headingAppearElements.length > 0) gsap.set(headingAppearElements, { y: config.appear.y, opacity: 0 });
315
+
316
+ // Disable page-wide tabbing and interactions until animation completes
317
+ const allFocusableElements = document.querySelectorAll('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])');
318
+ allFocusableElements.forEach(el => {
319
+ el.style.pointerEvents = 'none';
320
+ el.setAttribute('data-original-tabindex', el.getAttribute('tabindex') || '0');
321
+ el.setAttribute('tabindex', '-1');
322
+ });
323
+
324
+ // Animation timeline
325
+ document.fonts.ready.then(() => {
326
+
327
+ // Split text setup (after fonts are loaded)
328
+ headingSplits = [];
329
+
330
+ if (heading.length > 0) {
331
+ headingSplitElements.forEach((parent, index) => {
332
+ const textElement = heading[index];
333
+ const splitType = parent.getAttribute('data-hs-heroconfig') || 'word';
334
+
335
+ let splitConfig = {};
336
+ let elementsClass = '';
337
+
338
+ if (splitType === 'char') {
339
+ splitConfig = {
340
+ type: "words,chars",
341
+ mask: "chars",
342
+ charsClass: "char"
343
+ };
344
+ elementsClass = 'chars';
345
+ } else if (splitType === 'line') {
346
+ splitConfig = {
347
+ type: "lines",
348
+ mask: "lines",
349
+ linesClass: "line"
350
+ };
351
+ elementsClass = 'lines';
352
+ } else {
353
+ splitConfig = {
354
+ type: "words",
355
+ mask: "words",
356
+ wordsClass: "word"
357
+ };
358
+ elementsClass = 'words';
359
+ }
360
+
361
+ const split = new SplitText(textElement, splitConfig);
362
+ split.elementsClass = elementsClass;
363
+ headingSplits.push(split);
364
+
365
+ gsap.set(split[elementsClass], { yPercent: config.headingSplit.yPercent });
366
+ gsap.set(textElement, { autoAlpha: 1 });
367
+ });
368
+ }
369
+
370
+ // Split text setup for subheadings
371
+ if (subheading.length > 0) {
372
+ subheadingSplitElements.forEach((parent, index) => {
373
+ const textElement = subheading[index];
374
+ const splitType = parent.getAttribute('data-hs-heroconfig') || 'word';
375
+
376
+ let splitConfig = {};
377
+ let elementsClass = '';
378
+
379
+ if (splitType === 'char') {
380
+ splitConfig = {
381
+ type: "words,chars",
382
+ mask: "chars",
383
+ charsClass: "char"
384
+ };
385
+ elementsClass = 'chars';
386
+ } else if (splitType === 'line') {
387
+ splitConfig = {
388
+ type: "lines",
389
+ mask: "lines",
390
+ linesClass: "line"
391
+ };
392
+ elementsClass = 'lines';
393
+ } else {
394
+ splitConfig = {
395
+ type: "words",
396
+ mask: "words",
397
+ wordsClass: "word"
398
+ };
399
+ elementsClass = 'words';
400
+ }
401
+
402
+ const split = new SplitText(textElement, splitConfig);
403
+ split.elementsClass = elementsClass;
404
+ subheadingSplits.push(split);
405
+
406
+ gsap.set(split[elementsClass], { yPercent: config.subheadingSplit.yPercent });
407
+ gsap.set(textElement, { autoAlpha: 1 });
408
+ });
409
+ }
410
+
411
+ heroTimeout = setTimeout(() => {
412
+ heroTimeline = gsap.timeline();
413
+
414
+ if (announceElements.length > 0) {
415
+ heroTimeline.to(announceElements,
416
+ { opacity: 1, y: 0, duration: config.nav.duration, ease: config.nav.ease },
417
+ timing.announce
418
+ );
419
+ }
420
+
421
+ if (navElement) {
422
+ heroTimeline.to(navElement,
423
+ { opacity: 1, y: 0, duration: config.nav.duration, ease: config.nav.ease },
424
+ timing.nav
425
+ );
426
+ }
427
+
428
+ if (hasAdvancedNav && navLogoElements.length > 0) {
429
+ heroTimeline.to(navLogoElements,
430
+ { opacity: 1, duration: .5, ease: config.nav.ease },
431
+ timing.navLogo
432
+ );
433
+ }
434
+
435
+ if (hasAdvancedNav && navListAllChildren.length > 0) {
436
+ heroTimeline.to(navListAllChildren,
437
+ {
438
+ opacity: 1,
439
+ yPercent: 0,
440
+ duration: config.nav.duration,
441
+ stagger: 0.05,
442
+ ease: config.nav.ease,
443
+ onComplete: () => {
444
+ // Remove u-overflow-clip class from list children
445
+ const navListParents = document.querySelectorAll('[data-hs-hero="nav-list"]');
446
+ navListParents.forEach(parent => {
447
+ const children = parent.children;
448
+ Array.from(children).forEach(child => {
449
+ child.classList.remove('u-overflow-clip');
450
+ });
451
+ });
452
+ }
453
+ },
454
+ timing.navList
455
+ );
456
+ }
457
+
458
+ if (hasAdvancedNav && navMenuElements.length > 0) {
459
+ heroTimeline.to(navMenuElements,
460
+ { opacity: 1, duration: config.nav.duration, ease: config.nav.ease },
461
+ timing.navMenu
462
+ );
463
+ }
464
+
465
+ if (hasAdvancedNav && navButton.length > 0) {
466
+ heroTimeline.to(navButton,
467
+ { opacity: 1, duration: config.nav.duration, ease: config.nav.ease },
468
+ timing.navButton
469
+ );
470
+ }
471
+
472
+ if (headingSplits.length > 0) {
473
+ headingSplits.forEach(split => {
474
+ heroTimeline.to(split[split.elementsClass],
475
+ {
476
+ yPercent: 0,
477
+ duration: config.headingSplit.duration,
478
+ stagger: config.headingSplit.stagger,
479
+ ease: config.headingSplit.ease,
480
+ onComplete: () => {
481
+ if (split && split.revert) {
482
+
483
+ }
484
+ }
485
+ },
486
+ timing.heading
487
+ );
488
+ });
489
+ }
490
+
491
+ if (subheadingSplits.length > 0) {
492
+ subheadingSplits.forEach(split => {
493
+ heroTimeline.to(split[split.elementsClass],
494
+ {
495
+ yPercent: 0,
496
+ duration: config.subheadingSplit.duration,
497
+ stagger: config.subheadingSplit.stagger,
498
+ ease: config.subheadingSplit.ease,
499
+ onComplete: () => {
500
+ if (split && split.revert) {
501
+
502
+ }
503
+ }
504
+ },
505
+ timing.subheading
506
+ );
507
+ });
508
+ }
509
+
510
+ if (subheadingAppearElements.length > 0) {
511
+ heroTimeline.to(subheadingAppearElements,
512
+ { y: 0, opacity: 1, duration: config.appear.duration, ease: config.appear.ease },
513
+ timing.subheading
514
+ );
515
+ }
516
+
517
+ if (tag.length > 0) {
518
+ heroTimeline.to(tag,
519
+ { y: 0, opacity: 1, duration: config.appear.duration, ease: config.appear.ease },
520
+ timing.tag
521
+ );
522
+ }
523
+
524
+ if (buttonAllChildren.length > 0) {
525
+ heroTimeline.to(buttonAllChildren,
526
+ { y: 0, opacity: 1, duration: config.navStagger.duration, stagger: config.navStagger.stagger, ease: config.navStagger.ease },
527
+ timing.button
528
+ );
529
+ }
530
+
531
+ if (imageElements.length > 0) {
532
+ heroTimeline.to(imageElements,
533
+ { opacity: 1, duration: config.appear.duration, ease: config.appear.ease },
534
+ timing.image
535
+ );
536
+ }
537
+
538
+ // Combine appear elements and heading appear elements
539
+ const allAppearElements = [...appearElements, ...headingAppearElements];
540
+
541
+ if (allAppearElements.length > 0) {
542
+ heroTimeline.to(allAppearElements,
543
+ {
544
+ y: 0,
545
+ opacity: 1,
546
+ duration: config.appear.duration,
547
+ ease: config.appear.ease,
548
+ onComplete: () => {
549
+ // Restore page-wide tabbing and interactions after hero animation completes
550
+ const allFocusableElements = document.querySelectorAll('[data-original-tabindex]');
551
+ allFocusableElements.forEach(el => {
552
+ el.style.pointerEvents = '';
553
+ const originalTabindex = el.getAttribute('data-original-tabindex');
554
+ if (originalTabindex === '0') {
555
+ el.removeAttribute('tabindex');
556
+ } else {
557
+ el.setAttribute('tabindex', originalTabindex);
558
+ }
559
+ el.removeAttribute('data-original-tabindex');
560
+ });
561
+ // Restore nav pointer events
562
+ if (navElement) {
563
+ navElement.style.pointerEvents = '';
564
+ }
565
+ }
566
+ },
567
+ timing.appear
568
+ );
569
+ } else {
570
+ // If no appear elements, restore tabbing when timeline completes
571
+ heroTimeline.call(() => {
572
+ const allFocusableElements = document.querySelectorAll('[data-original-tabindex]');
573
+ allFocusableElements.forEach(el => {
574
+ el.style.pointerEvents = '';
575
+ const originalTabindex = el.getAttribute('data-original-tabindex');
576
+ if (originalTabindex === '0') {
577
+ el.removeAttribute('tabindex');
578
+ } else {
579
+ el.setAttribute('tabindex', originalTabindex);
580
+ }
581
+ el.removeAttribute('data-original-tabindex');
582
+ });
583
+ // Restore nav pointer events
584
+ if (navElement) {
585
+ navElement.style.pointerEvents = '';
586
+ }
587
+ });
588
+ }
589
+
590
+ heroTimeout = null;
591
+
592
+ }, config.global.animationDelay * 1000);
593
+
594
+ });
595
+
596
+ // API exposure
597
+ api.heroAnimations = {
598
+ config: config,
599
+ updateConfig: (newConfig) => {
600
+ updateConfig(newConfig);
601
+ // Store config reference for persistence across restarts
602
+ api.heroAnimations.config = config;
603
+ },
604
+ start: startHeroAnimations,
605
+ kill: killHeroAnimations,
606
+ restart: () => {
607
+ killHeroAnimations();
608
+ startHeroAnimations();
609
+ }
610
+ };
611
+
612
+ return { result: 'anim-hero initialized' };
613
+ }