@brandocms/jupiter 3.55.0 → 4.0.0-beta.2

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 (76) hide show
  1. package/README.md +509 -54
  2. package/package.json +30 -18
  3. package/src/index.js +15 -10
  4. package/src/modules/Application/index.js +236 -158
  5. package/src/modules/Breakpoints/index.js +116 -36
  6. package/src/modules/Cookies/index.js +95 -64
  7. package/src/modules/CoverOverlay/index.js +21 -14
  8. package/src/modules/Dataloader/index.js +71 -24
  9. package/src/modules/Dataloader/url-sync.js +238 -0
  10. package/src/modules/Dom/index.js +24 -0
  11. package/src/modules/DoubleHeader/index.js +571 -0
  12. package/src/modules/Dropdown/index.js +108 -73
  13. package/src/modules/EqualHeightElements/index.js +8 -8
  14. package/src/modules/EqualHeightImages/index.js +15 -7
  15. package/src/modules/FixedHeader/index.js +116 -30
  16. package/src/modules/FooterReveal/index.js +5 -5
  17. package/src/modules/HeroSlider/index.js +231 -106
  18. package/src/modules/HeroVideo/index.js +72 -44
  19. package/src/modules/Lazyload/index.js +128 -80
  20. package/src/modules/Lightbox/index.js +101 -80
  21. package/src/modules/Links/index.js +77 -51
  22. package/src/modules/Looper/index.js +1737 -0
  23. package/src/modules/Marquee/index.js +106 -37
  24. package/src/modules/MobileMenu/index.js +105 -130
  25. package/src/modules/Moonwalk/index.js +479 -153
  26. package/src/modules/Parallax/index.js +280 -57
  27. package/src/modules/Popover/index.js +187 -17
  28. package/src/modules/Popup/index.js +172 -53
  29. package/src/modules/ScrollSpy/index.js +21 -0
  30. package/src/modules/StackedBoxes/index.js +8 -6
  31. package/src/modules/StickyHeader/index.js +394 -164
  32. package/src/modules/Toggler/index.js +207 -11
  33. package/src/modules/Typography/index.js +33 -20
  34. package/src/utils/motion-helpers.js +330 -0
  35. package/types/README.md +159 -0
  36. package/types/events/index.d.ts +20 -0
  37. package/types/index.d.ts +6 -0
  38. package/types/modules/Application/index.d.ts +168 -0
  39. package/types/modules/Breakpoints/index.d.ts +40 -0
  40. package/types/modules/Cookies/index.d.ts +81 -0
  41. package/types/modules/CoverOverlay/index.d.ts +6 -0
  42. package/types/modules/Dataloader/index.d.ts +38 -0
  43. package/types/modules/Dataloader/url-sync.d.ts +36 -0
  44. package/types/modules/Dom/index.d.ts +47 -0
  45. package/types/modules/DoubleHeader/index.d.ts +63 -0
  46. package/types/modules/Dropdown/index.d.ts +15 -0
  47. package/types/modules/EqualHeightElements/index.d.ts +8 -0
  48. package/types/modules/EqualHeightImages/index.d.ts +11 -0
  49. package/types/modules/FeatureTests/index.d.ts +27 -0
  50. package/types/modules/FixedHeader/index.d.ts +219 -0
  51. package/types/modules/Fontloader/index.d.ts +5 -0
  52. package/types/modules/FooterReveal/index.d.ts +5 -0
  53. package/types/modules/HeroSlider/index.d.ts +28 -0
  54. package/types/modules/HeroVideo/index.d.ts +83 -0
  55. package/types/modules/Lazyload/index.d.ts +80 -0
  56. package/types/modules/Lightbox/index.d.ts +123 -0
  57. package/types/modules/Links/index.d.ts +55 -0
  58. package/types/modules/Looper/index.d.ts +127 -0
  59. package/types/modules/Marquee/index.d.ts +23 -0
  60. package/types/modules/MobileMenu/index.d.ts +63 -0
  61. package/types/modules/Moonwalk/index.d.ts +322 -0
  62. package/types/modules/Parallax/index.d.ts +71 -0
  63. package/types/modules/Popover/index.d.ts +29 -0
  64. package/types/modules/Popup/index.d.ts +76 -0
  65. package/types/modules/ScrollSpy/index.d.ts +29 -0
  66. package/types/modules/StackedBoxes/index.d.ts +9 -0
  67. package/types/modules/StickyHeader/index.d.ts +220 -0
  68. package/types/modules/Toggler/index.d.ts +48 -0
  69. package/types/modules/Typography/index.d.ts +77 -0
  70. package/types/utils/dispatchElementEvent.d.ts +1 -0
  71. package/types/utils/imageIsLoaded.d.ts +1 -0
  72. package/types/utils/imagesAreLoaded.d.ts +1 -0
  73. package/types/utils/loadScript.d.ts +2 -0
  74. package/types/utils/prefersReducedMotion.d.ts +4 -0
  75. package/types/utils/rafCallback.d.ts +2 -0
  76. package/types/utils/zoom.d.ts +4 -0
