@lovince/motion 0.0.0-stage → 0.1.0

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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +254 -2
  3. package/dist/accessibility/focus-management.d.ts +9 -0
  4. package/dist/accessibility/index.d.ts +2 -0
  5. package/dist/accessibility/reduced-motion.d.ts +7 -0
  6. package/dist/core/cleanup.d.ts +10 -0
  7. package/dist/core/config.d.ts +7 -0
  8. package/dist/core/context.d.ts +40 -0
  9. package/dist/core/entrance.d.ts +22 -0
  10. package/dist/core/index.d.ts +7 -0
  11. package/dist/core/preferences.d.ts +6 -0
  12. package/dist/core/targets.d.ts +4 -0
  13. package/dist/core/types.d.ts +20 -0
  14. package/dist/css/dom.d.ts +9 -0
  15. package/dist/css/fade-in.d.ts +25 -0
  16. package/dist/css/index.d.ts +3 -0
  17. package/dist/css/reveal.d.ts +22 -0
  18. package/dist/dom-CK6FnKcV.js +74 -0
  19. package/dist/focus-management-DTJ2C7Pn.js +8 -0
  20. package/dist/gsap/engine.d.ts +17 -0
  21. package/dist/gsap/horizontal-scroll.d.ts +14 -0
  22. package/dist/gsap/index.d.ts +8 -0
  23. package/dist/gsap/parallax.d.ts +14 -0
  24. package/dist/gsap/scroll-reveal.d.ts +18 -0
  25. package/dist/gsap/split-text.d.ts +27 -0
  26. package/dist/gsap/stagger.d.ts +15 -0
  27. package/dist/gsap/timeline.d.ts +23 -0
  28. package/dist/gsap.js +96 -0
  29. package/dist/index.d.ts +4 -0
  30. package/dist/index.js +75 -0
  31. package/dist/presets/entrance.d.ts +20 -0
  32. package/dist/presets/image.d.ts +14 -0
  33. package/dist/presets/index.d.ts +5 -0
  34. package/dist/presets/navigation.d.ts +17 -0
  35. package/dist/presets/page-transition.d.ts +26 -0
  36. package/dist/presets/typography.d.ts +20 -0
  37. package/dist/presets.js +138 -0
  38. package/dist/reveal-By5zvLLc.js +39 -0
  39. package/dist/scroll/create-smooth-scroll.d.ts +37 -0
  40. package/dist/scroll/gsap-integration.d.ts +8 -0
  41. package/dist/scroll/index.d.ts +1 -0
  42. package/dist/scroll.js +89 -0
  43. package/dist/split-text-Cnl9gkUu.js +141 -0
  44. package/dist/targets-x7JiHhWI.js +49 -0
  45. package/dist/timeline-Cg3eq5ft.js +60 -0
  46. package/dist/vue/index.d.ts +27 -0
  47. package/dist/vue.js +33 -0
  48. package/package.json +97 -4
  49. package/styles/motion.css +4 -0
  50. package/styles/presets.css +58 -0
  51. package/styles/tokens.css +23 -0
  52. package/styles/utilities.css +128 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lovince Tech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,255 @@
