@pyreon/kinetic 0.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present Vit Bokisch
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 ADDED
@@ -0,0 +1,220 @@
1
+ # @pyreon/kinetic
2
+
3
+ CSS-first animation library for Pyreon. Enter/exit transitions, staggered animations, height collapse, and list reconciliation — all in ~3KB gzipped.
4
+
5
+ ## Why Kinetic?
6
+
7
+ Most animation libraries run their own JavaScript animation loop on the main thread. Kinetic takes a different approach: it delegates all interpolation to the browser's CSS transition engine (compositor thread for `transform`/`opacity`), and only handles orchestration — mount/unmount lifecycle, stagger timing, height measurement, and list diffing.
8
+
9
+ The result: GPU-composited 60/120 FPS animations with a 3.2KB footprint.
10
+
11
+ ### How It Compares
12
+
13
+ | Library | Gzipped | Engine | Enter/Exit | Stagger | List Recon. | Collapse | Reduced Motion |
14
+ | ------- | ------- | ------ | ---------- | ------- | ----------- | -------- | -------------- |
15
+ | **@pyreon/kinetic** | **3.2 KB** | CSS transitions | Yes | Yes | Yes | Yes | Yes |
16
+ | Motion (framer-motion) | ~34 KB | JS (rAF + WAAPI) | Yes | Yes | Yes | Quirky | Yes |
17
+ | @react-spring/web | ~16-24 KB | JS (spring physics) | Yes | Partial | Yes | Manual | Yes |
18
+ | react-transition-group | ~5 KB | CSS classes | Yes | No | Yes | No | No |
19
+ | AutoAnimate | ~2.5 KB | JS (FLIP) | Yes | No | Yes | No | Yes |
20
+
21
+ **Key advantages:**
22
+ - **10x smaller than Motion** for CSS-transition use cases
23
+ - **CSS-first**: `transform`/`opacity` run on GPU compositor thread, not main thread
24
+ - **Only library** combining CSS transitions + stagger + collapse + list reconciliation
25
+ - **122 presets** available via `@pyreon/kinetic-presets`
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ bun add @pyreon/kinetic
31
+ ```
32
+
33
+ ## Quick Start
34
+
35
+ ```ts
36
+ import { kinetic, fade, slideUp } from '@pyreon/kinetic'
37
+ import { signal } from '@pyreon/reactivity'
38
+
39
+ // Create animated components at module level
40
+ const FadeDiv = kinetic('div').preset(fade)
41
+ const SlideSection = kinetic('section').preset(slideUp)
42
+
43
+ // Use with signals for reactive show/hide
44
+ const show = signal(true)
45
+
46
+ FadeDiv({ show: show(), children: 'Hello, world!' })
47
+ ```
48
+
49
+ ## API
50
+
51
+ ### `kinetic(tag)`
52
+
53
+ Creates an animated component. `tag` can be any HTML element string or Pyreon component.
54
+
55
+ ```ts
56
+ kinetic('div') // HTML element
57
+ kinetic('section') // Any HTML tag
58
+ kinetic(MyComponent) // Pyreon component
59
+ ```
60
+
61
+ Returns a renderable Pyreon component with chain methods attached. Default mode: **transition**.
62
+
63
+ ### Chain Methods
64
+
65
+ All methods return a new component (immutable). The tag generic flows through, preserving HTML attribute types.
66
+
67
+ ```ts
68
+ // Style-based animation config
69
+ .enter(styles) // CSSProperties applied at enter start
70
+ .enterTo(styles) // CSSProperties applied after first frame
71
+ .enterTransition(value) // CSS transition string for enter
72
+ .leave(styles) // CSSProperties applied at leave start
73
+ .leaveTo(styles) // CSSProperties applied after first frame
74
+ .leaveTransition(value) // CSS transition string for leave
75
+
76
+ // Class-based animation config
77
+ .enterClass({ active?, from?, to? })
78
+ .leaveClass({ active?, from?, to? })
79
+
80
+ // Apply a preset (spreads style + class props)
81
+ .preset(preset)
82
+
83
+ // Behavior config
84
+ .config(opts) // appear, unmount, timeout (+ mode-specific)
85
+ .on(callbacks) // onEnter, onAfterEnter, onLeave, onAfterLeave
86
+
87
+ // Mode switches
88
+ .collapse(opts?) // Height animation mode
89
+ .stagger(opts?) // Staggered children mode
90
+ .group() // Key-based list reconciliation mode
91
+ ```
92
+
93
+ ### Four Modes
94
+
95
+ #### Transition (default)
96
+
97
+ Single element enter/leave with CSS transitions.
98
+
99
+ ```ts
100
+ const FadeDiv = kinetic('div').preset(fade)
101
+
102
+ FadeDiv({ show: isOpen, children: 'Content' })
103
+ ```
104
+
105
+ #### Collapse
106
+
107
+ Height animation with `overflow: hidden`. Measures `scrollHeight` automatically.
108
+
109
+ ```ts
110
+ const Accordion = kinetic('div').collapse()
111
+ const FancyAccordion = kinetic('section').collapse({
112
+ transition: 'height 400ms cubic-bezier(0.4, 0, 0.2, 1)'
113
+ })
114
+
115
+ Accordion({ show: isExpanded, children: 'Expandable content' })
116
+ ```
117
+
118
+ #### Stagger
119
+
120
+ Staggered entrance/exit for child elements.
121
+
122
+ ```ts
123
+ const StaggerList = kinetic('ul').preset(slideUp).stagger({ interval: 75 })
124
+
125
+ StaggerList({ show: isVisible, children: [
126
+ h('li', { key: '1' }, 'Item 1'),
127
+ h('li', { key: '2' }, 'Item 2'),
128
+ h('li', { key: '3' }, 'Item 3'),
129
+ ]})
130
+ ```
131
+
132
+ #### Group
133
+
134
+ Key-based enter/exit — adding a child triggers enter animation, removing triggers leave + unmount. No `show` prop.
135
+
136
+ ```ts
137
+ const AnimatedList = kinetic('ul').preset(fade).group()
138
+
139
+ AnimatedList({ children: items.map(item =>
140
+ h('li', { key: item.id }, item.text)
141
+ )})
142
+ ```
143
+
144
+ ### Inline Configuration
145
+
146
+ Build animations without presets:
147
+
148
+ ```ts
149
+ const SlidePanel = kinetic('aside')
150
+ .enter({ opacity: 0, transform: 'translateX(-100%)' })
151
+ .enterTo({ opacity: 1, transform: 'translateX(0)' })
152
+ .enterTransition('all 300ms ease-out')
153
+ .leave({ opacity: 1, transform: 'translateX(0)' })
154
+ .leaveTo({ opacity: 0, transform: 'translateX(-100%)' })
155
+ .leaveTransition('all 200ms ease-in')
156
+ ```
157
+
158
+ ### Class-Based Transitions
159
+
160
+ Works with Tailwind CSS, CSS modules, or any class-based approach:
161
+
162
+ ```ts
163
+ const TailwindFade = kinetic('div')
164
+ .enterClass({ active: 'transition-opacity duration-300', from: 'opacity-0', to: 'opacity-100' })
165
+ .leaveClass({ active: 'transition-opacity duration-200', from: 'opacity-100', to: 'opacity-0' })
166
+ ```
167
+
168
+ ### Lifecycle Callbacks
169
+
170
+ ```ts
171
+ FadeDiv({
172
+ show: isOpen,
173
+ onEnter: () => console.log('entering'),
174
+ onAfterEnter: () => console.log('entered'),
175
+ onLeave: () => console.log('leaving'),
176
+ onAfterLeave: () => console.log('left'),
177
+ children: 'Content',
178
+ })
179
+ ```
180
+
181
+ ### Accessibility
182
+
183
+ Kinetic automatically detects `prefers-reduced-motion: reduce`. When enabled, animations are skipped instantly — callbacks still fire, but no visual animation occurs. No configuration needed.
184
+
185
+ ## Built-in Presets
186
+
187
+ Six presets are included in the core package:
188
+
189
+ ```ts
190
+ import { fade, scaleIn, slideUp, slideDown, slideLeft, slideRight } from '@pyreon/kinetic'
191
+ ```
192
+
193
+ For 122 presets, factories, and composition utilities, install `@pyreon/kinetic-presets`.
194
+
195
+ ## Composition with Rocketstyle
196
+
197
+ Kinetic and rocketstyle compose naturally:
198
+
199
+ ```ts
200
+ import rocketstyle from '@pyreon/rocketstyle'
201
+
202
+ const Button = rocketstyle()({ component: 'button', name: 'Button' })
203
+ .theme({ primaryColor: 'blue' })
204
+
205
+ const AnimatedButton = kinetic(Button).preset(fade)
206
+
207
+ // Has both rocketstyle props AND kinetic props
208
+ AnimatedButton({ show: isVisible, primary: true, size: 'large', children: 'Click me' })
209
+ ```
210
+
211
+ ## Peer Dependencies
212
+
213
+ | Package | Version |
214
+ | ------- | ------- |
215
+ | @pyreon/core | >= 0.0.1 |
216
+ | @pyreon/reactivity | >= 0.0.1 |
217
+
218
+ ## License
219
+
220
+ MIT
package/lib/index.d.ts ADDED
@@ -0,0 +1,173 @@
1
+ import { ComponentFn, Ref, VNodeChild } from "@pyreon/core";
2
+ import { Signal } from "@pyreon/reactivity";
3
+
4
+ //#region src/types.d.ts
5
+ type CSSProperties = Record<string, string | number | undefined>;
6
+ /** Internal lifecycle stages of a transition. */
7
+ type TransitionStage = "hidden" | "entering" | "entered" | "leaving";
8
+ /** Class-based transition definition. */
9
+ type ClassTransitionProps = {
10
+ /** Classes applied during the entire enter phase */enter?: string | undefined; /** Classes applied on first frame of enter, removed on next frame */
11
+ enterFrom?: string | undefined; /** Classes applied on second frame of enter, kept until complete */
12
+ enterTo?: string | undefined; /** Classes applied during the entire leave phase */
13
+ leave?: string | undefined; /** Classes applied on first frame of leave */
14
+ leaveFrom?: string | undefined; /** Classes applied on second frame of leave, kept until complete */
15
+ leaveTo?: string | undefined;
16
+ };
17
+ /** Style-object transition definition (zero-CSS option). */
18
+ type StyleTransitionProps = {
19
+ /** Inline styles for the start of enter */enterStyle?: CSSProperties | undefined; /** Inline styles for the end of enter */
20
+ enterToStyle?: CSSProperties | undefined; /** CSS transition shorthand applied during enter */
21
+ enterTransition?: string | undefined; /** Inline styles for the start of leave */
22
+ leaveStyle?: CSSProperties | undefined; /** Inline styles for the end of leave */
23
+ leaveToStyle?: CSSProperties | undefined; /** CSS transition shorthand applied during leave */
24
+ leaveTransition?: string | undefined;
25
+ };
26
+ /** Lifecycle callbacks. */
27
+ type TransitionCallbacks = {
28
+ /** Called immediately when entering begins */onEnter?: (() => void) | undefined; /** Called when enter animation completes */
29
+ onAfterEnter?: (() => void) | undefined; /** Called immediately when leaving begins */
30
+ onLeave?: (() => void) | undefined; /** Called when leave animation completes */
31
+ onAfterLeave?: (() => void) | undefined;
32
+ };
33
+ type TransitionStateResult = {
34
+ /** Current lifecycle stage (signal) */stage: Signal<TransitionStage>; /** Ref callback to attach to the transitioning element */
35
+ ref: Ref<HTMLElement> | ((node: HTMLElement | null) => void); /** Reactive accessor: whether the element should be rendered */
36
+ shouldMount: () => boolean; /** Call when the current animation finishes */
37
+ complete: () => void;
38
+ };
39
+ //#endregion
40
+ //#region src/kinetic/types.d.ts
41
+ type KineticMode = "transition" | "collapse" | "stagger" | "group";
42
+ type ClassConfig = {
43
+ active?: string | undefined;
44
+ from?: string | undefined;
45
+ to?: string | undefined;
46
+ };
47
+ type TransitionConfigOpts = {
48
+ appear?: boolean | undefined;
49
+ unmount?: boolean | undefined;
50
+ timeout?: number | undefined;
51
+ };
52
+ type CollapseConfigOpts = {
53
+ appear?: boolean | undefined;
54
+ timeout?: number | undefined;
55
+ transition?: string | undefined;
56
+ };
57
+ type StaggerConfigOpts = {
58
+ appear?: boolean | undefined;
59
+ timeout?: number | undefined;
60
+ interval?: number | undefined;
61
+ reverseLeave?: boolean | undefined;
62
+ };
63
+ type GroupConfigOpts = {
64
+ appear?: boolean | undefined;
65
+ timeout?: number | undefined;
66
+ };
67
+ type KineticTransitionProps<_Tag extends string> = Record<string, unknown> & {
68
+ show: () => boolean;
69
+ appear?: boolean | undefined;
70
+ unmount?: boolean | undefined;
71
+ timeout?: number | undefined;
72
+ children?: VNodeChild | undefined;
73
+ } & Partial<TransitionCallbacks>;
74
+ type KineticCollapseProps<_Tag extends string> = Record<string, unknown> & {
75
+ show: () => boolean;
76
+ appear?: boolean | undefined;
77
+ timeout?: number | undefined;
78
+ transition?: string | undefined;
79
+ children?: VNodeChild | undefined;
80
+ } & Partial<TransitionCallbacks>;
81
+ type KineticStaggerProps<_Tag extends string> = Record<string, unknown> & {
82
+ show: () => boolean;
83
+ appear?: boolean | undefined;
84
+ timeout?: number | undefined;
85
+ interval?: number | undefined;
86
+ reverseLeave?: boolean | undefined;
87
+ children: VNodeChild;
88
+ } & Partial<TransitionCallbacks>;
89
+ type KineticGroupProps<_Tag extends string> = Record<string, unknown> & {
90
+ appear?: boolean | undefined;
91
+ timeout?: number | undefined;
92
+ children: VNodeChild;
93
+ } & Partial<TransitionCallbacks>;
94
+ type KineticComponentProps<Tag extends string, Mode extends KineticMode> = Mode extends "collapse" ? KineticCollapseProps<Tag> : Mode extends "stagger" ? KineticStaggerProps<Tag> : Mode extends "group" ? KineticGroupProps<Tag> : KineticTransitionProps<Tag>;
95
+ type ConfigOpts<Mode extends KineticMode> = Mode extends "collapse" ? CollapseConfigOpts : Mode extends "stagger" ? StaggerConfigOpts : Mode extends "group" ? GroupConfigOpts : TransitionConfigOpts;
96
+ type KineticChain<Tag extends string, Mode extends KineticMode> = {
97
+ displayName: string;
98
+ preset: (preset: StyleTransitionProps & ClassTransitionProps) => KineticComponent<Tag, Mode>;
99
+ enter: (styles: CSSProperties) => KineticComponent<Tag, Mode>;
100
+ enterTo: (styles: CSSProperties) => KineticComponent<Tag, Mode>;
101
+ enterTransition: (value: string) => KineticComponent<Tag, Mode>;
102
+ leave: (styles: CSSProperties) => KineticComponent<Tag, Mode>;
103
+ leaveTo: (styles: CSSProperties) => KineticComponent<Tag, Mode>;
104
+ leaveTransition: (value: string) => KineticComponent<Tag, Mode>;
105
+ enterClass: (opts: ClassConfig) => KineticComponent<Tag, Mode>;
106
+ leaveClass: (opts: ClassConfig) => KineticComponent<Tag, Mode>;
107
+ config: (opts: ConfigOpts<Mode>) => KineticComponent<Tag, Mode>;
108
+ on: (callbacks: Partial<TransitionCallbacks>) => KineticComponent<Tag, Mode>;
109
+ collapse: (opts?: CollapseConfigOpts) => KineticComponent<Tag, "collapse">;
110
+ stagger: (opts?: {
111
+ interval?: number | undefined;
112
+ reverseLeave?: boolean | undefined;
113
+ }) => KineticComponent<Tag, "stagger">;
114
+ group: () => KineticComponent<Tag, "group">;
115
+ };
116
+ type KineticComponent<Tag extends string, Mode extends KineticMode = "transition"> = ComponentFn<KineticComponentProps<Tag, Mode>> & KineticChain<Tag, Mode>;
117
+ //#endregion
118
+ //#region src/kinetic.d.ts
119
+ /**
120
+ * Creates a reusable animated component via immutable chaining.
121
+ *
122
+ * @example
123
+ * ```tsx
124
+ * // Transition (default)
125
+ * const FadeDiv = kinetic('div').preset(fade)
126
+ *
127
+ * // Collapse
128
+ * const Accordion = kinetic('div').collapse()
129
+ *
130
+ * // Stagger
131
+ * const StaggerList = kinetic('ul').preset(slideUp).stagger({ interval: 50 })
132
+ *
133
+ * // Group (key-based enter/exit)
134
+ * const AnimatedList = kinetic('ul').preset(fade).group()
135
+ * ```
136
+ */
137
+ declare const kinetic: <Tag extends string>(tag: Tag) => KineticComponent<Tag, "transition">;
138
+ //#endregion
139
+ //#region src/presets.d.ts
140
+ type Preset = StyleTransitionProps & ClassTransitionProps;
141
+ declare const fade: Preset;
142
+ declare const scaleIn: Preset;
143
+ declare const slideUp: Preset;
144
+ declare const slideDown: Preset;
145
+ declare const slideLeft: Preset;
146
+ declare const slideRight: Preset;
147
+ declare const presets: {
148
+ readonly fade: Preset;
149
+ readonly scaleIn: Preset;
150
+ readonly slideUp: Preset;
151
+ readonly slideDown: Preset;
152
+ readonly slideLeft: Preset;
153
+ readonly slideRight: Preset;
154
+ };
155
+ //#endregion
156
+ //#region src/useAnimationEnd.d.ts
157
+ type UseAnimationEnd = (options: {
158
+ ref: Ref<HTMLElement>;
159
+ onEnd: () => void;
160
+ active: () => boolean;
161
+ timeout?: number | undefined;
162
+ }) => void;
163
+ declare const useAnimationEnd: UseAnimationEnd;
164
+ //#endregion
165
+ //#region src/useTransitionState.d.ts
166
+ type UseTransitionState = (options: {
167
+ show: () => boolean;
168
+ appear?: boolean | undefined;
169
+ }) => TransitionStateResult;
170
+ declare const useTransitionState: UseTransitionState;
171
+ //#endregion
172
+ export { type ClassTransitionProps, type KineticComponent, type Preset, type StyleTransitionProps, type TransitionCallbacks, type TransitionStage, type TransitionStateResult, type UseAnimationEnd, type UseTransitionState, fade, kinetic, presets, scaleIn, slideDown, slideLeft, slideRight, slideUp, useAnimationEnd, useTransitionState };
173
+ //# sourceMappingURL=index2.d.ts.map
package/lib/index.js ADDED
@@ -0,0 +1,903 @@
1
+ import { Show, createRef, h, onMount, onUnmount } from "@pyreon/core";
2
+ import { runUntracked, signal, watch } from "@pyreon/reactivity";
3
+ import { jsx } from "@pyreon/core/jsx-runtime";
4
+
5
+ //#region src/useAnimationEnd.ts
6
+ const DEFAULT_TIMEOUT = 5e3;
7
+ const useAnimationEnd = ({ ref, onEnd, active, timeout = DEFAULT_TIMEOUT }) => {
8
+ let called = false;
9
+ watch(active, (isActive) => {
10
+ if (!isActive) {
11
+ called = false;
12
+ return;
13
+ }
14
+ const el = ref.current;
15
+ if (!el) return;
16
+ called = false;
17
+ const done = () => {
18
+ if (called) return;
19
+ called = true;
20
+ el.removeEventListener("transitionend", handleEnd);
21
+ el.removeEventListener("animationend", handleEnd);
22
+ clearTimeout(timer);
23
+ onEnd();
24
+ };
25
+ const handleEnd = (e) => {
26
+ if (e.target !== el) return;
27
+ done();
28
+ };
29
+ el.addEventListener("transitionend", handleEnd);
30
+ el.addEventListener("animationend", handleEnd);
31
+ const timer = setTimeout(done, timeout);
32
+ return () => {
33
+ el.removeEventListener("transitionend", handleEnd);
34
+ el.removeEventListener("animationend", handleEnd);
35
+ clearTimeout(timer);
36
+ };
37
+ }, { immediate: true });
38
+ };
39
+
40
+ //#endregion
41
+ //#region src/useReducedMotion.ts
42
+ /**
43
+ * Inline reduced-motion check for kinetic package.
44
+ * Avoids depending on @pyreon/hooks for a single media query.
45
+ */
46
+ function useReducedMotion() {
47
+ const matches = signal(false);
48
+ let mql;
49
+ const onChange = (e) => {
50
+ matches.set(e.matches);
51
+ };
52
+ onMount(() => {
53
+ mql = window.matchMedia("(prefers-reduced-motion: reduce)");
54
+ matches.set(mql.matches);
55
+ mql.addEventListener("change", onChange);
56
+ });
57
+ onUnmount(() => {
58
+ mql?.removeEventListener("change", onChange);
59
+ });
60
+ return matches;
61
+ }
62
+
63
+ //#endregion
64
+ //#region src/kinetic/CollapseRenderer.tsx
65
+ /**
66
+ * Renders a height-animated collapse. The config.tag becomes the outer
67
+ * wrapper (overflow:hidden + animated height). An inner div measures
68
+ * scrollHeight for the target value.
69
+ */
70
+ const CollapseRenderer = ({ config, htmlProps, show, appear, timeout, transition, callbacks, children }) => {
71
+ const reducedMotion = useReducedMotion();
72
+ let wrapperRef = createRef();
73
+ const contentRef = createRef();
74
+ const effectiveAppear = appear ?? config.appear ?? false;
75
+ const effectiveTimeout = timeout ?? config.timeout ?? 5e3;
76
+ const effectiveTransition = transition ?? config.transition ?? "height 300ms ease";
77
+ const initialShow = show();
78
+ const needsAppear = effectiveAppear && initialShow;
79
+ const stage = signal(initialShow ? "entered" : "hidden");
80
+ let isInitialMount = true;
81
+ let appearTriggered = false;
82
+ if (needsAppear) {
83
+ const orig = wrapperRef;
84
+ const proxy = { current: null };
85
+ Object.defineProperty(proxy, "current", {
86
+ get() {
87
+ return orig.current;
88
+ },
89
+ set(node) {
90
+ orig.current = node;
91
+ if (node && !appearTriggered) {
92
+ appearTriggered = true;
93
+ queueMicrotask(() => stage.set("entering"));
94
+ }
95
+ }
96
+ });
97
+ wrapperRef = proxy;
98
+ }
99
+ watch(show, (showVal) => {
100
+ if (isInitialMount) {
101
+ isInitialMount = false;
102
+ return;
103
+ }
104
+ const currentStage = runUntracked(() => stage());
105
+ if (showVal && (currentStage === "hidden" || currentStage === "leaving")) stage.set("entering");
106
+ else if (!showVal && (currentStage === "entered" || currentStage === "entering")) stage.set("leaving");
107
+ }, { immediate: true });
108
+ watch(() => stage(), (currentStage) => {
109
+ const wrapper = wrapperRef.current;
110
+ const content = contentRef.current;
111
+ if (!wrapper || !content) return;
112
+ if (reducedMotion()) {
113
+ if (currentStage === "entering") {
114
+ callbacks.onEnter?.();
115
+ wrapper.style.height = "auto";
116
+ wrapper.style.overflow = "";
117
+ callbacks.onAfterEnter?.();
118
+ stage.set("entered");
119
+ } else if (currentStage === "leaving") {
120
+ callbacks.onLeave?.();
121
+ wrapper.style.height = "0px";
122
+ wrapper.style.overflow = "hidden";
123
+ callbacks.onAfterLeave?.();
124
+ stage.set("hidden");
125
+ }
126
+ return;
127
+ }
128
+ if (currentStage === "entering") {
129
+ callbacks.onEnter?.();
130
+ const height = content.scrollHeight;
131
+ wrapper.style.transition = "none";
132
+ wrapper.style.height = "0px";
133
+ wrapper.style.overflow = "hidden";
134
+ wrapper.offsetHeight;
135
+ wrapper.style.transition = effectiveTransition;
136
+ wrapper.style.height = `${height}px`;
137
+ }
138
+ if (currentStage === "leaving") {
139
+ callbacks.onLeave?.();
140
+ const height = content.scrollHeight;
141
+ wrapper.style.transition = "none";
142
+ wrapper.style.height = `${height}px`;
143
+ wrapper.style.overflow = "hidden";
144
+ wrapper.offsetHeight;
145
+ wrapper.style.transition = effectiveTransition;
146
+ wrapper.style.height = "0px";
147
+ }
148
+ }, { immediate: true });
149
+ useAnimationEnd({
150
+ ref: wrapperRef,
151
+ active: () => (stage() === "entering" || stage() === "leaving") && !reducedMotion(),
152
+ timeout: effectiveTimeout,
153
+ onEnd: () => {
154
+ const wrapper = wrapperRef.current;
155
+ if (stage() === "entering") {
156
+ if (wrapper) {
157
+ wrapper.style.height = "auto";
158
+ wrapper.style.overflow = "";
159
+ wrapper.style.transition = "";
160
+ }
161
+ callbacks.onAfterEnter?.();
162
+ stage.set("entered");
163
+ } else if (stage() === "leaving") {
164
+ callbacks.onAfterLeave?.();
165
+ stage.set("hidden");
166
+ }
167
+ }
168
+ });
169
+ const shouldRender = () => stage() !== "hidden";
170
+ const wrapperStyle = {
171
+ ...htmlProps.style ?? {},
172
+ ...stage() !== "entered" ? { overflow: "hidden" } : {},
173
+ ...stage() === "hidden" ? { height: "0px" } : stage() === "entered" ? { height: "auto" } : {}
174
+ };
175
+ return h(config.tag, {
176
+ ref: wrapperRef,
177
+ ...htmlProps,
178
+ style: wrapperStyle
179
+ }, /* @__PURE__ */ jsx(Show, {
180
+ when: shouldRender,
181
+ children: /* @__PURE__ */ jsx("div", {
182
+ ref: contentRef,
183
+ children
184
+ })
185
+ }));
186
+ };
187
+
188
+ //#endregion
189
+ //#region src/useTransitionState.ts
190
+ const useTransitionState = ({ show, appear = false }) => {
191
+ const initialShow = show();
192
+ const needsAppear = appear && initialShow;
193
+ const stage = signal(initialShow ? "entered" : "hidden");
194
+ const elementRef = createRef();
195
+ let isInitialMount = true;
196
+ let appearTriggered = false;
197
+ const refCallback = (node) => {
198
+ elementRef.current = node;
199
+ if (node && needsAppear && !appearTriggered) {
200
+ appearTriggered = true;
201
+ stage.set("entering");
202
+ }
203
+ };
204
+ watch(show, (showVal) => {
205
+ if (isInitialMount) {
206
+ isInitialMount = false;
207
+ return;
208
+ }
209
+ const currentStage = runUntracked(() => stage());
210
+ if (showVal && (currentStage === "hidden" || currentStage === "leaving")) stage.set("entering");
211
+ else if (!showVal && (currentStage === "entered" || currentStage === "entering")) stage.set("leaving");
212
+ }, { immediate: true });
213
+ const complete = () => {
214
+ const current = stage();
215
+ if (current === "entering") stage.set("entered");
216
+ if (current === "leaving") stage.set("hidden");
217
+ };
218
+ return {
219
+ stage,
220
+ ref: refCallback,
221
+ shouldMount: () => stage() !== "hidden",
222
+ complete
223
+ };
224
+ };
225
+
226
+ //#endregion
227
+ //#region src/utils.ts
228
+ const splitCache = /* @__PURE__ */ new Map();
229
+ const splitClasses = (classes) => {
230
+ let cached = splitCache.get(classes);
231
+ if (!cached) {
232
+ cached = classes.split(/\s+/).filter(Boolean);
233
+ splitCache.set(classes, cached);
234
+ }
235
+ return cached;
236
+ };
237
+ /** Adds space-separated CSS classes to an element. */
238
+ const addClasses = (el, classes) => {
239
+ if (!classes) return;
240
+ const list = splitClasses(classes);
241
+ if (list.length > 0) el.classList.add(...list);
242
+ };
243
+ /** Removes space-separated CSS classes from an element. */
244
+ const removeClasses = (el, classes) => {
245
+ if (!classes) return;
246
+ const list = splitClasses(classes);
247
+ if (list.length > 0) el.classList.remove(...list);
248
+ };
249
+ /**
250
+ * Executes callback after two animation frames (double-rAF).
251
+ * Ensures the browser paints the current state before applying changes,
252
+ * which is required for CSS transitions to trigger.
253
+ */
254
+ const nextFrame = (callback) => requestAnimationFrame(() => {
255
+ requestAnimationFrame(callback);
256
+ });
257
+ /** Merges two CSSProperties objects, with `b` taking precedence. */
258
+ const mergeStyles = (a, b) => {
259
+ if (!a && !b) return void 0;
260
+ if (!a) return b;
261
+ if (!b) return a;
262
+ return {
263
+ ...a,
264
+ ...b
265
+ };
266
+ };
267
+ /** Merges multiple refs (callback or object) into a single callback ref. */
268
+ const mergeRefs = (...refs) => {
269
+ return (node) => {
270
+ for (const ref of refs) {
271
+ if (!ref) continue;
272
+ if (typeof ref === "function") ref(node);
273
+ else ref.current = node;
274
+ }
275
+ };
276
+ };
277
+ /** Clones a VNode with merged props. */
278
+ const cloneVNode = (vnode, extraProps) => ({
279
+ ...vnode,
280
+ props: {
281
+ ...vnode.props,
282
+ ...extraProps
283
+ }
284
+ });
285
+
286
+ //#endregion
287
+ //#region src/kinetic/TransitionItem.tsx
288
+ const applyEnter$1 = (el, config) => {
289
+ addClasses(el, config.enter);
290
+ addClasses(el, config.enterFrom);
291
+ if (config.enterStyle) Object.assign(el.style, config.enterStyle);
292
+ if (config.enterTransition) el.style.transition = config.enterTransition;
293
+ return nextFrame(() => {
294
+ removeClasses(el, config.enterFrom);
295
+ addClasses(el, config.enterTo);
296
+ if (config.enterToStyle) Object.assign(el.style, config.enterToStyle);
297
+ });
298
+ };
299
+ const applyLeave$1 = (el, config) => {
300
+ removeClasses(el, config.enter);
301
+ removeClasses(el, config.enterTo);
302
+ addClasses(el, config.leave);
303
+ addClasses(el, config.leaveFrom);
304
+ if (config.leaveStyle) Object.assign(el.style, config.leaveStyle);
305
+ if (config.leaveTransition) el.style.transition = config.leaveTransition;
306
+ return nextFrame(() => {
307
+ removeClasses(el, config.leaveFrom);
308
+ addClasses(el, config.leaveTo);
309
+ if (config.leaveToStyle) Object.assign(el.style, config.leaveToStyle);
310
+ });
311
+ };
312
+ const applyReducedMotion$1 = (stage, callbacks, complete) => {
313
+ if (stage === "entering") {
314
+ callbacks.onEnter?.();
315
+ callbacks.onAfterEnter?.();
316
+ complete();
317
+ } else if (stage === "leaving") {
318
+ callbacks.onLeave?.();
319
+ callbacks.onAfterLeave?.();
320
+ complete();
321
+ }
322
+ };
323
+ /**
324
+ * Internal per-child transition component. Used by StaggerRenderer and
325
+ * GroupRenderer to give each child its own animation state.
326
+ *
327
+ * Uses cloneVNode to inject ref onto the child — the child must accept ref.
328
+ */
329
+ const TransitionItem = ({ show, appear = false, unmount = true, timeout = 5e3, enter, enterFrom, enterTo, leave, leaveFrom, leaveTo, enterStyle, enterToStyle, enterTransition, leaveStyle, leaveToStyle, leaveTransition, onEnter, onAfterEnter, onLeave, onAfterLeave, children }) => {
330
+ const reducedMotion = useReducedMotion();
331
+ const { stage, ref: stateRef, shouldMount, complete } = useTransitionState({
332
+ show,
333
+ appear
334
+ });
335
+ const elementRef = createRef();
336
+ const mergedRef = mergeRefs(elementRef, stateRef, children.props?.ref);
337
+ const callbacks = {
338
+ onEnter,
339
+ onAfterEnter,
340
+ onLeave,
341
+ onAfterLeave
342
+ };
343
+ const transitionConfig = {
344
+ enter,
345
+ enterFrom,
346
+ enterTo,
347
+ leave,
348
+ leaveFrom,
349
+ leaveTo,
350
+ enterStyle,
351
+ enterToStyle,
352
+ enterTransition,
353
+ leaveStyle,
354
+ leaveToStyle,
355
+ leaveTransition
356
+ };
357
+ useAnimationEnd({
358
+ ref: elementRef,
359
+ active: () => (stage() === "entering" || stage() === "leaving") && !reducedMotion(),
360
+ timeout,
361
+ onEnd: () => {
362
+ if (stage() === "entering") callbacks.onAfterEnter?.();
363
+ else if (stage() === "leaving") callbacks.onAfterLeave?.();
364
+ complete();
365
+ }
366
+ });
367
+ watch(() => stage(), (currentStage) => {
368
+ const el = elementRef.current;
369
+ if (!el) return;
370
+ if (reducedMotion()) {
371
+ applyReducedMotion$1(currentStage, callbacks, complete);
372
+ return;
373
+ }
374
+ if (currentStage === "entering") {
375
+ callbacks.onEnter?.();
376
+ const frameId = applyEnter$1(el, transitionConfig);
377
+ return () => cancelAnimationFrame(frameId);
378
+ }
379
+ if (currentStage === "leaving") {
380
+ callbacks.onLeave?.();
381
+ const frameId = applyLeave$1(el, transitionConfig);
382
+ return () => cancelAnimationFrame(frameId);
383
+ }
384
+ if (currentStage === "entered") {
385
+ removeClasses(el, enter);
386
+ el.style.transition = "";
387
+ }
388
+ }, { immediate: true });
389
+ return /* @__PURE__ */ jsx(Show, {
390
+ when: shouldMount,
391
+ fallback: unmount ? null : cloneVNode(children, {
392
+ ref: mergedRef,
393
+ style: mergeStyles(children.props?.style, { display: "none" })
394
+ }),
395
+ children: cloneVNode(children, { ref: mergedRef })
396
+ });
397
+ };
398
+
399
+ //#endregion
400
+ //#region src/kinetic/GroupRenderer.tsx
401
+ const isVNode$1 = (child) => child != null && typeof child === "object" && "type" in child;
402
+ const getKeyedChildren = (children) => {
403
+ const result = [];
404
+ for (const child of children) if (isVNode$1(child)) {
405
+ const key = child.key;
406
+ if (key != null) result.push({
407
+ key,
408
+ element: child
409
+ });
410
+ }
411
+ return result;
412
+ };
413
+ /**
414
+ * Renders children with key-based enter/exit animation (no `show` prop).
415
+ * Children that appear (new key) animate in. Children that disappear
416
+ * (removed key) stay in DOM during leave animation, then unmount.
417
+ * config.tag wraps all children as a container element.
418
+ */
419
+ const GroupRenderer = ({ config, htmlProps, appear, timeout, callbacks, children }) => {
420
+ const effectiveAppear = appear ?? config.appear ?? false;
421
+ const effectiveTimeout = timeout ?? config.timeout ?? 5e3;
422
+ const prevMap = /* @__PURE__ */ new Map();
423
+ const leavingMap = /* @__PURE__ */ new Map();
424
+ let initialKeys = null;
425
+ const forceUpdateSignal = signal(0);
426
+ const currentKeyed = getKeyedChildren(children);
427
+ const currentMap = /* @__PURE__ */ new Map();
428
+ for (const { key, element } of currentKeyed) currentMap.set(key, element);
429
+ if (initialKeys === null) initialKeys = new Set(currentMap.keys());
430
+ for (const [key, child] of prevMap) if (!currentMap.has(key)) leavingMap.set(key, child);
431
+ for (const key of currentMap.keys()) leavingMap.delete(key);
432
+ prevMap.clear();
433
+ for (const [key, element] of currentMap) prevMap.set(key, element);
434
+ const handleAfterLeave = (key) => {
435
+ leavingMap.delete(key);
436
+ callbacks.onAfterLeave?.();
437
+ forceUpdateSignal.update((c) => c + 1);
438
+ };
439
+ const allEntries = [...currentKeyed];
440
+ for (const [key, element] of leavingMap) allEntries.push({
441
+ key,
442
+ element
443
+ });
444
+ const groupedChildren = allEntries.map(({ key, element }) => {
445
+ const isInitial = initialKeys?.has(key) ?? false;
446
+ const isShowing = currentMap.has(key);
447
+ return /* @__PURE__ */ jsx(TransitionItem, {
448
+ show: () => isShowing,
449
+ appear: isInitial ? effectiveAppear : true,
450
+ timeout: effectiveTimeout,
451
+ enterStyle: config.enterStyle,
452
+ enterToStyle: config.enterToStyle,
453
+ enterTransition: config.enterTransition,
454
+ leaveStyle: config.leaveStyle,
455
+ leaveToStyle: config.leaveToStyle,
456
+ leaveTransition: config.leaveTransition,
457
+ enter: config.enter,
458
+ enterFrom: config.enterFrom,
459
+ enterTo: config.enterTo,
460
+ leave: config.leave,
461
+ leaveFrom: config.leaveFrom,
462
+ leaveTo: config.leaveTo,
463
+ onAfterLeave: () => handleAfterLeave(key),
464
+ children: element
465
+ });
466
+ });
467
+ return h(config.tag, { ...htmlProps }, ...groupedChildren);
468
+ };
469
+
470
+ //#endregion
471
+ //#region src/kinetic/StaggerRenderer.tsx
472
+ const isVNode = (child) => child != null && typeof child === "object" && "type" in child;
473
+ /**
474
+ * Renders children with staggered enter/exit animation.
475
+ * config.tag wraps the staggered children as a container element.
476
+ * Each child is individually animated via TransitionItem.
477
+ */
478
+ const StaggerRenderer = ({ config, htmlProps, show, appear, timeout, interval, reverseLeave, callbacks, children }) => {
479
+ const effectiveAppear = appear ?? config.appear ?? false;
480
+ const effectiveTimeout = timeout ?? config.timeout ?? 5e3;
481
+ const effectiveInterval = interval ?? config.interval ?? 50;
482
+ const effectiveReverseLeave = reverseLeave ?? config.reverseLeave ?? false;
483
+ const childArray = (Array.isArray(children) ? children : [children]).filter(isVNode);
484
+ const count = childArray.length;
485
+ const staggeredChildren = childArray.map((child, index) => {
486
+ const staggerIndex = !show() && effectiveReverseLeave ? count - 1 - index : index;
487
+ const delay = staggerIndex * effectiveInterval;
488
+ return /* @__PURE__ */ jsx(TransitionItem, {
489
+ show,
490
+ appear: effectiveAppear,
491
+ timeout: effectiveTimeout + delay,
492
+ enterStyle: config.enterStyle,
493
+ enterToStyle: config.enterToStyle,
494
+ enterTransition: config.enterTransition,
495
+ leaveStyle: config.leaveStyle,
496
+ leaveToStyle: config.leaveToStyle,
497
+ leaveTransition: config.leaveTransition,
498
+ enter: config.enter,
499
+ enterFrom: config.enterFrom,
500
+ enterTo: config.enterTo,
501
+ leave: config.leave,
502
+ leaveFrom: config.leaveFrom,
503
+ leaveTo: config.leaveTo,
504
+ onAfterLeave: index === (effectiveReverseLeave ? 0 : count - 1) ? callbacks.onAfterLeave : void 0,
505
+ children: cloneVNode(child, { style: {
506
+ ...child.props?.style,
507
+ "--stagger-index": staggerIndex,
508
+ "--stagger-interval": `${effectiveInterval}ms`,
509
+ transitionDelay: `${delay}ms`
510
+ } })
511
+ }, child.key ?? index);
512
+ });
513
+ return h(config.tag, { ...htmlProps }, ...staggeredChildren);
514
+ };
515
+
516
+ //#endregion
517
+ //#region src/kinetic/TransitionRenderer.tsx
518
+ const applyEnter = (el, config) => {
519
+ addClasses(el, config.enter);
520
+ addClasses(el, config.enterFrom);
521
+ if (config.enterStyle) Object.assign(el.style, config.enterStyle);
522
+ if (config.enterTransition) el.style.transition = config.enterTransition;
523
+ return nextFrame(() => {
524
+ removeClasses(el, config.enterFrom);
525
+ addClasses(el, config.enterTo);
526
+ if (config.enterToStyle) Object.assign(el.style, config.enterToStyle);
527
+ });
528
+ };
529
+ const applyLeave = (el, config) => {
530
+ removeClasses(el, config.enter);
531
+ removeClasses(el, config.enterTo);
532
+ addClasses(el, config.leave);
533
+ addClasses(el, config.leaveFrom);
534
+ if (config.leaveStyle) Object.assign(el.style, config.leaveStyle);
535
+ if (config.leaveTransition) el.style.transition = config.leaveTransition;
536
+ return nextFrame(() => {
537
+ removeClasses(el, config.leaveFrom);
538
+ addClasses(el, config.leaveTo);
539
+ if (config.leaveToStyle) Object.assign(el.style, config.leaveToStyle);
540
+ });
541
+ };
542
+ const applyReducedMotion = (stage, cbs, complete) => {
543
+ if (stage === "entering") {
544
+ cbs.onEnter?.();
545
+ cbs.onAfterEnter?.();
546
+ complete();
547
+ } else if (stage === "leaving") {
548
+ cbs.onLeave?.();
549
+ cbs.onAfterLeave?.();
550
+ complete();
551
+ }
552
+ };
553
+ /**
554
+ * Renders a single element with CSS transition enter/exit animation.
555
+ * Uses h(config.tag) — no cloneElement needed.
556
+ */
557
+ const TransitionRenderer = ({ config, htmlProps, show, appear, unmount, timeout, callbacks, children }) => {
558
+ const reducedMotion = useReducedMotion();
559
+ const { stage, ref: stateRef, shouldMount, complete } = useTransitionState({
560
+ show,
561
+ appear: appear ?? config.appear ?? false
562
+ });
563
+ const elementRef = createRef();
564
+ const mergedRef = mergeRefs(elementRef, stateRef);
565
+ const effectiveUnmount = unmount ?? config.unmount ?? true;
566
+ useAnimationEnd({
567
+ ref: elementRef,
568
+ active: () => (stage() === "entering" || stage() === "leaving") && !reducedMotion(),
569
+ timeout: timeout ?? config.timeout ?? 5e3,
570
+ onEnd: () => {
571
+ if (stage() === "entering") callbacks.onAfterEnter?.();
572
+ else if (stage() === "leaving") callbacks.onAfterLeave?.();
573
+ complete();
574
+ }
575
+ });
576
+ watch(() => stage(), (currentStage) => {
577
+ const el = elementRef.current;
578
+ if (!el) return;
579
+ if (reducedMotion()) {
580
+ applyReducedMotion(currentStage, callbacks, complete);
581
+ return;
582
+ }
583
+ if (currentStage === "entering") {
584
+ callbacks.onEnter?.();
585
+ const frameId = applyEnter(el, config);
586
+ return () => cancelAnimationFrame(frameId);
587
+ }
588
+ if (currentStage === "leaving") {
589
+ callbacks.onLeave?.();
590
+ const frameId = applyLeave(el, config);
591
+ return () => cancelAnimationFrame(frameId);
592
+ }
593
+ if (currentStage === "entered") {
594
+ removeClasses(el, config.enter);
595
+ el.style.transition = "";
596
+ }
597
+ }, { immediate: true });
598
+ return /* @__PURE__ */ jsx(Show, {
599
+ when: shouldMount,
600
+ fallback: effectiveUnmount ? null : h(config.tag, {
601
+ ref: mergedRef,
602
+ ...htmlProps,
603
+ style: {
604
+ ...htmlProps.style ?? {},
605
+ display: "none"
606
+ }
607
+ }, children),
608
+ children: h(config.tag, {
609
+ ref: mergedRef,
610
+ ...htmlProps
611
+ }, children)
612
+ });
613
+ };
614
+
615
+ //#endregion
616
+ //#region src/kinetic/createKineticComponent.tsx
617
+ /** Keys that are kinetic-specific and should not be forwarded as HTML attrs. */
618
+ const KINETIC_KEYS = new Set([
619
+ "show",
620
+ "appear",
621
+ "unmount",
622
+ "timeout",
623
+ "transition",
624
+ "interval",
625
+ "reverseLeave",
626
+ "onEnter",
627
+ "onAfterEnter",
628
+ "onLeave",
629
+ "onAfterLeave"
630
+ ]);
631
+ /**
632
+ * Core factory. Creates a component that delegates to the appropriate
633
+ * renderer based on config.mode, then attaches immutable chain methods
634
+ * via Object.assign.
635
+ */
636
+ const createKineticComponent = (config) => {
637
+ const Component = (props) => {
638
+ const htmlProps = {};
639
+ const kineticProps = {};
640
+ for (const key in props) if (KINETIC_KEYS.has(key)) kineticProps[key] = props[key];
641
+ else htmlProps[key] = props[key];
642
+ const { show, appear, unmount, timeout, transition, interval, reverseLeave, onEnter, onAfterEnter, onLeave, onAfterLeave } = kineticProps;
643
+ const callbacks = {
644
+ onEnter: onEnter ?? config.onEnter,
645
+ onAfterEnter: onAfterEnter ?? config.onAfterEnter,
646
+ onLeave: onLeave ?? config.onLeave,
647
+ onAfterLeave: onAfterLeave ?? config.onAfterLeave
648
+ };
649
+ const { children, ...restHtml } = htmlProps;
650
+ if (config.mode === "collapse") return /* @__PURE__ */ jsx(CollapseRenderer, {
651
+ config,
652
+ htmlProps: restHtml,
653
+ show,
654
+ appear,
655
+ timeout,
656
+ transition,
657
+ callbacks,
658
+ children
659
+ });
660
+ if (config.mode === "stagger") return /* @__PURE__ */ jsx(StaggerRenderer, {
661
+ config,
662
+ htmlProps: restHtml,
663
+ show,
664
+ appear,
665
+ timeout,
666
+ interval,
667
+ reverseLeave,
668
+ callbacks,
669
+ children
670
+ });
671
+ if (config.mode === "group") return /* @__PURE__ */ jsx(GroupRenderer, {
672
+ config,
673
+ htmlProps: restHtml,
674
+ appear,
675
+ timeout,
676
+ callbacks,
677
+ children
678
+ });
679
+ return /* @__PURE__ */ jsx(TransitionRenderer, {
680
+ config,
681
+ htmlProps: restHtml,
682
+ show,
683
+ appear,
684
+ unmount,
685
+ timeout,
686
+ callbacks,
687
+ children
688
+ });
689
+ };
690
+ Component.displayName = `kinetic(${config.tag})`;
691
+ return Object.assign(Component, {
692
+ preset: (preset) => createKineticComponent({
693
+ ...config,
694
+ ...preset
695
+ }),
696
+ enter: (styles) => createKineticComponent({
697
+ ...config,
698
+ enterStyle: styles
699
+ }),
700
+ enterTo: (styles) => createKineticComponent({
701
+ ...config,
702
+ enterToStyle: styles
703
+ }),
704
+ enterTransition: (value) => createKineticComponent({
705
+ ...config,
706
+ enterTransition: value
707
+ }),
708
+ leave: (styles) => createKineticComponent({
709
+ ...config,
710
+ leaveStyle: styles
711
+ }),
712
+ leaveTo: (styles) => createKineticComponent({
713
+ ...config,
714
+ leaveToStyle: styles
715
+ }),
716
+ leaveTransition: (value) => createKineticComponent({
717
+ ...config,
718
+ leaveTransition: value
719
+ }),
720
+ enterClass: ({ active, from, to }) => createKineticComponent({
721
+ ...config,
722
+ enter: active,
723
+ enterFrom: from,
724
+ enterTo: to
725
+ }),
726
+ leaveClass: ({ active, from, to }) => createKineticComponent({
727
+ ...config,
728
+ leave: active,
729
+ leaveFrom: from,
730
+ leaveTo: to
731
+ }),
732
+ config: (opts) => createKineticComponent({
733
+ ...config,
734
+ ...opts
735
+ }),
736
+ on: (cbs) => createKineticComponent({
737
+ ...config,
738
+ ...cbs
739
+ }),
740
+ collapse: (opts) => createKineticComponent({
741
+ ...config,
742
+ mode: "collapse",
743
+ ...opts
744
+ }),
745
+ stagger: (opts) => createKineticComponent({
746
+ ...config,
747
+ mode: "stagger",
748
+ ...opts
749
+ }),
750
+ group: () => createKineticComponent({
751
+ ...config,
752
+ mode: "group"
753
+ })
754
+ });
755
+ };
756
+
757
+ //#endregion
758
+ //#region src/kinetic.ts
759
+ /**
760
+ * Creates a reusable animated component via immutable chaining.
761
+ *
762
+ * @example
763
+ * ```tsx
764
+ * // Transition (default)
765
+ * const FadeDiv = kinetic('div').preset(fade)
766
+ *
767
+ * // Collapse
768
+ * const Accordion = kinetic('div').collapse()
769
+ *
770
+ * // Stagger
771
+ * const StaggerList = kinetic('ul').preset(slideUp).stagger({ interval: 50 })
772
+ *
773
+ * // Group (key-based enter/exit)
774
+ * const AnimatedList = kinetic('ul').preset(fade).group()
775
+ * ```
776
+ */
777
+ const kinetic = (tag) => createKineticComponent({
778
+ tag,
779
+ mode: "transition"
780
+ });
781
+
782
+ //#endregion
783
+ //#region src/presets.ts
784
+ const fade = {
785
+ enterStyle: { opacity: 0 },
786
+ enterToStyle: { opacity: 1 },
787
+ enterTransition: "opacity 300ms ease-out",
788
+ leaveStyle: { opacity: 1 },
789
+ leaveToStyle: { opacity: 0 },
790
+ leaveTransition: "opacity 200ms ease-in"
791
+ };
792
+ const scaleIn = {
793
+ enterStyle: {
794
+ opacity: 0,
795
+ transform: "scale(0.95)"
796
+ },
797
+ enterToStyle: {
798
+ opacity: 1,
799
+ transform: "scale(1)"
800
+ },
801
+ enterTransition: "opacity 300ms ease-out, transform 300ms ease-out",
802
+ leaveStyle: {
803
+ opacity: 1,
804
+ transform: "scale(1)"
805
+ },
806
+ leaveToStyle: {
807
+ opacity: 0,
808
+ transform: "scale(0.95)"
809
+ },
810
+ leaveTransition: "opacity 200ms ease-in, transform 200ms ease-in"
811
+ };
812
+ const slideUp = {
813
+ enterStyle: {
814
+ opacity: 0,
815
+ transform: "translateY(16px)"
816
+ },
817
+ enterToStyle: {
818
+ opacity: 1,
819
+ transform: "translateY(0)"
820
+ },
821
+ enterTransition: "opacity 300ms ease-out, transform 300ms ease-out",
822
+ leaveStyle: {
823
+ opacity: 1,
824
+ transform: "translateY(0)"
825
+ },
826
+ leaveToStyle: {
827
+ opacity: 0,
828
+ transform: "translateY(16px)"
829
+ },
830
+ leaveTransition: "opacity 200ms ease-in, transform 200ms ease-in"
831
+ };
832
+ const slideDown = {
833
+ enterStyle: {
834
+ opacity: 0,
835
+ transform: "translateY(-16px)"
836
+ },
837
+ enterToStyle: {
838
+ opacity: 1,
839
+ transform: "translateY(0)"
840
+ },
841
+ enterTransition: "opacity 300ms ease-out, transform 300ms ease-out",
842
+ leaveStyle: {
843
+ opacity: 1,
844
+ transform: "translateY(0)"
845
+ },
846
+ leaveToStyle: {
847
+ opacity: 0,
848
+ transform: "translateY(-16px)"
849
+ },
850
+ leaveTransition: "opacity 200ms ease-in, transform 200ms ease-in"
851
+ };
852
+ const slideLeft = {
853
+ enterStyle: {
854
+ opacity: 0,
855
+ transform: "translateX(16px)"
856
+ },
857
+ enterToStyle: {
858
+ opacity: 1,
859
+ transform: "translateX(0)"
860
+ },
861
+ enterTransition: "opacity 300ms ease-out, transform 300ms ease-out",
862
+ leaveStyle: {
863
+ opacity: 1,
864
+ transform: "translateX(0)"
865
+ },
866
+ leaveToStyle: {
867
+ opacity: 0,
868
+ transform: "translateX(16px)"
869
+ },
870
+ leaveTransition: "opacity 200ms ease-in, transform 200ms ease-in"
871
+ };
872
+ const slideRight = {
873
+ enterStyle: {
874
+ opacity: 0,
875
+ transform: "translateX(-16px)"
876
+ },
877
+ enterToStyle: {
878
+ opacity: 1,
879
+ transform: "translateX(0)"
880
+ },
881
+ enterTransition: "opacity 300ms ease-out, transform 300ms ease-out",
882
+ leaveStyle: {
883
+ opacity: 1,
884
+ transform: "translateX(0)"
885
+ },
886
+ leaveToStyle: {
887
+ opacity: 0,
888
+ transform: "translateX(-16px)"
889
+ },
890
+ leaveTransition: "opacity 200ms ease-in, transform 200ms ease-in"
891
+ };
892
+ const presets = {
893
+ fade,
894
+ scaleIn,
895
+ slideUp,
896
+ slideDown,
897
+ slideLeft,
898
+ slideRight
899
+ };
900
+
901
+ //#endregion
902
+ export { fade, kinetic, presets, scaleIn, slideDown, slideLeft, slideRight, slideUp, useAnimationEnd, useTransitionState };
903
+ //# sourceMappingURL=index.js.map
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@pyreon/kinetic",
3
+ "version": "0.0.2",
4
+ "description": "CSS-transition-based animation components for Pyreon",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ "source": "./src/index.ts",
10
+ "import": "./lib/index.js",
11
+ "types": "./lib/index.d.ts"
12
+ },
13
+ "types": "./lib/index.d.ts",
14
+ "main": "./lib/index.js",
15
+ "files": [
16
+ "lib",
17
+ "!lib/**/*.map",
18
+ "!lib/analysis",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "engines": {
23
+ "node": ">= 18"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "scripts": {
29
+ "prepublish": "bun run build",
30
+ "build": "bun run vl_rolldown_build",
31
+ "build:watch": "bun run vl_rolldown_build-watch",
32
+ "lint": "biome check src/",
33
+ "test": "vitest run",
34
+ "test:coverage": "vitest run --coverage",
35
+ "test:watch": "vitest",
36
+ "typecheck": "tsc --noEmit"
37
+ },
38
+ "peerDependencies": {
39
+ "@pyreon/core": ">=0.3.0",
40
+ "@pyreon/reactivity": ">=0.3.0"
41
+ },
42
+ "devDependencies": {
43
+ "@vitus-labs/tools-rolldown": "^1.15.0",
44
+ "@vitus-labs/tools-typescript": "^1.15.0"
45
+ }
46
+ }