@@ -1,42 +1,238 @@
1
- import { gsap } from 'gsap'
1
+ import { animate } from 'motion'
2
2
  import Dom from '../Dom'
3
+ import { set } from '../../utils/motion-helpers'
3
4
 
5
+ /**
6
+ * Toggler component for show/hide functionality
7
+ * Uses [data-toggle-trigger] for the toggle button and [data-toggle-content] for toggleable content
8
+ * Can be grouped using [data-toggle-group] to create accordion-like behavior
9
+ *
10
+ * IMPORTANT: For smooth animations, avoid padding/margins on [data-toggle-content].
11
+ * Instead, wrap content in a child element with padding/margins:
12
+ *
13
+ * @example
14
+ * // ❌ DON'T: Padding/margins directly on toggle content
15
+ * <div data-toggle-content style="padding: 20px; margin-top: 10px">
16
+ * Content here
17
+ * </div>
18
+ *
19
+ * // ✅ DO: Wrap content in child element
20
+ * <div data-toggle-content>
21
+ * <div style="padding: 20px; margin-top: 10px">
22
+ * Content here
23
+ * </div>
24
+ * </div>
25
+ */
4
26
  export default class Toggler {
5
- constructor(app, el) {
27
+ /**
28
+ * Create a new Toggler instance
29
+ * @param {Object} app - Application instance
30
+ * @param {HTMLElement} el - Container element with [data-toggle] attribute
31
+ * @param {Object} options - Configuration options
32
+ * @param {Function} options.onOpen - Callback when toggle opens
33
+ * @param {Function} options.onClose - Callback when toggle closes
34
+ */
35
+ constructor(app, el, options = {}) {
6
36
  this.open = false
7
37
  this.app = app
8
38
  this.el = el
39
+ this.onOpen = options.onOpen
40
+ this.onClose = options.onClose
41
+ this.onBeforeOpen = options.onBeforeOpen
42
+ this.onBeforeClose = options.onBeforeClose
9
43
  this.trigger = Dom.find(this.el, '[data-toggle-trigger]')
44
+ this.triggerTarget = this.trigger.dataset.toggleTrigger
45
+ this.group = this.el.dataset.toggleGroup
46
+ if (this.triggerTarget) {
47
+ this.content = Dom.all(this.el, `[data-toggle-content="${this.triggerTarget}"]`)
48
+ } else {
49
+ this.content = Dom.all(this.el, '[data-toggle-content]')
50
+ }
10
51
  this.triggerIcon = Dom.find(this.trigger, 'span.icon')
11
- this.content = Dom.find(this.el, '[data-toggle-content]')
12
52
  this.trigger.addEventListener('click', this.onClick.bind(this))
13
53
  }
14
54
 
55
+ /**
56
+ * Get the index of this toggle within its group (1-based)
57
+ * @returns {number} The 1-based index of this toggle in its group
58
+ */
59
+ getGroupIndex() {
60
+ if (!this.group || !this.app.togglers) return 1
61
+
62
+ const groupTogglers = this.app.togglers.filter(t => t.group === this.group)
63
+ return groupTogglers.indexOf(this) + 1
64
+ }
65
+
66
+ /**
67
+ * Handle click on trigger element
68
+ */
15
69
  onClick() {
70
+ // If this toggler belongs to a group and is currently closed,
71
+ // close all others in the same group
72
+ if (this.group && !this.open) {
73
+ this.closeOthersInGroup()
74
+ }
75
+
16
76
  this.toggleState()
17
77
 
18
78
  if (this.open) {
19
79
  if (this.triggerIcon) {
20
80
  this.triggerIcon.classList.toggle('active')
21
81
  }
22
- gsap.set(this.content, { height: 'auto', display: 'block' })
82
+ this.trigger.setAttribute('data-toggle-trigger-active', '')
83
+
84
+ // Set initial state before making visible
85
+ this.content.forEach(el => {
86
+ el.style.height = '0'
87
+ el.style.display = 'block'
88
+ // Force layout reflow to ensure 0 height is applied
89
+ el.offsetHeight
90
+ })
91
+
23
92
  this.el.classList.toggle('open')
24
- gsap.from(this.content, { height: 0, ease: 'power1.inOut' })
93
+ if (this.onBeforeOpen) {
94
+ this.onBeforeOpen(this, this.getGroupIndex())
95
+ }
96
+
97
+ // Animate each content element with stagger
98
+ const animations = []
99
+ this.content.forEach((el, index) => {
100
+ // Measure the actual height now
101
+ const targetHeight = el.scrollHeight
102
+
103
+ animations.push(
104
+ animate(el, { height: [0, targetHeight + 'px'] }, {
105
+ ease: 'easeInOut',
106
+ delay: index * 0.1
107
+ })
108
+ )
109
+ })
110
+
111
+ // Wait for the last animation to complete
112
+ const lastAnimation = animations[animations.length - 1]
113
+ if (lastAnimation) {
114
+ lastAnimation.finished.then(() => {
115
+ // Set height to auto for responsiveness
116
+ this.content.forEach(el => {
117
+ el.style.height = 'auto'
118
+ el.removeAttribute('data-toggle-hidden')
119
+ el.setAttribute('data-toggle-visible', '')
120
+ })
121
+ if (this.onOpen) {
122
+ this.onOpen(this, this.getGroupIndex())
123
+ }
124
+ })
125
+ }
25
126
  } else {
26
127
  if (this.triggerIcon) {
27
128
  this.triggerIcon.classList.toggle('active')
28
129
  }
29
- gsap.to(this.content, {
30
- duration: 0.25,
31
- onComplete: () => {
32
- this.el.classList.toggle('open')
33
- }
130
+ this.trigger.removeAttribute('data-toggle-trigger-active')
131
+ if (this.onBeforeClose) {
132
+ this.onBeforeClose(this, this.getGroupIndex())
133
+ }
134
+
135
+ // Animate each content element with stagger
136
+ const animations = []
137
+ this.content.forEach((el, index) => {
138
+ // Get current height and set explicitly before animating
139
+ const currentHeight = el.scrollHeight
140
+ el.style.height = currentHeight + 'px'
141
+ // Force layout reflow to ensure pixel height is applied
142
+ el.offsetHeight
143
+
144
+ animations.push(
145
+ animate(el, { height: 0 }, {
146
+ duration: 0.25,
147
+ ease: 'easeOut',
148
+ delay: index * 0.1
149
+ })
150
+ )
34
151
  })
35
152
 
36
- gsap.to(this.content, { height: 0, ease: 'power3.out' })
153
+ // Wait for the last animation to complete
154
+ const lastAnimation = animations[animations.length - 1]
155
+ if (lastAnimation) {
156
+ lastAnimation.finished.then(() => {
157
+ this.el.classList.toggle('open')
158
+ this.content.forEach(el => {
159
+ el.style.display = 'none'
160
+ el.style.removeProperty('height')
161
+ el.removeAttribute('data-toggle-visible')
162
+ el.setAttribute('data-toggle-hidden', '')
163
+ })
164
+ if (this.onClose) {
165
+ this.onClose(this, this.getGroupIndex())
166
+ }
167
+ })
168
+ }
37
169
  }
38
170
  }
39
171
 
172
+ /**
173
+ * Close all other togglers in the same group
174
+ */
175
+ closeOthersInGroup() {
176
+ if (!this.group || !this.app.togglers) return
177
+
178
+ this.app.togglers.forEach(toggler => {
179
+ // Skip if it's this toggler or not in the same group
180
+ if (toggler === this || toggler.group !== this.group) return
181
+
182
+ // Only close if it's open
183
+ if (toggler.open) {
184
+ // Close the toggler
185
+ toggler.open = false
186
+
187
+ // Update UI
188
+ if (toggler.triggerIcon) {
189
+ toggler.triggerIcon.classList.remove('active')
190
+ }
191
+
192
+ toggler.trigger.removeAttribute('data-toggle-trigger-active')
193
+ toggler.el.classList.remove('open')
194
+
195
+ // Animate content closing with stagger
196
+ const animations = []
197
+ toggler.content.forEach((el, index) => {
198
+ // Get current height and set explicitly before animating
199
+ const currentHeight = el.scrollHeight
200
+ el.style.height = currentHeight + 'px'
201
+ // Force layout reflow to ensure pixel height is applied
202
+ el.offsetHeight
203
+
204
+ animations.push(
205
+ animate(el, { height: 0 }, {
206
+ duration: 0.25,
207
+ ease: 'easeOut',
208
+ delay: index * 0.1
209
+ })
210
+ )
211
+ })
212
+
213
+ // Wait for the last animation to complete
214
+ const lastAnimation = animations[animations.length - 1]
215
+ if (lastAnimation) {
216
+ lastAnimation.finished.then(() => {
217
+ toggler.content.forEach(el => {
218
+ el.style.display = 'none'
219
+ el.style.removeProperty('height')
220
+ el.removeAttribute('data-toggle-visible')
221
+ el.setAttribute('data-toggle-hidden', '')
222
+ })
223
+
224
+ if (toggler.onClose) {
225
+ toggler.onClose(toggler, toggler.getGroupIndex())
226
+ }
227
+ })
228
+ }
229
+ }
230
+ })
231
+ }
232
+
233
+ /**
234
+ * Toggle open/closed state
235
+ */
40
236
  toggleState() {
41
237
  if (this.open) {
42
238
  this.open = false
@@ -1,9 +1,23 @@
1
+ /**
2
+ * @typedef {Object} TypographySettings
3
+ * @property {number} [minWords=4] - Minimum number of words required to apply fixes
4
+ * @property {string} [selector='[data-typo]'] - Selector for elements to process
5
+ * @property {string} [ignoreClass='no-typo-fix'] - Class to exclude elements from processing
6
+ * @property {boolean} [ignoreExistingSpaceChars=false] - Whether to ignore elements with existing non-breaking spaces
7
+ */
8
+
9
+ /**
10
+ * Typography class for enhancing text presentation, including orphan prevention
11
+ */
1
12
  export default class Typography {
13
+ /**
14
+ * Create a new Typography instance
15
+ * @param {HTMLElement|undefined} parent - Parent element to search for typography elements, or undefined for document
16
+ * @param {TypographySettings} settings - Typography settings
17
+ */
2
18
  constructor(parent, settings = {}) {
3
- const self = this
4
-
5
19
  // Set some settings, by merging defaults and passed settings
6
- self.settings = {
20
+ this.settings = {
7
21
  minWords: 4,
8
22
  selector: '[data-typo]',
9
23
  ignoreClass: 'no-typo-fix',
@@ -11,19 +25,19 @@ export default class Typography {
11
25
  ...settings
12
26
  }
13
27
 
14
- self.elems = []
28
+ this.elems = []
15
29
 
16
30
  // Either load from root or the passed parent element
17
31
  if (typeof parent === 'undefined') {
18
- self.elems = [...self.elems, ...document.querySelectorAll(self.settings.selector)]
32
+ this.elems = [...this.elems, ...document.querySelectorAll(this.settings.selector)]
19
33
  } else {
20
- self.elems = [...self.elems, ...parent.querySelectorAll(self.settings.selector)]
34
+ this.elems = [...this.elems, ...parent.querySelectorAll(this.settings.selector)]
21
35
  }
22
36
 
23
37
  // load children
24
38
  const typoParents = document.querySelectorAll('[data-typo-children]')
25
39
  typoParents.forEach(typoParent => {
26
- self.elems = [...self.elems, ...typoParent.children]
40
+ this.elems = [...this.elems, ...typoParent.children]
27
41
  })
28
42
 
29
43
  this.apply()
@@ -34,11 +48,9 @@ export default class Typography {
34
48
  * @return void
35
49
  */
36
50
  apply() {
37
- const self = this
38
-
39
- self.elems.map(elem => {
51
+ this.elems.map(elem => {
40
52
  // Run the ignore checker nd bail if required
41
- if (self.shouldElementBeIgnored(elem)) {
53
+ if (this.shouldElementBeIgnored(elem)) {
42
54
  return false
43
55
  }
44
56
 
@@ -52,12 +64,12 @@ export default class Typography {
52
64
  .split(/ (?=[^>]*(?:<|$))/)
53
65
 
54
66
  // Check if the text warrants this module
55
- if (textItems.length < self.settings.minWords) {
67
+ if (textItems.length < this.settings.minWords) {
56
68
  return false
57
69
  }
58
70
 
59
71
  // Run orphans filter
60
- textItems = self.preventOrphans(textItems)
72
+ textItems = this.preventOrphans(textItems)
61
73
 
62
74
  // Join the words back together
63
75
  result = textItems.join(' ')
@@ -91,11 +103,9 @@ export default class Typography {
91
103
  * @return void
92
104
  */
93
105
  reset() {
94
- const self = this
95
-
96
- self.elems.map(elem => {
106
+ this.elems.map(elem => {
97
107
  // Run the ignore checker nd bail if required
98
- if (self.shouldElementBeIgnored(elem)) {
108
+ if (this.shouldElementBeIgnored(elem)) {
99
109
  return false
100
110
  }
101
111
 
@@ -111,11 +121,14 @@ export default class Typography {
111
121
  * @returns boolean
112
122
  */
113
123
  shouldElementBeIgnored(elem) {
114
- const self = this
115
-
116
124
  // Check if the element already contains 1 or more &nbsp; characters and the
117
125
  // ignore setting is true. If so: bail.
118
- if (elem.innerHTML.indexOf('&nbsp;') > -1 && self.settings.ignoreExistingSpaceChars) {
126
+ if (elem.innerHTML.indexOf('&nbsp;') > -1 && this.settings.ignoreExistingSpaceChars) {
127
+ return true
128
+ }
129
+
130
+ // Check if the element has the ignore class
131
+ if (elem.classList.contains(this.settings.ignoreClass)) {
119
132
  return true
120
133
  }
121
134
 
@@ -0,0 +1,330 @@
1
+ import { animate, delay } from 'motion'
2
+
3
+ /**
4
+ * Set properties immediately (like gsap.set)
5
+ * Uses direct DOM manipulation for synchronous style application
6
+ *
7
+ * @param {Element|string|NodeList|Array} target - Element(s) or selector
8
+ * @param {Object} values - Properties to set
9
+ */
10
+ export function set(target, values) {
11
+ // Get elements as array
12
+ let elements
13
+ if (typeof target === 'string') {
14
+ elements = Array.from(document.querySelectorAll(target))
15
+ } else if (target instanceof NodeList) {
16
+ elements = Array.from(target)
17
+ } else if (Array.isArray(target)) {
18
+ elements = target
19
+ } else {
20
+ elements = [target]
21
+ }
22
+
23
+ elements.forEach((element) => {
24
+ if (!element) return
25
+
26
+ // Build transform string from transform properties
27
+ const transformProps = []
28
+ const styleProps = {}
29
+
30
+ Object.entries(values).forEach(([key, value]) => {
31
+ // Handle transform properties
32
+ if (key === 'x') {
33
+ transformProps.push(`translateX(${typeof value === 'number' ? value + 'px' : value})`)
34
+ } else if (key === 'y') {
35
+ transformProps.push(`translateY(${typeof value === 'number' ? value + 'px' : value})`)
36
+ } else if (key === 'scale') {
37
+ transformProps.push(`scale(${value})`)
38
+ } else if (key === 'scaleX') {
39
+ transformProps.push(`scaleX(${value})`)
40
+ } else if (key === 'scaleY') {
41
+ transformProps.push(`scaleY(${value})`)
42
+ } else if (key === 'rotate') {
43
+ transformProps.push(`rotate(${typeof value === 'number' ? value + 'deg' : value})`)
44
+ } else if (key === 'rotateX') {
45
+ transformProps.push(`rotateX(${typeof value === 'number' ? value + 'deg' : value})`)
46
+ } else if (key === 'rotateY') {
47
+ transformProps.push(`rotateY(${typeof value === 'number' ? value + 'deg' : value})`)
48
+ } else if (key === 'rotateZ') {
49
+ transformProps.push(`rotateZ(${typeof value === 'number' ? value + 'deg' : value})`)
50
+ } else {
51
+ // Regular CSS property
52
+ styleProps[key] = value
53
+ }
54
+ })
55
+
56
+ // Apply transform
57
+ if (transformProps.length > 0) {
58
+ element.style.transform = transformProps.join(' ')
59
+ }
60
+
61
+ // Apply other styles
62
+ Object.entries(styleProps).forEach(([key, value]) => {
63
+ element.style[key] = value
64
+ })
65
+ })
66
+ }
67
+
68
+ /**
69
+ * Animate autoAlpha (opacity + visibility)
70
+ * Mimics GSAP's autoAlpha property
71
+ *
72
+ * @param {Element|string} target - Element or selector
73
+ * @param {number} value - Target alpha value (0 or 1)
74
+ * @param {Object} options - Animation options
75
+ * @returns {Object} Animation object
76
+ */
77
+ export function animateAutoAlpha(target, value, options = {}) {
78
+ const element = typeof target === 'string'
79
+ ? document.querySelector(target)
80
+ : target
81
+
82
+ if (value === 0) {
83
+ // Fade out, then hide
84
+ return animate(element, { opacity: 0 }, {
85
+ ...options,
86
+ onComplete: () => {
87
+ element.style.visibility = 'hidden'
88
+ options.onComplete?.()
89
+ }
90
+ })
91
+ } else {
92
+ // Show, then fade in
93
+ element.style.visibility = 'visible'
94
+ return animate(element, { opacity: value }, options)
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Clear inline styles
100
+ * Mimics GSAP's clearProps
101
+ *
102
+ * @param {Element|string|NodeList|Array} target - Element(s) or selector
103
+ * @param {string|Array} props - Properties to clear or 'all'
104
+ */
105
+ export function clearProps(target, props = 'all') {
106
+ // Get elements as array
107
+ let elements
108
+ if (typeof target === 'string') {
109
+ elements = Array.from(document.querySelectorAll(target))
110
+ } else if (target instanceof NodeList) {
111
+ elements = Array.from(target)
112
+ } else if (Array.isArray(target)) {
113
+ elements = target
114
+ } else {
115
+ elements = [target]
116
+ }
117
+
118
+ elements.forEach(element => {
119
+ if (!element) return
120
+
121
+ if (props === 'all') {
122
+ element.removeAttribute('style')
123
+ } else {
124
+ const properties = Array.isArray(props) ? props : [props]
125
+ properties.forEach(prop => {
126
+ element.style.removeProperty(prop)
127
+ })
128
+ }
129
+ })
130
+ }
131
+
132
+ /**
133
+ * Delayed call helper
134
+ * Mimics gsap.delayedCall
135
+ * Uses Motion's delay function (locked to animation frame loop for better sync)
136
+ *
137
+ * @param {number} duration - Delay in seconds
138
+ * @param {Function} callback - Callback function
139
+ * @returns {Promise} Promise that resolves after delay
140
+ */
141
+ export function delayedCall(duration, callback) {
142
+ return new Promise(resolve => {
143
+ delay(() => {
144
+ callback()
145
+ resolve()
146
+ }, duration)
147
+ })
148
+ }
149
+
150
+ /**
151
+ * Paused Timeline helper
152
+ * Mimics GSAP's paused timeline pattern for building sequences
153
+ * Used by modules like Lightbox that build animations before playing them
154
+ */
155
+ export class PausedTimeline {
156
+ constructor() {
157
+ this.sequence = []
158
+ }
159
+
160
+ /**
161
+ * Add animation to timeline
162
+ * @param {Element|string} target - Element or selector
163
+ * @param {Object} values - Properties to animate
164
+ * @param {Object} options - Animation options
165
+ * @returns {PausedTimeline} this for chaining
166
+ */
167
+ to(target, values, options = {}) {
168
+ this.sequence.push(['animate', target, values, options])
169
+ return this
170
+ }
171
+
172
+ /**
173
+ * Add callback to timeline
174
+ * @param {Function} callback - Function to call
175
+ * @returns {PausedTimeline} this for chaining
176
+ */
177
+ call(callback) {
178
+ this.sequence.push(['call', callback])
179
+ return this
180
+ }
181
+
182
+ /**
183
+ * Clear timeline sequence
184
+ * @returns {PausedTimeline} this for chaining
185
+ */
186
+ clear() {
187
+ this.sequence = []
188
+ return this
189
+ }
190
+
191
+ /**
192
+ * Play timeline sequence
193
+ * Executes all animations and callbacks in order
194
+ * @returns {Promise} Promise that resolves when sequence completes
195
+ */
196
+ async play() {
197
+ const sequence = [...this.sequence] // Copy so we can clear while playing
198
+ this.sequence = [] // Clear for next time
199
+
200
+ for (const item of sequence) {
201
+ if (item[0] === 'animate') {
202
+ const [, target, values, options] = item
203
+ // Handle autoAlpha
204
+ if (values.autoAlpha !== undefined) {
205
+ const autoAlphaOptions = { ...options }
206
+ delete autoAlphaOptions.autoAlpha
207
+ await animateAutoAlpha(target, values.autoAlpha, autoAlphaOptions).finished
208
+ } else {
209
+ await animate(target, values, options).finished
210
+ }
211
+ } else if (item[0] === 'call') {
212
+ item[1]() // Execute callback
213
+ }
214
+ }
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Convert GSAP easing strings to Motion.js compatible easings
220
+ * Handles common GSAP easing types and returns valid Motion.js easing
221
+ *
222
+ * @param {string|Array} easing - GSAP easing string or bezier array
223
+ * @returns {string|Array} Motion.js compatible easing
224
+ *
225
+ * Valid Motion.js easings:
226
+ * - "linear"
227
+ * - "easeIn"
228
+ * - "easeInOut"
229
+ * - "easeOut"
230
+ * - "circIn"
231
+ * - "circInOut"
232
+ * - "circOut"
233
+ * - "backIn"
234
+ * - "backInOut"
235
+ * - "backOut"
236
+ * - "anticipate"
237
+ * - Bezier arrays: [x1, y1, x2, y2]
238
+ */
239
+ export function convertEasing(easing) {
240
+ // If already an array (bezier), return as-is
241
+ if (Array.isArray(easing)) {
242
+ return easing
243
+ }
244
+
245
+ // If not a string, return default
246
+ if (typeof easing !== 'string') {
247
+ return 'easeOut'
248
+ }
249
+
250
+ // Already a valid Motion.js easing
251
+ const validMotionEasings = [
252
+ 'linear',
253
+ 'easeIn',
254
+ 'easeInOut',
255
+ 'easeOut',
256
+ 'circIn',
257
+ 'circInOut',
258
+ 'circOut',
259
+ 'backIn',
260
+ 'backInOut',
261
+ 'backOut',
262
+ 'anticipate',
263
+ ]
264
+
265
+ if (validMotionEasings.includes(easing)) {
266
+ return easing
267
+ }
268
+
269
+ // Convert GSAP easings to Motion.js equivalents
270
+ const easingMap = {
271
+ // Power easings (most common)
272
+ 'power1.in': 'easeIn',
273
+ 'power1.out': 'easeOut',
274
+ 'power1.inOut': 'easeInOut',
275
+ 'power2.in': 'easeIn',
276
+ 'power2.out': 'easeOut',
277
+ 'power2.inOut': 'easeInOut',
278
+ 'power3.in': 'easeIn',
279
+ 'power3.out': 'easeOut',
280
+ 'power3.inOut': 'easeInOut',
281
+ 'power4.in': 'easeIn',
282
+ 'power4.out': 'easeOut',
283
+ 'power4.inOut': 'easeInOut',
284
+
285
+ // Sine easings
286
+ 'sine.in': 'easeIn',
287
+ 'sine.out': 'easeOut',
288
+ 'sine.inOut': 'easeInOut',
289
+
290
+ // Expo easings
291
+ 'expo.in': 'easeIn',
292
+ 'expo.out': 'easeOut',
293
+ 'expo.inOut': 'easeInOut',
294
+
295
+ // Circ easings (Motion.js has these!)
296
+ 'circ.in': 'circIn',
297
+ 'circ.out': 'circOut',
298
+ 'circ.inOut': 'circInOut',
299
+
300
+ // Back easings (Motion.js has these!)
301
+ 'back.in': 'backIn',
302
+ 'back.out': 'backOut',
303
+ 'back.inOut': 'backInOut',
304
+
305
+ // Elastic and bounce - no direct equivalent, use anticipate or backOut
306
+ 'elastic.in': 'backIn',
307
+ 'elastic.out': 'backOut',
308
+ 'elastic.inOut': 'backInOut',
309
+ 'bounce.in': 'backIn',
310
+ 'bounce.out': 'anticipate',
311
+ 'bounce.inOut': 'backInOut',
312
+
313
+ // Common aliases
314
+ none: 'linear',
315
+ 'linear': 'linear',
316
+ }
317
+
318
+ // Try to find a mapping
319
+ const converted = easingMap[easing.toLowerCase()]
320
+
321
+ if (converted) {
322
+ return converted
323
+ }
324
+
325
+ // If no mapping found, log warning and return default
326
+ console.warn(
327
+ `[Motion Helpers] Unknown easing type "${easing}", using "easeOut" as fallback`
328
+ )
329
+ return 'easeOut'
330
+ }