1
- # Temporary Holding Version
1
+ # @lovince/motion
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A framework-agnostic motion system. **CSS** handles light interface motion,
4
+ **GSAP** orchestrates timelines and scroll scenes, and **Lenis** provides
5
+ smooth scrolling. Every animation has an owner, a cleanup and a
6
+ reduced-motion policy.
7
+
8
+ > **Status: `0.1.0`, experimental.** The API may change before `1.0`.
9
+
10
+ ```bash
11
+ bun add @lovince/motion
12
+ bun add gsap lenis # only for the entries that use them
13
+ ```
14
+
15
+ The goal is not to animate everything. It is to make the motion a site does
16
+ have consistent, interruptible and accessible.
17
+
18
+ ## Entries
19
+
20
+ Each engine sits behind its own entry, so an app pays only for what it imports
21
+ and a missing optional peer breaks nothing else.
22
+
23
+ | Import | Needs | Gives |
24
+ |---|---|---|
25
+ | `@lovince/motion` | — | `createMotion`, preferences, `revealOnScroll`, `fadeIn`, `splitText`, `focusTarget`, `whenMotionAllowed`, motion config and tokens |
26
+ | `@lovince/motion/gsap` | `gsap` | `createTimeline`, `createStagger`, `createScrollReveal`, `createParallax`, `createHorizontalScroll`, `gsapScrollDriver` |
27
+ | `@lovince/motion/scroll` | `lenis` | `createSmoothScroll` |
28
+ | `@lovince/motion/presets` | `gsap` | `heroEntrance`, `revealText`, `revealImage`, `followState`, `createPageTransition` |
29
+ | `@lovince/motion/vue` | `vue` | `useMotion`, `useMotionEffect`, `v-reveal` |
30
+ | `@lovince/motion/styles.css` | — | tokens + utilities + reveal presets (or `tokens.css`, `utilities.css`, `presets.css` on their own) |
31
+
32
+ `gsap`, `lenis` and `vue` are optional peers, so your app controls their
33
+ versions and there is never a second copy at runtime.
34
+
35
+ ## Who owns what
36
+
37
+ | Engine | Owns |
38
+ |---|---|
39
+ | CSS | Hover, focus and press feedback, underlines, small transitions, decorative loops, simple scroll reveals |
40
+ | GSAP | Hero sequences, staggered and split typography, scroll-linked scenes, pinning, horizontal galleries, page transitions |
41
+ | Lenis | Smooth scrolling, as one service per scroller, on GSAP's clock when both are used |
42
+
43
+ Give each animation one owner. Never animate the same element with CSS and
44
+ GSAP at once.
45
+
46
+ ## Quick start
47
+
48
+ ```ts
49
+ import '@lovince/motion/styles.css'
50
+ import { createMotion, revealOnScroll } from '@lovince/motion'
51
+ import { gsapScrollDriver } from '@lovince/motion/gsap'
52
+ import { heroEntrance } from '@lovince/motion/presets'
53
+ import { createSmoothScroll } from '@lovince/motion/scroll'
54
+
55
+ const motion = createMotion({ root: '#app' })
56
+
57
+ heroEntrance({ label: '.eyebrow', heading: 'h1', body: '.lede', actions: '.ctas', media: '.hero-image' }, { context: motion })
58
+ revealOnScroll('[data-reveal]', { context: motion })
59
+ const scroll = createSmoothScroll({ driver: gsapScrollDriver() })
60
+
61
+ // When this view goes away:
62
+ motion.destroy() // reverts every tween, trigger, observer and split it owns
63
+ scroll.destroy()
64
+ ```
65
+
66
+ ## The model
67
+
68
+ - **`createMotion()` is the owner.** It scopes selectors to a root, holds every
69
+ effect and cleanup, and `destroy()` reverts them all, newest first. Each API
70
+ takes `{ context }`. Without one, the call gets its own context and returns
71
+ a cleanup.
72
+ - **Effects re-run when the preference changes.** An effect sets up and returns
73
+ its undo. If the user switches reduced motion on or off while the page is
74
+ open, the effect is undone and run again with `scope.reduced` updated.
75
+ Progress carries over, so a finished entrance never replays.
76
+ - **Outcomes, not timeouts.** Anything that ends returns a promise that settles
77
+ once, `'finished'` or `'cancelled'`, and never rejects. Destroying the
78
+ owning context cancels it. Nothing waits on a timer.
79
+ - **One vocabulary.** The entrances `fade`, `fade-up`, `fade-down`,
80
+ `slide-left`, `slide-right`, `scale` and `clip` mean the same in CSS
81
+ reveals, `fadeIn` (Web Animations) and every GSAP API.
82
+ - **Tokens decide.** Durations, curves and travel distances come from
83
+ `@lovince/tokens`. `defaultMotionConfig` uses `duration.travel` and
84
+ `ease.out` (`power3.out`), and the CSS `--motion-*` properties are aliases
85
+ of `--lovince-*` ones, so CSS and GSAP move alike.
86
+
87
+ ```ts
88
+ const config: MotionConfig = { duration: 0.72, ease: 'power3.out', stagger: 0.08, respectReducedMotion: true }
89
+ ```
90
+
91
+ ## Reduced motion
92
+
93
+ One central policy: under `prefers-reduced-motion: reduce`, motion is replaced
94
+ by its end state, not merely shortened.
95
+
96
+ | API | Under reduced motion |
97
+ |---|---|
98
+ | `revealOnScroll`, `createScrollReveal` | Content is shown; nothing is hidden |
99
+ | `fadeIn`, `createTimeline`, presets | Jump to the end state; promises settle `finished` |
100
+ | `createParallax`, `createHorizontalScroll` | Not created; the static layout stands |
101
+ | `revealText` | The text is never split |
102
+ | `followState` | States apply instantly |
103
+ | `createSmoothScroll` | No Lenis; native scrolling, switching live |
104
+
105
+ The stylesheet has the same rules, so CSS-only motion respects it too. Set
106
+ `respectReducedMotion: false` only for motion that carries meaning.
107
+
108
+ ## Progressive enhancement
109
+
110
+ Content is never hidden by CSS alone. A reveal hides an element only by
111
+ setting `data-motion-state="hidden"` from script, and cleanup removes it. If
112
+ JavaScript, GSAP or the stylesheet fails to load, the page is simply static.
113
+ On the server no effect runs, and every entry imports safely.
114
+
115
+ ## CSS layer
116
+
117
+ ```html
118
+ <a class="motion-link" href="/docs">Docs</a>
119
+ <section data-motion-reveal="fade-up">…</section>
120
+ <h1 class="motion-fade-up" style="--motion-delay: 0.1s">Hello</h1>
121
+ ```
122
+
123
+ `revealOnScroll(targets, { effect, once, threshold, rootMargin, stagger })` uses
124
+ IntersectionObserver, with no GSAP. Elements entering together get a
125
+ `--motion-delay` stagger. `fadeIn(targets, { effect, distance, duration,
126
+ stagger })` runs a Web Animations entrance with `fill: backwards`, so nothing
127
+ lingers afterwards.
128
+
129
+ Utilities: `.motion-link`, `.motion-lift`, `.motion-press`, `.motion-fade-up`,
130
+ `.motion-fade-in`, `.motion-scale-in`, `.motion-spin`, `.motion-pulse`. All of
131
+ them are opt-in classes, and none touches an element that didn't ask for it.
132
+
133
+ ## GSAP layer
134
+
135
+ ```ts
136
+ const intro = createTimeline((tl, scope) => {
137
+ tl.from(scope.select('h1'), { autoAlpha: 0, y: 28 })
138
+ .from(scope.select('.lede'), { autoAlpha: 0, y: 18, duration: 0.5 }, '-=0.4')
139
+ }, { context: motion })
140
+
141
+ await intro.finished // 'finished' | 'cancelled'
142
+ ```
143
+
144
+ - `createTimeline(build, { context, vars, scrollTrigger })`: tweens are
145
+ reverted on destroy. A `scrollTrigger` is dropped under reduced motion.
146
+ - `createStagger(targets, { effect, stagger, onScroll })`: a collection, in
147
+ document order.
148
+ - `createScrollReveal(targets, { effect, start, repeat })`: batches items
149
+ entering together.
150
+ - `createParallax(targets, { speed, axis })`: linear, scrubbed, measured once.
151
+ Keep `speed` small.
152
+ - `createHorizontalScroll(section, track, { minWidth })`: pins below
153
+ `minWidth` only. Tabbing to an off-screen item scrolls it into view.
154
+ - `splitText(element, { by, mask, onResplit })`: words, chars or lines. It
155
+ keeps inline markup, handles any script via `Intl.Segmenter`, reads whole
156
+ words to screen readers, and re-splits lines when the width changes.
157
+ `revert()` restores the original nodes.
158
+
159
+ ScrollTrigger is registered on first use, not at import.
160
+
161
+ ## Smooth scrolling
162
+
163
+ ```ts
164
+ const scroll = createSmoothScroll({ driver: gsapScrollDriver(), lenis: { lerp: 0.1 } })
165
+ scroll.scrollTo('#pricing', { offset: -64 })
166
+ scroll.stop() // e.g. while a modal is open
167
+ ```
168
+
169
+ There is one service per scroller. A second call for the same scroller
170
+ shares the instance, and the last `destroy()` tears it down, so components
171
+ can each ask for it safely. With `gsapScrollDriver()`, Lenis runs on GSAP's
172
+ ticker and keeps ScrollTrigger in sync: one frame loop, not two. Without a
173
+ driver, Lenis runs its own loop. Prefer native scrolling for apps, long forms
174
+ and content-heavy pages.
175
+
176
+ ## Presets
177
+
178
+ - `heroEntrance({ label, heading, body, actions, media })`: reading order,
179
+ overlapped into one entrance. `actions` children stagger.
180
+ - `revealText(target, { by: 'words' | 'lines' | 'chars', onScroll })`: line
181
+ reveals rise from behind a mask, and rebuild where they were on resize.
182
+ - `revealImage(frames, { scale })`: the frame opens upward while the image
183
+ settles from a slight zoom. Only clip-path and transform change.
184
+ - `followState(target, { attribute, states })`: animates to whatever state the
185
+ component reports (e.g. `data-state` from `@lovince/ui-vue`), so a menu can
186
+ never look open while it is closed.
187
+ - `createPageTransition({ leave, enter, focus })`: `await leave(old)`, swap,
188
+ `enter(next)`. Every call settles, a new call cancels the last, and focus
189
+ moves into the new page.
190
+
191
+ ## Frameworks
192
+
193
+ **Vanilla.** Create a context after the DOM exists, and call
194
+ `destroy()` when the view is removed.
195
+
196
+ **Vue / Nuxt.**
197
+
198
+ ```vue
199
+ <script setup lang="ts">
200
+ import { useMotion, useMotionEffect, vReveal } from '@lovince/motion/vue'
201
+ import { createSmoothScroll } from '@lovince/motion/scroll'
202
+ import { gsapScrollDriver } from '@lovince/motion/gsap'
203
+ import { heroEntrance } from '@lovince/motion/presets'
204
+
205
+ const root = ref<HTMLElement | null>(null)
206
+ useMotion((motion) => heroEntrance({ heading: 'h1', body: 'p' }, { context: motion }), { root })
207
+ const scroll = useMotionEffect(() => createSmoothScroll({ driver: gsapScrollDriver() }))
208
+ </script>
209
+
210
+ <template>
211
+ <section ref="root"><h1>Hello</h1><p v-reveal="'fade-up'">…</p></section>
212
+ </template>
213
+ ```
214
+
215
+ Everything starts on mount and is reverted on unmount. Nothing runs during
216
+ server rendering.
217
+
218
+ **React.**
219
+
220
+ ```tsx
221
+ useLayoutEffect(() => {
222
+ const motion = createMotion({ root: ref.current })
223
+ heroEntrance({ heading: 'h1' }, { context: motion })
224
+ return motion.destroy // Strict Mode's double run is safe: destroy reverts fully
225
+ }, [])
226
+ ```
227
+
228
+ **Svelte.**
229
+
230
+ ```svelte
231
+ <script>
232
+ import { onMount } from 'svelte'
233
+ import { createMotion, revealOnScroll } from '@lovince/motion'
234
+ let root
235
+ onMount(() => {
236
+ const motion = createMotion({ root })
237
+ revealOnScroll('[data-reveal]', { context: motion })
238
+ return motion.destroy
239
+ })
240
+ </script>
241
+ ```
242
+
243
+ ## Performance
244
+
245
+ - Only `transform`, `opacity` and, for reveals, `clip-path` animate.
246
+ - Parallax and pinning measure through ScrollTrigger's refresh, never per
247
+ scroll event. Line splits re-measure only when the width changes.
248
+ - No permanent `will-change`. GSAP sets it per tween.
249
+ - One clock: Lenis rides GSAP's ticker when both are present.
250
+ - The main entry is under 6 KB gzip with no engine. GSAP and Lenis load only
251
+ through their entries.
252
+
253
+ ## License
254
+
255
+ MIT
@@ -0,0 +1,9 @@
1
+ import { MotionTarget } from '../core/types';
2
+ /**
3
+ * Moves focus to the first element a target names, making it focusable for
4
+ * this purpose if it is not. Used after page transitions, so keyboard and
5
+ * screen-reader users land on the new content. Returns whether focus moved.
6
+ */
7
+ export declare const focusTarget: (target: MotionTarget, options?: {
8
+ readonly preventScroll?: boolean;
9
+ }) => boolean;
@@ -0,0 +1,2 @@
1
+ export { focusTarget } from './focus-management';
2
+ export { whenMotionAllowed } from './reduced-motion';
@@ -0,0 +1,7 @@
1
+ import { Cleanup } from '../core/types';
2
+ /**
3
+ * Runs `setup` only while the user has not asked for reduced motion, tearing
4
+ * it down when they do and setting it up again if they change back. For
5
+ * motion that has no reduced form at all, such as decorative loops.
6
+ */
7
+ export declare const whenMotionAllowed: (setup: () => Cleanup | void) => Cleanup;
@@ -0,0 +1,10 @@
1
+ import { Cleanup } from './types';
2
+ export declare const noop: Cleanup;
3
+ export type Disposer = {
4
+ /** Registers a cleanup; runs it at once if already disposed. Returns a function that runs it early. */
5
+ readonly add: (cleanup: Cleanup) => Cleanup;
6
+ /** Runs every cleanup, newest first. Safe to call more than once. */
7
+ readonly dispose: () => void;
8
+ readonly disposed: () => boolean;
9
+ };
10
+ export declare const createDisposer: () => Disposer;
@@ -0,0 +1,7 @@
1
+ import { DistanceName, MotionConfig } from './types';
2
+ export declare const defaultMotionConfig: MotionConfig;
3
+ export declare const resolveConfig: (overrides?: Partial<MotionConfig>) => MotionConfig;
4
+ /** Travel distances, in rem, on the spacing scale. The CSS layer reads the same steps as `--motion-distance-*`. */
5
+ export declare const motionDistance: Readonly<Record<DistanceName, string>>;
6
+ /** A distance in pixels, for engines that animate numbers. Follows the user's root font size. */
7
+ export declare const distancePx: (name: DistanceName) => number;
@@ -0,0 +1,40 @@
1
+ import { Cleanup, MotionConfig, MotionTarget } from './types';
2
+ export type MotionRoot = Element | Document;
3
+ /** What an effect sees each time it runs. */
4
+ export type MotionScope = {
5
+ readonly root: MotionRoot;
6
+ readonly config: MotionConfig;
7
+ /** True when motion should be skipped: the user asked for less and the config respects it. */
8
+ readonly reduced: boolean;
9
+ readonly select: (target: MotionTarget) => Element[];
10
+ };
11
+ /**
12
+ * Sets an animation up and returns how to undo it. It runs again, after its
13
+ * cleanup, whenever the motion preference changes, so carry anything that must
14
+ * survive a re-run (progress, "already played") outside the function.
15
+ */
16
+ export type MotionEffect = (scope: MotionScope) => Cleanup | void;
17
+ export type MotionContext = {
18
+ readonly root: MotionRoot | null;
19
+ readonly config: MotionConfig;
20
+ readonly reduced: () => boolean;
21
+ readonly select: (target: MotionTarget) => Element[];
22
+ readonly effect: (setup: MotionEffect) => Cleanup;
23
+ /** Ties any other teardown to this context. */
24
+ readonly add: (cleanup: Cleanup) => Cleanup;
25
+ readonly destroy: () => void;
26
+ readonly destroyed: () => boolean;
27
+ };
28
+ export type MotionOptions = {
29
+ /** Scopes selectors and GSAP contexts. A selector is looked up in the document. */
30
+ readonly root?: MotionRoot | string | null;
31
+ readonly config?: Partial<MotionConfig>;
32
+ };
33
+ /**
34
+ * A scope that owns animations: every effect and cleanup registered here ends
35
+ * with `destroy()`. On the server, and when its root is missing, effects never
36
+ * run, so nothing is hidden that could not be revealed.
37
+ */
38
+ export declare const createMotion: (options?: MotionOptions) => MotionContext;
39
+ /** Runs an effect in the given context, or in one of its own that the returned cleanup destroys. */
40
+ export declare const runInContext: (context: MotionContext | undefined, setup: MotionEffect, config?: Partial<MotionConfig>) => Cleanup;
@@ -0,0 +1,22 @@
1
+ import { DistanceName, EntranceName } from './types';
2
+ /** Where an entrance starts, as engine-neutral numbers. `x`/`y` are in units of the chosen travel distance. */
3
+ export type EntranceFrom = {
4
+ readonly opacity: number;
5
+ readonly x: number;
6
+ readonly y: number;
7
+ readonly scale: number;
8
+ /** Starts clipped away from the bottom edge, revealed upward. */
9
+ readonly clip: boolean;
10
+ };
11
+ export declare const ENTRANCES: Readonly<Record<EntranceName, EntranceFrom>>;
12
+ export declare const CLIP_HIDDEN = "inset(0% 0% 100% 0%)";
13
+ export declare const CLIP_SHOWN = "inset(0% 0% 0% 0%)";
14
+ export type ResolvedEntrance = {
15
+ readonly opacity: number;
16
+ readonly x: number;
17
+ readonly y: number;
18
+ readonly scale: number;
19
+ readonly clipPath: string | null;
20
+ };
21
+ /** An entrance's starting state in pixels, ready for any engine. */
22
+ export declare const resolveEntrance: (name: EntranceName, distance?: DistanceName) => ResolvedEntrance;
@@ -0,0 +1,7 @@
1
+ export { createDisposer, noop, type Disposer } from './cleanup';
2
+ export { defaultMotionConfig, distancePx, motionDistance, resolveConfig } from './config';
3
+ export { createMotion, runInContext, type MotionContext, type MotionEffect, type MotionOptions, type MotionRoot, type MotionScope, } from './context';
4
+ export { CLIP_HIDDEN, CLIP_SHOWN, ENTRANCES, resolveEntrance, type EntranceFrom, type ResolvedEntrance } from './entrance';
5
+ export { getMotionPreference, prefersReducedMotion, watchMotionPreference } from './preferences';
6
+ export { isElement, resolveTargets } from './targets';
7
+ export { DISTANCE_NAMES, ENTRANCE_NAMES, type Cleanup, type DistanceName, type EntranceName, type MotionConfig, type MotionOutcome, type MotionPreference, type MotionTarget, } from './types';
@@ -0,0 +1,6 @@
1
+ import { Cleanup, MotionPreference } from './types';
2
+ /** The user's motion preference. `no-preference` on the server and where it cannot be read. */
3
+ export declare const getMotionPreference: () => MotionPreference;
4
+ export declare const prefersReducedMotion: () => boolean;
5
+ /** Calls back whenever the preference changes while the page is open. */
6
+ export declare const watchMotionPreference: (listener: (preference: MotionPreference) => void) => Cleanup;
@@ -0,0 +1,4 @@
1
+ import { MotionTarget } from './types';
2
+ export declare const isElement: (value: unknown) => value is Element;
3
+ /** Every element a target names, in order and without duplicates. Missing targets and bad selectors give `[]`, never a throw. */
4
+ export declare const resolveTargets: (target: MotionTarget, scope?: ParentNode | null) => Element[];
@@ -0,0 +1,20 @@
1
+ /** What an animation acts on. Selectors resolve inside the owning context's root. */
2
+ export type MotionTarget = Element | string | Iterable<Element> | ArrayLike<Element> | null | undefined;
3
+ export type Cleanup = () => void;
4
+ export type MotionPreference = 'reduce' | 'no-preference';
5
+ export type MotionConfig = {
6
+ /** Seconds, as GSAP counts them. */
7
+ readonly duration: number;
8
+ /** A GSAP ease name; the CSS layer uses the matching token curve. */
9
+ readonly ease: string;
10
+ /** Seconds between items in a collection. */
11
+ readonly stagger: number;
12
+ readonly respectReducedMotion: boolean;
13
+ };
14
+ export declare const ENTRANCE_NAMES: readonly ["fade", "fade-up", "fade-down", "slide-left", "slide-right", "scale", "clip"];
15
+ /** One vocabulary for every engine: CSS reveals, WAAPI and GSAP all accept these names. */
16
+ export type EntranceName = (typeof ENTRANCE_NAMES)[number];
17
+ export declare const DISTANCE_NAMES: readonly ["sm", "md", "lg"];
18
+ export type DistanceName = (typeof DISTANCE_NAMES)[number];
19
+ /** How an animation ended. Cancellation is an outcome, never a rejection. */
20
+ export type MotionOutcome = 'finished' | 'cancelled';
@@ -0,0 +1,9 @@
1
+ export declare const styleOf: (element: Element) => CSSStyleDeclaration | null;
2
+ /** The CSS curve for a GSAP ease name from the token set, so both engines move alike. */
3
+ export declare const cssEase: (gsapEase: string) => string;
4
+ /** Resolves once, with an outcome; later calls are ignored. */
5
+ export declare const createOutcome: <T>() => {
6
+ readonly promise: Promise<T>;
7
+ readonly settle: (value: T) => void;
8
+ readonly settled: () => boolean;
9
+ };
@@ -0,0 +1,25 @@
1
+ import { MotionContext } from '../core/context';
2
+ import { DistanceName, EntranceName, MotionOutcome, MotionTarget } from '../core/types';
3
+ export type FadeInOptions = {
4
+ readonly context?: MotionContext;
5
+ readonly effect?: EntranceName;
6
+ readonly distance?: DistanceName;
7
+ /** Seconds. Defaults to the context's duration. */
8
+ readonly duration?: number;
9
+ readonly delay?: number;
10
+ readonly stagger?: number;
11
+ /** A CSS easing; defaults to the token curve matching the context's GSAP ease. */
12
+ readonly easing?: string;
13
+ };
14
+ export type MotionAnimation = {
15
+ /** Settles once: `finished`, or `cancelled` when stopped or its context is destroyed. Never rejects. */
16
+ readonly finished: Promise<MotionOutcome>;
17
+ readonly cancel: () => void;
18
+ };
19
+ /**
20
+ * A one-shot entrance on the Web Animations API: no GSAP, nothing left behind.
21
+ * `fill: backwards` hides elements only while they wait their turn; once done
22
+ * the animation drops away and the element's own styles apply. With reduced
23
+ * motion it settles at once and the content simply stays visible.
24
+ */
25
+ export declare const fadeIn: (targets: MotionTarget, options?: FadeInOptions) => MotionAnimation;
@@ -0,0 +1,3 @@
1
+ export { cssEase } from './dom';
2
+ export { fadeIn, type FadeInOptions, type MotionAnimation } from './fade-in';
3
+ export { REVEAL_ATTR, STATE_ATTR, revealOnScroll, type RevealOptions } from './reveal';
@@ -0,0 +1,22 @@
1
+ import { MotionContext } from '../core/context';
2
+ import { Cleanup, EntranceName, MotionTarget } from '../core/types';
3
+ export declare const REVEAL_ATTR = "data-motion-reveal";
4
+ export declare const STATE_ATTR = "data-motion-state";
5
+ export type RevealOptions = {
6
+ readonly context?: MotionContext;
7
+ /** Used when an element has no `data-motion-reveal` of its own. */
8
+ readonly effect?: EntranceName;
9
+ /** Reveal once, or hide again on leaving the viewport. */
10
+ readonly once?: boolean;
11
+ readonly threshold?: number;
12
+ readonly rootMargin?: string;
13
+ /** Seconds between elements that enter together. */
14
+ readonly stagger?: number;
15
+ };
16
+ /**
17
+ * Reveals elements as they scroll into view, with the stylesheet's
18
+ * `[data-motion-reveal]` presets doing the motion. Content is hidden only once
19
+ * this has run, and cleanup leaves it visible, so a failed or skipped setup
20
+ * never strands it invisible. Needs no GSAP.
21
+ */
22
+ export declare const revealOnScroll: (targets: MotionTarget, options?: RevealOptions) => Cleanup;
@@ -0,0 +1,74 @@
1
+ import { a as e, n as t, o as n, r, s as i, t as a } from "./targets-x7JiHhWI.js";
2
+ import { duration as o, ease as s, space as c } from "@lovince/tokens";
3
+ //#region src/core/config.ts
4
+ var l = {
5
+ duration: o.travel,
6
+ ease: s.out.gsap,
7
+ stagger: .08,
8
+ respectReducedMotion: !0
9
+ }, u = (e = {}) => ({
10
+ ...l,
11
+ ...e
12
+ }), d = {
13
+ sm: c[3],
14
+ md: c[6],
15
+ lg: c[12]
16
+ }, f = () => {
17
+ if (typeof document > "u") return 16;
18
+ let e = Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
19
+ return Number.isFinite(e) && e > 0 ? e : 16;
20
+ }, p = (e) => Number.parseFloat(d[e]) * f(), m = (e) => typeof document > "u" ? null : e == null ? document : typeof e == "string" ? t(e)[0] ?? null : a(e) || e instanceof Document ? e : null, h = (a = {}) => {
21
+ let o = u(a.config), s = m(a.root), c = n(), l = /* @__PURE__ */ new Set(), d = r(), f = () => o.respectReducedMotion && d === "reduce", p = (e) => s === null ? [] : t(e, s), h = () => s === null ? null : {
22
+ root: s,
23
+ config: o,
24
+ reduced: f(),
25
+ select: p
26
+ }, g = (e) => {
27
+ let t = h();
28
+ e.cleanup = (t === null ? void 0 : e.setup(t)) ?? i;
29
+ };
30
+ return c.add(e((e) => {
31
+ if (e !== d) {
32
+ d = e;
33
+ for (let e of l) e.cleanup(), g(e);
34
+ }
35
+ })), c.add(() => {
36
+ for (let e of [...l].reverse()) e.cleanup();
37
+ l.clear();
38
+ }), {
39
+ root: s,
40
+ config: o,
41
+ reduced: f,
42
+ select: p,
43
+ effect: (e) => {
44
+ if (c.disposed()) return i;
45
+ let t = {
46
+ setup: e,
47
+ cleanup: i
48
+ };
49
+ return l.add(t), g(t), () => {
50
+ l.delete(t) && t.cleanup();
51
+ };
52
+ },
53
+ add: c.add,
54
+ destroy: c.dispose,
55
+ destroyed: c.disposed
56
+ };
57
+ }, g = (e, t, n) => {
58
+ if (e !== void 0) return e.effect(t);
59
+ let r = h({ config: n });
60
+ return r.effect(t), r.destroy;
61
+ }, _ = (e) => typeof HTMLElement < "u" && (e instanceof HTMLElement || e instanceof SVGElement) ? e.style : null, v = (e) => Object.values(s).find((t) => t.gsap === e)?.css ?? s.out.css, y = () => {
62
+ let e = () => void 0, t = !1;
63
+ return {
64
+ promise: new Promise((t) => {
65
+ e = t;
66
+ }),
67
+ settle: (n) => {
68
+ t || (t = !0, e(n));
69
+ },
70
+ settled: () => t
71
+ };
72
+ };
73
+ //#endregion
74
+ export { g as a, d as c, h as i, u as l, v as n, l as o, _ as r, p as s, y as t };
@@ -0,0 +1,8 @@
1
+ import { n as e } from "./targets-x7JiHhWI.js";
2
+ //#region src/accessibility/focus-management.ts
3
+ var t = "a[href], button, input, select, textarea, summary, [tabindex], [contenteditable=\"true\"]", n = (n, r = {}) => {
4
+ let i = e(n)[0];
5
+ return i === void 0 || !(i instanceof HTMLElement || i instanceof SVGElement) ? !1 : (i.matches(t) || (i.setAttribute("tabindex", "-1"), i.addEventListener("blur", () => i.removeAttribute("tabindex"), { once: !0 })), i.focus({ preventScroll: r.preventScroll ?? !0 }), i.ownerDocument.activeElement === i);
6
+ };
7
+ //#endregion
8
+ export { n as t };
@@ -0,0 +1,17 @@
1
+ import { ScrollTrigger } from 'gsap/ScrollTrigger';
2
+ import { MotionScope } from '../core/context';
3
+ import { DistanceName, EntranceName } from '../core/types';
4
+ /** ScrollTrigger, registered on first use rather than at import, so importing this layer has no side effects. */
5
+ export declare const scrollTrigger: () => typeof ScrollTrigger;
6
+ /** The element GSAP should scope selectors and `gsap.context` to; the document needs none. */
7
+ export declare const scopeElement: (scope: MotionScope) => Element | undefined;
8
+ /** Where an entrance starts, as GSAP vars. `autoAlpha` also hides it from assistive tech until it shows. */
9
+ export declare const entranceFrom: (effect: EntranceName, distance?: DistanceName) => gsap.TweenVars;
10
+ /** Where every entrance ends. */
11
+ export declare const entranceTo: (effect: EntranceName) => gsap.TweenVars;
12
+ /** Adds an entrance to a timeline. `fromTo` keeps clip reveals exact; `gsap.context` reverts either way. */
13
+ export declare const addEntrance: (timeline: gsap.core.Timeline, targets: gsap.TweenTarget, effect: EntranceName, options?: {
14
+ readonly distance?: DistanceName;
15
+ readonly vars?: gsap.TweenVars;
16
+ readonly position?: gsap.Position;
17
+ }) => gsap.core.Timeline;
@@ -0,0 +1,14 @@
1
+ import { MotionContext } from '../core/context';
2
+ import { Cleanup, MotionTarget } from '../core/types';
3
+ export type HorizontalScrollOptions = {
4
+ readonly context?: MotionContext;
5
+ /** Below this width the section stays a normal, natively scrollable layout. */
6
+ readonly minWidth?: string;
7
+ };
8
+ /**
9
+ * Pins a section and moves its track sideways as the page scrolls. Narrow
10
+ * screens and reduced motion get the static layout. Keyboard focus moving to
11
+ * an off-screen item scrolls the page to the point where that item is in view,
12
+ * so tabbing through the gallery always shows what is focused.
13
+ */
14
+ export declare const createHorizontalScroll: (section: MotionTarget, track: MotionTarget, options?: HorizontalScrollOptions) => Cleanup;
@@ -0,0 +1,8 @@
1
+ export { addEntrance, entranceFrom, entranceTo, scrollTrigger } from './engine';
2
+ export { createHorizontalScroll, type HorizontalScrollOptions } from './horizontal-scroll';
3
+ export { createParallax, type ParallaxOptions } from './parallax';
4
+ export { createScrollReveal, type ScrollRevealOptions } from './scroll-reveal';
5
+ export { splitText, type SplitBy, type SplitOptions, type SplitText } from './split-text';
6
+ export { createStagger, type StaggerOptions } from './stagger';
7
+ export { createTimeline, type ManagedTimeline, type TimelineBuild, type TimelineOptions } from './timeline';
8
+ export { gsapScrollDriver } from '../scroll/gsap-integration';
@@ -0,0 +1,14 @@
1
+ import { MotionContext } from '../core/context';
2
+ import { Cleanup, MotionTarget } from '../core/types';
3
+ export type ParallaxOptions = {
4
+ readonly context?: MotionContext;
5
+ /** Travel across the viewport as a fraction of the element's size; negative moves against the scroll. Keep it small. */
6
+ readonly speed?: number;
7
+ readonly axis?: 'x' | 'y';
8
+ };
9
+ /**
10
+ * Scroll-linked drift, linear and scrubbed to scroll progress. Each element
11
+ * moves relative to its own scroll range, so nothing is measured on scroll.
12
+ * Skipped entirely under reduced motion: parallax has no gentle form.
13
+ */
14
+ export declare const createParallax: (targets: MotionTarget, options?: ParallaxOptions) => Cleanup;