@rootnative/inertia-gradients 0.0.0-alpha.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@rootnative/inertia-gradients` are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Pre-`1.0`, breaking changes may land in minor versions and are called out under their release.
4
+
5
+ This package ships in lockstep with `@rootnative/inertia` — version numbers track the core release that introduced or last touched the adapter.
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.0.0-alpha.0]
10
+
11
+ ### Added
12
+
13
+ - `MotionLinearGradient` over `expo-linear-gradient`. Accepts the same `initial` / `animate` / `transition` shape as the core `Motion.*` primitives, with animatable keys for `colors`, `start`, `end`, and `locations`.
14
+
15
+ [unreleased]: https://github.com/rootnative/inertia/compare/v0.0.0-alpha.0...HEAD
16
+ [0.0.0-alpha.0]: https://github.com/rootnative/inertia/releases/tag/v0.0.0-alpha.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RootNative
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,96 @@
1
+ # @rootnative/inertia-gradients
2
+
3
+ [![npm](https://img.shields.io/npm/v/@rootnative/inertia-gradients.svg)](https://www.npmjs.com/package/@rootnative/inertia-gradients)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
5
+
6
+ Animatable linear gradient primitive for [`@rootnative/inertia`](../core), built on [`expo-linear-gradient`](https://docs.expo.dev/versions/latest/sdk/linear-gradient/).
7
+
8
+ `MotionLinearGradient` accepts the same `initial` / `animate` / `transition` shape as the core `Motion.*` primitives, with animatable keys for `colors`, `start`, `end`, and `locations`.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ pnpm add @rootnative/inertia-gradients expo-linear-gradient
14
+ ```
15
+
16
+ `expo-linear-gradient` works in bare React Native projects as well as Expo — no `expo-modules-core` runtime is required.
17
+
18
+ **Peer dependencies:** `@rootnative/inertia` (workspace or installed), `react >=19.0.0`, `react-native >=0.81.0`, `react-native-reanimated >=4.0.0`, `expo-linear-gradient >=14.0.0`.
19
+
20
+ ## Usage
21
+
22
+ ```tsx
23
+ import { StyleSheet } from 'react-native'
24
+ import { MotionLinearGradient } from '@rootnative/inertia-gradients'
25
+
26
+ export function Hero() {
27
+ return (
28
+ <MotionLinearGradient
29
+ colors={['#0f172a', '#1e293b']}
30
+ animate={{ colors: ['#7c3aed', '#0ea5e9'] }}
31
+ transition={{ type: 'timing', duration: 600 }}
32
+ style={StyleSheet.absoluteFill}
33
+ />
34
+ )
35
+ }
36
+ ```
37
+
38
+ The static `colors` prop sets the visual on first render and **locks the slot count** for the component's lifetime. To resize the gradient (e.g. swap a 2-stop for a 3-stop), remount via `key={...}`. The component throws in dev if `colors.length` changes between renders.
39
+
40
+ ## Animatable props
41
+
42
+ | Key | Shape | Notes |
43
+ | ----------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
44
+ | `colors` | `readonly string[]` | Element-wise color interpolation via Reanimated's color setter. Length must match the static `colors` prop. |
45
+ | `start` | `{ x: number, y: number }` | Normalized `[0, 1]` coordinates. `x` and `y` animate independently. |
46
+ | `end` | `{ x: number, y: number }` | Same shape as `start`. |
47
+ | `locations` | `readonly number[]` | Optional stop positions. If supplied at mount, must remain supplied and same-length as `colors` for the component's lifetime. |
48
+
49
+ Per-property transitions work just like the core primitives:
50
+
51
+ ```tsx
52
+ <MotionLinearGradient
53
+ colors={['#000', '#000']}
54
+ animate={{
55
+ colors: ['#7c3aed', '#0ea5e9'],
56
+ start: { x: 0, y: 0 },
57
+ end: { x: 1, y: 1 },
58
+ }}
59
+ transition={{
60
+ colors: { type: 'timing', duration: 600 },
61
+ start: { type: 'spring', tension: 80, friction: 14 },
62
+ end: { type: 'spring', tension: 80, friction: 14 },
63
+ }}
64
+ />
65
+ ```
66
+
67
+ ## `initial`
68
+
69
+ Pass `initial` to override the mount-frame values (so the component starts somewhere other than the static props), or `initial={false}` to start at the `animate` target with no mount animation.
70
+
71
+ ```tsx
72
+ <MotionLinearGradient
73
+ colors={['#111', '#222']}
74
+ initial={{ colors: ['#000', '#000'] }} // fade up from black
75
+ animate={{ colors: ['#7c3aed', '#0ea5e9'] }}
76
+ />
77
+ ```
78
+
79
+ ## Reduced motion
80
+
81
+ `MotionLinearGradient` participates in `<MotionConfig reducedMotion>` the same way the core primitives do — when the OS reduce-motion setting is on, transitions resolve as direct assignment instead of `withSpring` / `withTiming`.
82
+
83
+ ## What this primitive doesn't do (v0.2)
84
+
85
+ - **Radial / conic gradients** — linear-only for v0.2.
86
+ - **Slot-count resize** — the colors array length is locked at mount.
87
+ - **Per-stop sequence keyframes** — `animate.colors` accepts a single target array, not nested arrays. For chained gradient transitions, drive the target through state.
88
+
89
+ ## Documentation
90
+
91
+ - Full docs: [https://rootnative.github.io/inertia/docs/gradients](https://rootnative.github.io/inertia/docs/gradients)
92
+ - Core library: [`@rootnative/inertia`](../core)
93
+
94
+ ## License
95
+
96
+ [MIT](./LICENSE) © RootNative
@@ -0,0 +1,107 @@
1
+ import * as react from 'react';
2
+ import { LinearGradientProps } from 'expo-linear-gradient';
3
+ import { TransitionConfig } from '@rootnative/inertia';
4
+
5
+ /**
6
+ * A 2D point on the gradient's `[0, 1]` square. `{ x: 0, y: 0 }` is the
7
+ * top-left corner; `{ x: 1, y: 1 }` is the bottom-right.
8
+ */
9
+ interface GradientPoint {
10
+ x: number;
11
+ y: number;
12
+ }
13
+ /**
14
+ * Animatable target snapshot for a linear gradient. Every field is optional —
15
+ * include only the dimensions you want to animate; the rest fall back to the
16
+ * static props on the component.
17
+ *
18
+ * `colors` and `locations` arrays must keep the same length as the static
19
+ * `colors` prop. Slot count is locked at first render so the shared-value
20
+ * table is stable across the animation's lifetime.
21
+ */
22
+ interface LinearGradientAnimate {
23
+ colors?: readonly string[];
24
+ start?: GradientPoint;
25
+ end?: GradientPoint;
26
+ locations?: readonly number[];
27
+ }
28
+ /**
29
+ * The four animatable dimensions of a linear gradient. Per-key transitions on
30
+ * `transition` are keyed against this shape.
31
+ */
32
+ interface LinearGradientStateShape {
33
+ colors: readonly string[];
34
+ start: GradientPoint;
35
+ end: GradientPoint;
36
+ locations: readonly number[];
37
+ }
38
+ /**
39
+ * Per-property transition map. Top-level entries on `transition` apply to all
40
+ * properties unless overridden by a per-key entry here.
41
+ */
42
+ type LinearGradientPerPropertyTransition = {
43
+ [K in keyof LinearGradientStateShape]?: TransitionConfig;
44
+ };
45
+ /**
46
+ * Transition shape accepted by `MotionLinearGradient`. Either a single
47
+ * top-level transition applied to every animated dimension, or a per-property
48
+ * map.
49
+ */
50
+ type LinearGradientTransition = TransitionConfig | LinearGradientPerPropertyTransition;
51
+
52
+ type AtLeastTwoStrings = readonly [string, string, ...string[]];
53
+ interface MotionLinearGradientProps extends Omit<LinearGradientProps, 'colors' | 'start' | 'end' | 'locations'> {
54
+ /**
55
+ * Initial color stops, in order. At least two are required. The array's
56
+ * length is **locked at first render** — to change the number of stops,
57
+ * remount with a new `key`.
58
+ */
59
+ colors: AtLeastTwoStrings;
60
+ /** Start point in normalized `[0, 1]` coordinates. Defaults to `{x:0,y:0}`. */
61
+ start?: GradientPoint;
62
+ /** End point in normalized `[0, 1]` coordinates. Defaults to `{x:1,y:0}`. */
63
+ end?: GradientPoint;
64
+ /**
65
+ * Optional stop positions. If supplied at mount, must remain supplied (and
66
+ * the same length as `colors`) for the lifetime of the component.
67
+ */
68
+ locations?: readonly number[];
69
+ /**
70
+ * Initial frame override. When present, the component mounts displaying
71
+ * these values, then animates to `animate` on the next effect. Pass `false`
72
+ * to skip the initial-mount animation entirely.
73
+ */
74
+ initial?: LinearGradientAnimate | false;
75
+ /** Target animation state. */
76
+ animate?: LinearGradientAnimate;
77
+ /**
78
+ * Transition config — either a single `TransitionConfig` applied to every
79
+ * animated dimension, or a per-property map (`{ colors, start, end,
80
+ * locations }`). Per-property entries win over the top-level transition.
81
+ */
82
+ transition?: LinearGradientTransition;
83
+ }
84
+ /**
85
+ * Animatable `LinearGradient`. Wraps `expo-linear-gradient`'s `LinearGradient`
86
+ * with declarative `initial` / `animate` / `transition` props.
87
+ *
88
+ * Animatable dimensions:
89
+ * - `colors` — array of color strings, element-wise interpolated. Slot count
90
+ * is locked at mount.
91
+ * - `start` / `end` — `{ x, y }` points; x and y animate independently.
92
+ * - `locations` — array of stop positions, element-wise interpolated. Locked
93
+ * to the same length as `colors` (and to its presence at mount).
94
+ *
95
+ * Example:
96
+ * ```tsx
97
+ * <MotionLinearGradient
98
+ * colors={['#0f172a', '#1e293b']}
99
+ * animate={{ colors: ['#7c3aed', '#0ea5e9'] }}
100
+ * transition={{ type: 'timing', duration: 600 }}
101
+ * style={StyleSheet.absoluteFill}
102
+ * />
103
+ * ```
104
+ */
105
+ declare function MotionLinearGradient(props: MotionLinearGradientProps): react.JSX.Element;
106
+
107
+ export { type GradientPoint, type LinearGradientAnimate, type LinearGradientPerPropertyTransition, type LinearGradientStateShape, type LinearGradientTransition, MotionLinearGradient, type MotionLinearGradientProps };
@@ -0,0 +1,107 @@
1
+ import * as react from 'react';
2
+ import { LinearGradientProps } from 'expo-linear-gradient';
3
+ import { TransitionConfig } from '@rootnative/inertia';
4
+
5
+ /**
6
+ * A 2D point on the gradient's `[0, 1]` square. `{ x: 0, y: 0 }` is the
7
+ * top-left corner; `{ x: 1, y: 1 }` is the bottom-right.
8
+ */
9
+ interface GradientPoint {
10
+ x: number;
11
+ y: number;
12
+ }
13
+ /**
14
+ * Animatable target snapshot for a linear gradient. Every field is optional —
15
+ * include only the dimensions you want to animate; the rest fall back to the
16
+ * static props on the component.
17
+ *
18
+ * `colors` and `locations` arrays must keep the same length as the static
19
+ * `colors` prop. Slot count is locked at first render so the shared-value
20
+ * table is stable across the animation's lifetime.
21
+ */
22
+ interface LinearGradientAnimate {
23
+ colors?: readonly string[];
24
+ start?: GradientPoint;
25
+ end?: GradientPoint;
26
+ locations?: readonly number[];
27
+ }
28
+ /**
29
+ * The four animatable dimensions of a linear gradient. Per-key transitions on
30
+ * `transition` are keyed against this shape.
31
+ */
32
+ interface LinearGradientStateShape {
33
+ colors: readonly string[];
34
+ start: GradientPoint;
35
+ end: GradientPoint;
36
+ locations: readonly number[];
37
+ }
38
+ /**
39
+ * Per-property transition map. Top-level entries on `transition` apply to all
40
+ * properties unless overridden by a per-key entry here.
41
+ */
42
+ type LinearGradientPerPropertyTransition = {
43
+ [K in keyof LinearGradientStateShape]?: TransitionConfig;
44
+ };
45
+ /**
46
+ * Transition shape accepted by `MotionLinearGradient`. Either a single
47
+ * top-level transition applied to every animated dimension, or a per-property
48
+ * map.
49
+ */
50
+ type LinearGradientTransition = TransitionConfig | LinearGradientPerPropertyTransition;
51
+
52
+ type AtLeastTwoStrings = readonly [string, string, ...string[]];
53
+ interface MotionLinearGradientProps extends Omit<LinearGradientProps, 'colors' | 'start' | 'end' | 'locations'> {
54
+ /**
55
+ * Initial color stops, in order. At least two are required. The array's
56
+ * length is **locked at first render** — to change the number of stops,
57
+ * remount with a new `key`.
58
+ */
59
+ colors: AtLeastTwoStrings;
60
+ /** Start point in normalized `[0, 1]` coordinates. Defaults to `{x:0,y:0}`. */
61
+ start?: GradientPoint;
62
+ /** End point in normalized `[0, 1]` coordinates. Defaults to `{x:1,y:0}`. */
63
+ end?: GradientPoint;
64
+ /**
65
+ * Optional stop positions. If supplied at mount, must remain supplied (and
66
+ * the same length as `colors`) for the lifetime of the component.
67
+ */
68
+ locations?: readonly number[];
69
+ /**
70
+ * Initial frame override. When present, the component mounts displaying
71
+ * these values, then animates to `animate` on the next effect. Pass `false`
72
+ * to skip the initial-mount animation entirely.
73
+ */
74
+ initial?: LinearGradientAnimate | false;
75
+ /** Target animation state. */
76
+ animate?: LinearGradientAnimate;
77
+ /**
78
+ * Transition config — either a single `TransitionConfig` applied to every
79
+ * animated dimension, or a per-property map (`{ colors, start, end,
80
+ * locations }`). Per-property entries win over the top-level transition.
81
+ */
82
+ transition?: LinearGradientTransition;
83
+ }
84
+ /**
85
+ * Animatable `LinearGradient`. Wraps `expo-linear-gradient`'s `LinearGradient`
86
+ * with declarative `initial` / `animate` / `transition` props.
87
+ *
88
+ * Animatable dimensions:
89
+ * - `colors` — array of color strings, element-wise interpolated. Slot count
90
+ * is locked at mount.
91
+ * - `start` / `end` — `{ x, y }` points; x and y animate independently.
92
+ * - `locations` — array of stop positions, element-wise interpolated. Locked
93
+ * to the same length as `colors` (and to its presence at mount).
94
+ *
95
+ * Example:
96
+ * ```tsx
97
+ * <MotionLinearGradient
98
+ * colors={['#0f172a', '#1e293b']}
99
+ * animate={{ colors: ['#7c3aed', '#0ea5e9'] }}
100
+ * transition={{ type: 'timing', duration: 600 }}
101
+ * style={StyleSheet.absoluteFill}
102
+ * />
103
+ * ```
104
+ */
105
+ declare function MotionLinearGradient(props: MotionLinearGradientProps): react.JSX.Element;
106
+
107
+ export { type GradientPoint, type LinearGradientAnimate, type LinearGradientPerPropertyTransition, type LinearGradientStateShape, type LinearGradientTransition, MotionLinearGradient, type MotionLinearGradientProps };
package/dist/index.js ADDED
@@ -0,0 +1,142 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var expoLinearGradient = require('expo-linear-gradient');
5
+ var Animated = require('react-native-reanimated');
6
+ var inertia = require('@rootnative/inertia');
7
+ var jsxRuntime = require('react/jsx-runtime');
8
+
9
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
+
11
+ var Animated__default = /*#__PURE__*/_interopDefault(Animated);
12
+
13
+ // src/MotionLinearGradient.tsx
14
+ var AnimatedLinearGradient = Animated__default.default.createAnimatedComponent(expoLinearGradient.LinearGradient);
15
+ var NO_ANIMATION = { type: "no-animation" };
16
+ var DEFAULT_START = { x: 0, y: 0 };
17
+ var DEFAULT_END = { x: 1, y: 0 };
18
+ function pickTransition(per, key) {
19
+ if (!per) return void 0;
20
+ if ("type" in per) return per;
21
+ return per[key];
22
+ }
23
+ function MotionLinearGradient(props) {
24
+ const {
25
+ colors,
26
+ start = DEFAULT_START,
27
+ end = DEFAULT_END,
28
+ locations,
29
+ initial,
30
+ animate,
31
+ transition,
32
+ ...rest
33
+ } = props;
34
+ const slotCountRef = react.useRef(colors.length);
35
+ const hasLocationsRef = react.useRef(locations !== void 0);
36
+ if (__DEV__) {
37
+ if (slotCountRef.current !== colors.length) {
38
+ throw new Error(
39
+ `[inertia-gradients] colors length changed from ${slotCountRef.current} to ${colors.length} \u2014 colors length is locked at mount; remount via key={...} to resize.`
40
+ );
41
+ }
42
+ if (hasLocationsRef.current !== (locations !== void 0)) {
43
+ throw new Error(
44
+ `[inertia-gradients] locations presence changed \u2014 locations must be either always present or always absent (locked at mount).`
45
+ );
46
+ }
47
+ if (locations !== void 0 && locations.length !== slotCountRef.current) {
48
+ throw new Error(
49
+ `[inertia-gradients] locations length (${locations.length}) must match colors length (${slotCountRef.current}).`
50
+ );
51
+ }
52
+ }
53
+ const seedSource = initial === false ? animate : initial ?? void 0;
54
+ const seedColors = seedSource?.colors ?? colors;
55
+ const seedStart = seedSource?.start ?? start;
56
+ const seedEnd = seedSource?.end ?? end;
57
+ const seedLocations = seedSource?.locations ?? locations;
58
+ const colorSvs = [];
59
+ for (let i = 0; i < slotCountRef.current; i++) {
60
+ const seed = seedColors[i] ?? colors[i] ?? "";
61
+ colorSvs.push(Animated.useSharedValue(seed));
62
+ }
63
+ const startX = Animated.useSharedValue(seedStart.x);
64
+ const startY = Animated.useSharedValue(seedStart.y);
65
+ const endX = Animated.useSharedValue(seedEnd.x);
66
+ const endY = Animated.useSharedValue(seedEnd.y);
67
+ const locationSvs = [];
68
+ for (let i = 0; i < slotCountRef.current; i++) {
69
+ locationSvs.push(Animated.useSharedValue(seedLocations?.[i] ?? 0));
70
+ }
71
+ const reduce = inertia.useShouldReduceMotion();
72
+ const colorsKey = animate?.colors ? animate.colors.join("|") : "";
73
+ const startKey = animate?.start ? `${animate.start.x},${animate.start.y}` : "";
74
+ const endKey = animate?.end ? `${animate.end.x},${animate.end.y}` : "";
75
+ const locationsKey = animate?.locations ? animate.locations.join("|") : "";
76
+ const animateColors = animate?.colors;
77
+ const animateStart = animate?.start;
78
+ const animateEnd = animate?.end;
79
+ const animateLocations = animate?.locations;
80
+ react.useEffect(() => {
81
+ if (!animateColors) return;
82
+ const cfg = reduce ? NO_ANIMATION : pickTransition(transition, "colors");
83
+ for (let i = 0; i < colorSvs.length; i++) {
84
+ const target = animateColors[i] ?? colors[i] ?? "";
85
+ colorSvs[i].value = inertia.resolveTransition(cfg, target);
86
+ }
87
+ }, [colorsKey, reduce, transition]);
88
+ react.useEffect(() => {
89
+ if (!animateStart) return;
90
+ const cfg = reduce ? NO_ANIMATION : pickTransition(transition, "start");
91
+ startX.value = inertia.resolveTransition(cfg, animateStart.x);
92
+ startY.value = inertia.resolveTransition(cfg, animateStart.y);
93
+ }, [startKey, reduce, transition]);
94
+ react.useEffect(() => {
95
+ if (!animateEnd) return;
96
+ const cfg = reduce ? NO_ANIMATION : pickTransition(transition, "end");
97
+ endX.value = inertia.resolveTransition(cfg, animateEnd.x);
98
+ endY.value = inertia.resolveTransition(cfg, animateEnd.y);
99
+ }, [endKey, reduce, transition]);
100
+ react.useEffect(() => {
101
+ if (!animateLocations) return;
102
+ const cfg = reduce ? NO_ANIMATION : pickTransition(transition, "locations");
103
+ for (let i = 0; i < locationSvs.length; i++) {
104
+ const target = animateLocations[i];
105
+ if (target !== void 0) {
106
+ locationSvs[i].value = inertia.resolveTransition(cfg, target);
107
+ }
108
+ }
109
+ }, [locationsKey, reduce, transition]);
110
+ const animatedProps = Animated.useAnimatedProps(() => {
111
+ "worklet";
112
+ const colorsOut = new Array(colorSvs.length);
113
+ for (let i = 0; i < colorSvs.length; i++) colorsOut[i] = colorSvs[i].value;
114
+ const out = {
115
+ colors: colorsOut,
116
+ start: { x: startX.value, y: startY.value },
117
+ end: { x: endX.value, y: endY.value }
118
+ };
119
+ if (hasLocationsRef.current) {
120
+ const locsOut = new Array(locationSvs.length);
121
+ for (let i = 0; i < locationSvs.length; i++)
122
+ locsOut[i] = locationSvs[i].value;
123
+ out.locations = locsOut;
124
+ }
125
+ return out;
126
+ });
127
+ return /* @__PURE__ */ jsxRuntime.jsx(
128
+ AnimatedLinearGradient,
129
+ {
130
+ animatedProps,
131
+ colors,
132
+ start,
133
+ end,
134
+ locations,
135
+ ...rest
136
+ }
137
+ );
138
+ }
139
+
140
+ exports.MotionLinearGradient = MotionLinearGradient;
141
+ //# sourceMappingURL=index.js.map
142
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/MotionLinearGradient.tsx"],"names":["Animated","LinearGradient","useRef","useSharedValue","useShouldReduceMotion","useEffect","resolveTransition","useAnimatedProps","jsx"],"mappings":"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","file":"index.js","sourcesContent":["import { useEffect, useRef } from 'react'\nimport { LinearGradient, type LinearGradientProps } from 'expo-linear-gradient'\nimport Animated, {\n useAnimatedProps,\n useSharedValue,\n type SharedValue,\n} from 'react-native-reanimated'\nimport {\n resolveTransition,\n useShouldReduceMotion,\n type TransitionConfig,\n} from '@rootnative/inertia'\nimport type {\n GradientPoint,\n LinearGradientAnimate,\n LinearGradientPerPropertyTransition,\n LinearGradientTransition,\n} from './types'\n\nconst AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient)\n\nconst NO_ANIMATION: TransitionConfig = { type: 'no-animation' }\nconst DEFAULT_START: GradientPoint = { x: 0, y: 0 }\nconst DEFAULT_END: GradientPoint = { x: 1, y: 0 }\n\n/**\n * Extract the per-key transition for `key`, falling back to the top-level\n * transition if the user passed `transition` as a single `TransitionConfig`\n * rather than a per-property map. Mirrors the precedence rule used by the\n * core factory: per-key wins, top-level fills, library default below that.\n */\nfunction pickTransition(\n per: LinearGradientTransition | undefined,\n key: keyof LinearGradientPerPropertyTransition,\n): TransitionConfig | undefined {\n if (!per) return undefined\n if ('type' in per) return per as TransitionConfig\n return (per as LinearGradientPerPropertyTransition)[key]\n}\n\ntype AtLeastTwoStrings = readonly [string, string, ...string[]]\n\nexport interface MotionLinearGradientProps extends Omit<\n LinearGradientProps,\n 'colors' | 'start' | 'end' | 'locations'\n> {\n /**\n * Initial color stops, in order. At least two are required. The array's\n * length is **locked at first render** — to change the number of stops,\n * remount with a new `key`.\n */\n colors: AtLeastTwoStrings\n /** Start point in normalized `[0, 1]` coordinates. Defaults to `{x:0,y:0}`. */\n start?: GradientPoint\n /** End point in normalized `[0, 1]` coordinates. Defaults to `{x:1,y:0}`. */\n end?: GradientPoint\n /**\n * Optional stop positions. If supplied at mount, must remain supplied (and\n * the same length as `colors`) for the lifetime of the component.\n */\n locations?: readonly number[]\n /**\n * Initial frame override. When present, the component mounts displaying\n * these values, then animates to `animate` on the next effect. Pass `false`\n * to skip the initial-mount animation entirely.\n */\n initial?: LinearGradientAnimate | false\n /** Target animation state. */\n animate?: LinearGradientAnimate\n /**\n * Transition config — either a single `TransitionConfig` applied to every\n * animated dimension, or a per-property map (`{ colors, start, end,\n * locations }`). Per-property entries win over the top-level transition.\n */\n transition?: LinearGradientTransition\n}\n\n/**\n * Animatable `LinearGradient`. Wraps `expo-linear-gradient`'s `LinearGradient`\n * with declarative `initial` / `animate` / `transition` props.\n *\n * Animatable dimensions:\n * - `colors` — array of color strings, element-wise interpolated. Slot count\n * is locked at mount.\n * - `start` / `end` — `{ x, y }` points; x and y animate independently.\n * - `locations` — array of stop positions, element-wise interpolated. Locked\n * to the same length as `colors` (and to its presence at mount).\n *\n * Example:\n * ```tsx\n * <MotionLinearGradient\n * colors={['#0f172a', '#1e293b']}\n * animate={{ colors: ['#7c3aed', '#0ea5e9'] }}\n * transition={{ type: 'timing', duration: 600 }}\n * style={StyleSheet.absoluteFill}\n * />\n * ```\n */\nexport function MotionLinearGradient(props: MotionLinearGradientProps) {\n const {\n colors,\n start = DEFAULT_START,\n end = DEFAULT_END,\n locations,\n initial,\n animate,\n transition,\n ...rest\n } = props\n\n // Slot count is locked at first render; subsequent renders must keep\n // `colors.length` constant so the hook order (one `useSharedValue` per slot\n // below) stays stable. `locations` presence is similarly locked because we\n // allocate its shared-value table on the same path.\n const slotCountRef = useRef(colors.length)\n const hasLocationsRef = useRef(locations !== undefined)\n\n if (__DEV__) {\n if (slotCountRef.current !== colors.length) {\n throw new Error(\n `[inertia-gradients] colors length changed from ${slotCountRef.current} to ${colors.length} — colors length is locked at mount; remount via key={...} to resize.`,\n )\n }\n if (hasLocationsRef.current !== (locations !== undefined)) {\n throw new Error(\n `[inertia-gradients] locations presence changed — locations must be either always present or always absent (locked at mount).`,\n )\n }\n if (locations !== undefined && locations.length !== slotCountRef.current) {\n throw new Error(\n `[inertia-gradients] locations length (${locations.length}) must match colors length (${slotCountRef.current}).`,\n )\n }\n }\n\n // `initial: false` means \"start at the animate target — no mount animation\".\n // `initial: {...}` overrides the static prop with explicit initial values.\n // `initial: undefined` (default) seeds from the static props.\n const seedSource = initial === false ? animate : (initial ?? undefined)\n const seedColors = seedSource?.colors ?? colors\n const seedStart = seedSource?.start ?? start\n const seedEnd = seedSource?.end ?? end\n const seedLocations = seedSource?.locations ?? locations\n\n // Loop-of-hooks pattern: safe because slotCountRef enforces a constant\n // length. ESLint can't see the invariant, so we suppress per call site.\n const colorSvs: SharedValue<string>[] = []\n for (let i = 0; i < slotCountRef.current; i++) {\n // Slot `i` is in-bounds for `colors` by the length lock, but TS sees a\n // generic `readonly string[]` index so coerce via a non-null fallback.\n const seed = seedColors[i] ?? colors[i] ?? ''\n // eslint-disable-next-line react-hooks/rules-of-hooks\n colorSvs.push(useSharedValue<string>(seed))\n }\n\n const startX = useSharedValue(seedStart.x)\n const startY = useSharedValue(seedStart.y)\n const endX = useSharedValue(seedEnd.x)\n const endY = useSharedValue(seedEnd.y)\n\n const locationSvs: SharedValue<number>[] = []\n for (let i = 0; i < slotCountRef.current; i++) {\n // eslint-disable-next-line react-hooks/rules-of-hooks\n locationSvs.push(useSharedValue<number>(seedLocations?.[i] ?? 0))\n }\n\n const reduce = useShouldReduceMotion()\n\n // Serialize targets into scalar keys so effects re-run on value change, not\n // on every parent re-render (a fresh `animate` literal each render is the\n // common case in callers).\n const colorsKey = animate?.colors ? animate.colors.join('|') : ''\n const startKey = animate?.start ? `${animate.start.x},${animate.start.y}` : ''\n const endKey = animate?.end ? `${animate.end.x},${animate.end.y}` : ''\n const locationsKey = animate?.locations ? animate.locations.join('|') : ''\n\n const animateColors = animate?.colors\n const animateStart = animate?.start\n const animateEnd = animate?.end\n const animateLocations = animate?.locations\n\n useEffect(() => {\n if (!animateColors) return\n const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'colors')\n for (let i = 0; i < colorSvs.length; i++) {\n const target = animateColors[i] ?? colors[i] ?? ''\n colorSvs[i]!.value = resolveTransition(cfg, target) as string\n }\n // colorSvs / colors are stable across renders by the length-lock above.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [colorsKey, reduce, transition])\n\n useEffect(() => {\n if (!animateStart) return\n const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'start')\n startX.value = resolveTransition(cfg, animateStart.x) as number\n startY.value = resolveTransition(cfg, animateStart.y) as number\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [startKey, reduce, transition])\n\n useEffect(() => {\n if (!animateEnd) return\n const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'end')\n endX.value = resolveTransition(cfg, animateEnd.x) as number\n endY.value = resolveTransition(cfg, animateEnd.y) as number\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [endKey, reduce, transition])\n\n useEffect(() => {\n if (!animateLocations) return\n const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'locations')\n for (let i = 0; i < locationSvs.length; i++) {\n const target = animateLocations[i]\n if (target !== undefined) {\n locationSvs[i]!.value = resolveTransition(cfg, target) as number\n }\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [locationsKey, reduce, transition])\n\n const animatedProps = useAnimatedProps(() => {\n 'worklet'\n const colorsOut = new Array<string>(colorSvs.length)\n for (let i = 0; i < colorSvs.length; i++) colorsOut[i] = colorSvs[i]!.value\n const out: {\n colors: string[]\n start: GradientPoint\n end: GradientPoint\n locations?: number[]\n } = {\n colors: colorsOut,\n start: { x: startX.value, y: startY.value },\n end: { x: endX.value, y: endY.value },\n }\n if (hasLocationsRef.current) {\n const locsOut = new Array<number>(locationSvs.length)\n for (let i = 0; i < locationSvs.length; i++)\n locsOut[i] = locationSvs[i]!.value\n out.locations = locsOut\n }\n return out\n })\n\n return (\n <AnimatedLinearGradient\n // `animatedProps` overrides the static `colors` / `start` / `end` /\n // `locations` each frame; the static props below are the first-render\n // seeds so the gradient renders before the first effect tick. The cast\n // sheds Reanimated's strict-tuple constraint that the worklet's return\n // type can't express — the runtime value is the same shape.\n animatedProps={animatedProps as never}\n colors={colors}\n start={start}\n end={end}\n locations={locations as never}\n {...rest}\n />\n )\n}\n\ndeclare const __DEV__: boolean\n"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,136 @@
1
+ import { useRef, useEffect } from 'react';
2
+ import { LinearGradient } from 'expo-linear-gradient';
3
+ import Animated, { useSharedValue, useAnimatedProps } from 'react-native-reanimated';
4
+ import { useShouldReduceMotion, resolveTransition } from '@rootnative/inertia';
5
+ import { jsx } from 'react/jsx-runtime';
6
+
7
+ // src/MotionLinearGradient.tsx
8
+ var AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient);
9
+ var NO_ANIMATION = { type: "no-animation" };
10
+ var DEFAULT_START = { x: 0, y: 0 };
11
+ var DEFAULT_END = { x: 1, y: 0 };
12
+ function pickTransition(per, key) {
13
+ if (!per) return void 0;
14
+ if ("type" in per) return per;
15
+ return per[key];
16
+ }
17
+ function MotionLinearGradient(props) {
18
+ const {
19
+ colors,
20
+ start = DEFAULT_START,
21
+ end = DEFAULT_END,
22
+ locations,
23
+ initial,
24
+ animate,
25
+ transition,
26
+ ...rest
27
+ } = props;
28
+ const slotCountRef = useRef(colors.length);
29
+ const hasLocationsRef = useRef(locations !== void 0);
30
+ if (__DEV__) {
31
+ if (slotCountRef.current !== colors.length) {
32
+ throw new Error(
33
+ `[inertia-gradients] colors length changed from ${slotCountRef.current} to ${colors.length} \u2014 colors length is locked at mount; remount via key={...} to resize.`
34
+ );
35
+ }
36
+ if (hasLocationsRef.current !== (locations !== void 0)) {
37
+ throw new Error(
38
+ `[inertia-gradients] locations presence changed \u2014 locations must be either always present or always absent (locked at mount).`
39
+ );
40
+ }
41
+ if (locations !== void 0 && locations.length !== slotCountRef.current) {
42
+ throw new Error(
43
+ `[inertia-gradients] locations length (${locations.length}) must match colors length (${slotCountRef.current}).`
44
+ );
45
+ }
46
+ }
47
+ const seedSource = initial === false ? animate : initial ?? void 0;
48
+ const seedColors = seedSource?.colors ?? colors;
49
+ const seedStart = seedSource?.start ?? start;
50
+ const seedEnd = seedSource?.end ?? end;
51
+ const seedLocations = seedSource?.locations ?? locations;
52
+ const colorSvs = [];
53
+ for (let i = 0; i < slotCountRef.current; i++) {
54
+ const seed = seedColors[i] ?? colors[i] ?? "";
55
+ colorSvs.push(useSharedValue(seed));
56
+ }
57
+ const startX = useSharedValue(seedStart.x);
58
+ const startY = useSharedValue(seedStart.y);
59
+ const endX = useSharedValue(seedEnd.x);
60
+ const endY = useSharedValue(seedEnd.y);
61
+ const locationSvs = [];
62
+ for (let i = 0; i < slotCountRef.current; i++) {
63
+ locationSvs.push(useSharedValue(seedLocations?.[i] ?? 0));
64
+ }
65
+ const reduce = useShouldReduceMotion();
66
+ const colorsKey = animate?.colors ? animate.colors.join("|") : "";
67
+ const startKey = animate?.start ? `${animate.start.x},${animate.start.y}` : "";
68
+ const endKey = animate?.end ? `${animate.end.x},${animate.end.y}` : "";
69
+ const locationsKey = animate?.locations ? animate.locations.join("|") : "";
70
+ const animateColors = animate?.colors;
71
+ const animateStart = animate?.start;
72
+ const animateEnd = animate?.end;
73
+ const animateLocations = animate?.locations;
74
+ useEffect(() => {
75
+ if (!animateColors) return;
76
+ const cfg = reduce ? NO_ANIMATION : pickTransition(transition, "colors");
77
+ for (let i = 0; i < colorSvs.length; i++) {
78
+ const target = animateColors[i] ?? colors[i] ?? "";
79
+ colorSvs[i].value = resolveTransition(cfg, target);
80
+ }
81
+ }, [colorsKey, reduce, transition]);
82
+ useEffect(() => {
83
+ if (!animateStart) return;
84
+ const cfg = reduce ? NO_ANIMATION : pickTransition(transition, "start");
85
+ startX.value = resolveTransition(cfg, animateStart.x);
86
+ startY.value = resolveTransition(cfg, animateStart.y);
87
+ }, [startKey, reduce, transition]);
88
+ useEffect(() => {
89
+ if (!animateEnd) return;
90
+ const cfg = reduce ? NO_ANIMATION : pickTransition(transition, "end");
91
+ endX.value = resolveTransition(cfg, animateEnd.x);
92
+ endY.value = resolveTransition(cfg, animateEnd.y);
93
+ }, [endKey, reduce, transition]);
94
+ useEffect(() => {
95
+ if (!animateLocations) return;
96
+ const cfg = reduce ? NO_ANIMATION : pickTransition(transition, "locations");
97
+ for (let i = 0; i < locationSvs.length; i++) {
98
+ const target = animateLocations[i];
99
+ if (target !== void 0) {
100
+ locationSvs[i].value = resolveTransition(cfg, target);
101
+ }
102
+ }
103
+ }, [locationsKey, reduce, transition]);
104
+ const animatedProps = useAnimatedProps(() => {
105
+ "worklet";
106
+ const colorsOut = new Array(colorSvs.length);
107
+ for (let i = 0; i < colorSvs.length; i++) colorsOut[i] = colorSvs[i].value;
108
+ const out = {
109
+ colors: colorsOut,
110
+ start: { x: startX.value, y: startY.value },
111
+ end: { x: endX.value, y: endY.value }
112
+ };
113
+ if (hasLocationsRef.current) {
114
+ const locsOut = new Array(locationSvs.length);
115
+ for (let i = 0; i < locationSvs.length; i++)
116
+ locsOut[i] = locationSvs[i].value;
117
+ out.locations = locsOut;
118
+ }
119
+ return out;
120
+ });
121
+ return /* @__PURE__ */ jsx(
122
+ AnimatedLinearGradient,
123
+ {
124
+ animatedProps,
125
+ colors,
126
+ start,
127
+ end,
128
+ locations,
129
+ ...rest
130
+ }
131
+ );
132
+ }
133
+
134
+ export { MotionLinearGradient };
135
+ //# sourceMappingURL=index.mjs.map
136
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/MotionLinearGradient.tsx"],"names":[],"mappings":";;;;;;;AAmBA,IAAM,sBAAA,GAAyB,QAAA,CAAS,uBAAA,CAAwB,cAAc,CAAA;AAE9E,IAAM,YAAA,GAAiC,EAAE,IAAA,EAAM,cAAA,EAAe;AAC9D,IAAM,aAAA,GAA+B,EAAE,CAAA,EAAG,CAAA,EAAG,GAAG,CAAA,EAAE;AAClD,IAAM,WAAA,GAA6B,EAAE,CAAA,EAAG,CAAA,EAAG,GAAG,CAAA,EAAE;AAQhD,SAAS,cAAA,CACP,KACA,GAAA,EAC8B;AAC9B,EAAA,IAAI,CAAC,KAAK,OAAO,MAAA;AACjB,EAAA,IAAI,MAAA,IAAU,KAAK,OAAO,GAAA;AAC1B,EAAA,OAAQ,IAA4C,GAAG,CAAA;AACzD;AA4DO,SAAS,qBAAqB,KAAA,EAAkC;AACrE,EAAA,MAAM;AAAA,IACJ,MAAA;AAAA,IACA,KAAA,GAAQ,aAAA;AAAA,IACR,GAAA,GAAM,WAAA;AAAA,IACN,SAAA;AAAA,IACA,OAAA;AAAA,IACA,OAAA;AAAA,IACA,UAAA;AAAA,IACA,GAAG;AAAA,GACL,GAAI,KAAA;AAMJ,EAAA,MAAM,YAAA,GAAe,MAAA,CAAO,MAAA,CAAO,MAAM,CAAA;AACzC,EAAA,MAAM,eAAA,GAAkB,MAAA,CAAO,SAAA,KAAc,MAAS,CAAA;AAEtD,EAAA,IAAI,OAAA,EAAS;AACX,IAAA,IAAI,YAAA,CAAa,OAAA,KAAY,MAAA,CAAO,MAAA,EAAQ;AAC1C,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,+CAAA,EAAkD,YAAA,CAAa,OAAO,CAAA,IAAA,EAAO,OAAO,MAAM,CAAA,0EAAA;AAAA,OAC5F;AAAA,IACF;AACA,IAAA,IAAI,eAAA,CAAgB,OAAA,MAAa,SAAA,KAAc,MAAA,CAAA,EAAY;AACzD,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,iIAAA;AAAA,OACF;AAAA,IACF;AACA,IAAA,IAAI,SAAA,KAAc,MAAA,IAAa,SAAA,CAAU,MAAA,KAAW,aAAa,OAAA,EAAS;AACxE,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,sCAAA,EAAyC,SAAA,CAAU,MAAM,CAAA,4BAAA,EAA+B,aAAa,OAAO,CAAA,EAAA;AAAA,OAC9G;AAAA,IACF;AAAA,EACF;AAKA,EAAA,MAAM,UAAA,GAAa,OAAA,KAAY,KAAA,GAAQ,OAAA,GAAW,OAAA,IAAW,MAAA;AAC7D,EAAA,MAAM,UAAA,GAAa,YAAY,MAAA,IAAU,MAAA;AACzC,EAAA,MAAM,SAAA,GAAY,YAAY,KAAA,IAAS,KAAA;AACvC,EAAA,MAAM,OAAA,GAAU,YAAY,GAAA,IAAO,GAAA;AACnC,EAAA,MAAM,aAAA,GAAgB,YAAY,SAAA,IAAa,SAAA;AAI/C,EAAA,MAAM,WAAkC,EAAC;AACzC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,YAAA,CAAa,SAAS,CAAA,EAAA,EAAK;AAG7C,IAAA,MAAM,OAAO,UAAA,CAAW,CAAC,CAAA,IAAK,MAAA,CAAO,CAAC,CAAA,IAAK,EAAA;AAE3C,IAAA,QAAA,CAAS,IAAA,CAAK,cAAA,CAAuB,IAAI,CAAC,CAAA;AAAA,EAC5C;AAEA,EAAA,MAAM,MAAA,GAAS,cAAA,CAAe,SAAA,CAAU,CAAC,CAAA;AACzC,EAAA,MAAM,MAAA,GAAS,cAAA,CAAe,SAAA,CAAU,CAAC,CAAA;AACzC,EAAA,MAAM,IAAA,GAAO,cAAA,CAAe,OAAA,CAAQ,CAAC,CAAA;AACrC,EAAA,MAAM,IAAA,GAAO,cAAA,CAAe,OAAA,CAAQ,CAAC,CAAA;AAErC,EAAA,MAAM,cAAqC,EAAC;AAC5C,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,YAAA,CAAa,SAAS,CAAA,EAAA,EAAK;AAE7C,IAAA,WAAA,CAAY,KAAK,cAAA,CAAuB,aAAA,GAAgB,CAAC,CAAA,IAAK,CAAC,CAAC,CAAA;AAAA,EAClE;AAEA,EAAA,MAAM,SAAS,qBAAA,EAAsB;AAKrC,EAAA,MAAM,YAAY,OAAA,EAAS,MAAA,GAAS,QAAQ,MAAA,CAAO,IAAA,CAAK,GAAG,CAAA,GAAI,EAAA;AAC/D,EAAA,MAAM,QAAA,GAAW,OAAA,EAAS,KAAA,GAAQ,CAAA,EAAG,OAAA,CAAQ,KAAA,CAAM,CAAC,CAAA,CAAA,EAAI,OAAA,CAAQ,KAAA,CAAM,CAAC,CAAA,CAAA,GAAK,EAAA;AAC5E,EAAA,MAAM,MAAA,GAAS,OAAA,EAAS,GAAA,GAAM,CAAA,EAAG,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,CAAA,EAAI,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,CAAA,GAAK,EAAA;AACpE,EAAA,MAAM,eAAe,OAAA,EAAS,SAAA,GAAY,QAAQ,SAAA,CAAU,IAAA,CAAK,GAAG,CAAA,GAAI,EAAA;AAExE,EAAA,MAAM,gBAAgB,OAAA,EAAS,MAAA;AAC/B,EAAA,MAAM,eAAe,OAAA,EAAS,KAAA;AAC9B,EAAA,MAAM,aAAa,OAAA,EAAS,GAAA;AAC5B,EAAA,MAAM,mBAAmB,OAAA,EAAS,SAAA;AAElC,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,aAAA,EAAe;AACpB,IAAA,MAAM,GAAA,GAAM,MAAA,GAAS,YAAA,GAAe,cAAA,CAAe,YAAY,QAAQ,CAAA;AACvE,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,QAAA,CAAS,QAAQ,CAAA,EAAA,EAAK;AACxC,MAAA,MAAM,SAAS,aAAA,CAAc,CAAC,CAAA,IAAK,MAAA,CAAO,CAAC,CAAA,IAAK,EAAA;AAChD,MAAA,QAAA,CAAS,CAAC,CAAA,CAAG,KAAA,GAAQ,iBAAA,CAAkB,KAAK,MAAM,CAAA;AAAA,IACpD;AAAA,EAGF,CAAA,EAAG,CAAC,SAAA,EAAW,MAAA,EAAQ,UAAU,CAAC,CAAA;AAElC,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,YAAA,EAAc;AACnB,IAAA,MAAM,GAAA,GAAM,MAAA,GAAS,YAAA,GAAe,cAAA,CAAe,YAAY,OAAO,CAAA;AACtE,IAAA,MAAA,CAAO,KAAA,GAAQ,iBAAA,CAAkB,GAAA,EAAK,YAAA,CAAa,CAAC,CAAA;AACpD,IAAA,MAAA,CAAO,KAAA,GAAQ,iBAAA,CAAkB,GAAA,EAAK,YAAA,CAAa,CAAC,CAAA;AAAA,EAEtD,CAAA,EAAG,CAAC,QAAA,EAAU,MAAA,EAAQ,UAAU,CAAC,CAAA;AAEjC,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,UAAA,EAAY;AACjB,IAAA,MAAM,GAAA,GAAM,MAAA,GAAS,YAAA,GAAe,cAAA,CAAe,YAAY,KAAK,CAAA;AACpE,IAAA,IAAA,CAAK,KAAA,GAAQ,iBAAA,CAAkB,GAAA,EAAK,UAAA,CAAW,CAAC,CAAA;AAChD,IAAA,IAAA,CAAK,KAAA,GAAQ,iBAAA,CAAkB,GAAA,EAAK,UAAA,CAAW,CAAC,CAAA;AAAA,EAElD,CAAA,EAAG,CAAC,MAAA,EAAQ,MAAA,EAAQ,UAAU,CAAC,CAAA;AAE/B,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,gBAAA,EAAkB;AACvB,IAAA,MAAM,GAAA,GAAM,MAAA,GAAS,YAAA,GAAe,cAAA,CAAe,YAAY,WAAW,CAAA;AAC1E,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,WAAA,CAAY,QAAQ,CAAA,EAAA,EAAK;AAC3C,MAAA,MAAM,MAAA,GAAS,iBAAiB,CAAC,CAAA;AACjC,MAAA,IAAI,WAAW,MAAA,EAAW;AACxB,QAAA,WAAA,CAAY,CAAC,CAAA,CAAG,KAAA,GAAQ,iBAAA,CAAkB,KAAK,MAAM,CAAA;AAAA,MACvD;AAAA,IACF;AAAA,EAEF,CAAA,EAAG,CAAC,YAAA,EAAc,MAAA,EAAQ,UAAU,CAAC,CAAA;AAErC,EAAA,MAAM,aAAA,GAAgB,iBAAiB,MAAM;AAC3C,IAAA,SAAA;AACA,IAAA,MAAM,SAAA,GAAY,IAAI,KAAA,CAAc,QAAA,CAAS,MAAM,CAAA;AACnD,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,QAAA,CAAS,MAAA,EAAQ,CAAA,EAAA,EAAK,SAAA,CAAU,CAAC,CAAA,GAAI,QAAA,CAAS,CAAC,CAAA,CAAG,KAAA;AACtE,IAAA,MAAM,GAAA,GAKF;AAAA,MACF,MAAA,EAAQ,SAAA;AAAA,MACR,OAAO,EAAE,CAAA,EAAG,OAAO,KAAA,EAAO,CAAA,EAAG,OAAO,KAAA,EAAM;AAAA,MAC1C,KAAK,EAAE,CAAA,EAAG,KAAK,KAAA,EAAO,CAAA,EAAG,KAAK,KAAA;AAAM,KACtC;AACA,IAAA,IAAI,gBAAgB,OAAA,EAAS;AAC3B,MAAA,MAAM,OAAA,GAAU,IAAI,KAAA,CAAc,WAAA,CAAY,MAAM,CAAA;AACpD,MAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,WAAA,CAAY,MAAA,EAAQ,CAAA,EAAA;AACtC,QAAA,OAAA,CAAQ,CAAC,CAAA,GAAI,WAAA,CAAY,CAAC,CAAA,CAAG,KAAA;AAC/B,MAAA,GAAA,CAAI,SAAA,GAAY,OAAA;AAAA,IAClB;AACA,IAAA,OAAO,GAAA;AAAA,EACT,CAAC,CAAA;AAED,EAAA,uBACE,GAAA;AAAA,IAAC,sBAAA;AAAA,IAAA;AAAA,MAMC,aAAA;AAAA,MACA,MAAA;AAAA,MACA,KAAA;AAAA,MACA,GAAA;AAAA,MACA,SAAA;AAAA,MACC,GAAG;AAAA;AAAA,GACN;AAEJ","file":"index.mjs","sourcesContent":["import { useEffect, useRef } from 'react'\nimport { LinearGradient, type LinearGradientProps } from 'expo-linear-gradient'\nimport Animated, {\n useAnimatedProps,\n useSharedValue,\n type SharedValue,\n} from 'react-native-reanimated'\nimport {\n resolveTransition,\n useShouldReduceMotion,\n type TransitionConfig,\n} from '@rootnative/inertia'\nimport type {\n GradientPoint,\n LinearGradientAnimate,\n LinearGradientPerPropertyTransition,\n LinearGradientTransition,\n} from './types'\n\nconst AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient)\n\nconst NO_ANIMATION: TransitionConfig = { type: 'no-animation' }\nconst DEFAULT_START: GradientPoint = { x: 0, y: 0 }\nconst DEFAULT_END: GradientPoint = { x: 1, y: 0 }\n\n/**\n * Extract the per-key transition for `key`, falling back to the top-level\n * transition if the user passed `transition` as a single `TransitionConfig`\n * rather than a per-property map. Mirrors the precedence rule used by the\n * core factory: per-key wins, top-level fills, library default below that.\n */\nfunction pickTransition(\n per: LinearGradientTransition | undefined,\n key: keyof LinearGradientPerPropertyTransition,\n): TransitionConfig | undefined {\n if (!per) return undefined\n if ('type' in per) return per as TransitionConfig\n return (per as LinearGradientPerPropertyTransition)[key]\n}\n\ntype AtLeastTwoStrings = readonly [string, string, ...string[]]\n\nexport interface MotionLinearGradientProps extends Omit<\n LinearGradientProps,\n 'colors' | 'start' | 'end' | 'locations'\n> {\n /**\n * Initial color stops, in order. At least two are required. The array's\n * length is **locked at first render** — to change the number of stops,\n * remount with a new `key`.\n */\n colors: AtLeastTwoStrings\n /** Start point in normalized `[0, 1]` coordinates. Defaults to `{x:0,y:0}`. */\n start?: GradientPoint\n /** End point in normalized `[0, 1]` coordinates. Defaults to `{x:1,y:0}`. */\n end?: GradientPoint\n /**\n * Optional stop positions. If supplied at mount, must remain supplied (and\n * the same length as `colors`) for the lifetime of the component.\n */\n locations?: readonly number[]\n /**\n * Initial frame override. When present, the component mounts displaying\n * these values, then animates to `animate` on the next effect. Pass `false`\n * to skip the initial-mount animation entirely.\n */\n initial?: LinearGradientAnimate | false\n /** Target animation state. */\n animate?: LinearGradientAnimate\n /**\n * Transition config — either a single `TransitionConfig` applied to every\n * animated dimension, or a per-property map (`{ colors, start, end,\n * locations }`). Per-property entries win over the top-level transition.\n */\n transition?: LinearGradientTransition\n}\n\n/**\n * Animatable `LinearGradient`. Wraps `expo-linear-gradient`'s `LinearGradient`\n * with declarative `initial` / `animate` / `transition` props.\n *\n * Animatable dimensions:\n * - `colors` — array of color strings, element-wise interpolated. Slot count\n * is locked at mount.\n * - `start` / `end` — `{ x, y }` points; x and y animate independently.\n * - `locations` — array of stop positions, element-wise interpolated. Locked\n * to the same length as `colors` (and to its presence at mount).\n *\n * Example:\n * ```tsx\n * <MotionLinearGradient\n * colors={['#0f172a', '#1e293b']}\n * animate={{ colors: ['#7c3aed', '#0ea5e9'] }}\n * transition={{ type: 'timing', duration: 600 }}\n * style={StyleSheet.absoluteFill}\n * />\n * ```\n */\nexport function MotionLinearGradient(props: MotionLinearGradientProps) {\n const {\n colors,\n start = DEFAULT_START,\n end = DEFAULT_END,\n locations,\n initial,\n animate,\n transition,\n ...rest\n } = props\n\n // Slot count is locked at first render; subsequent renders must keep\n // `colors.length` constant so the hook order (one `useSharedValue` per slot\n // below) stays stable. `locations` presence is similarly locked because we\n // allocate its shared-value table on the same path.\n const slotCountRef = useRef(colors.length)\n const hasLocationsRef = useRef(locations !== undefined)\n\n if (__DEV__) {\n if (slotCountRef.current !== colors.length) {\n throw new Error(\n `[inertia-gradients] colors length changed from ${slotCountRef.current} to ${colors.length} — colors length is locked at mount; remount via key={...} to resize.`,\n )\n }\n if (hasLocationsRef.current !== (locations !== undefined)) {\n throw new Error(\n `[inertia-gradients] locations presence changed — locations must be either always present or always absent (locked at mount).`,\n )\n }\n if (locations !== undefined && locations.length !== slotCountRef.current) {\n throw new Error(\n `[inertia-gradients] locations length (${locations.length}) must match colors length (${slotCountRef.current}).`,\n )\n }\n }\n\n // `initial: false` means \"start at the animate target — no mount animation\".\n // `initial: {...}` overrides the static prop with explicit initial values.\n // `initial: undefined` (default) seeds from the static props.\n const seedSource = initial === false ? animate : (initial ?? undefined)\n const seedColors = seedSource?.colors ?? colors\n const seedStart = seedSource?.start ?? start\n const seedEnd = seedSource?.end ?? end\n const seedLocations = seedSource?.locations ?? locations\n\n // Loop-of-hooks pattern: safe because slotCountRef enforces a constant\n // length. ESLint can't see the invariant, so we suppress per call site.\n const colorSvs: SharedValue<string>[] = []\n for (let i = 0; i < slotCountRef.current; i++) {\n // Slot `i` is in-bounds for `colors` by the length lock, but TS sees a\n // generic `readonly string[]` index so coerce via a non-null fallback.\n const seed = seedColors[i] ?? colors[i] ?? ''\n // eslint-disable-next-line react-hooks/rules-of-hooks\n colorSvs.push(useSharedValue<string>(seed))\n }\n\n const startX = useSharedValue(seedStart.x)\n const startY = useSharedValue(seedStart.y)\n const endX = useSharedValue(seedEnd.x)\n const endY = useSharedValue(seedEnd.y)\n\n const locationSvs: SharedValue<number>[] = []\n for (let i = 0; i < slotCountRef.current; i++) {\n // eslint-disable-next-line react-hooks/rules-of-hooks\n locationSvs.push(useSharedValue<number>(seedLocations?.[i] ?? 0))\n }\n\n const reduce = useShouldReduceMotion()\n\n // Serialize targets into scalar keys so effects re-run on value change, not\n // on every parent re-render (a fresh `animate` literal each render is the\n // common case in callers).\n const colorsKey = animate?.colors ? animate.colors.join('|') : ''\n const startKey = animate?.start ? `${animate.start.x},${animate.start.y}` : ''\n const endKey = animate?.end ? `${animate.end.x},${animate.end.y}` : ''\n const locationsKey = animate?.locations ? animate.locations.join('|') : ''\n\n const animateColors = animate?.colors\n const animateStart = animate?.start\n const animateEnd = animate?.end\n const animateLocations = animate?.locations\n\n useEffect(() => {\n if (!animateColors) return\n const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'colors')\n for (let i = 0; i < colorSvs.length; i++) {\n const target = animateColors[i] ?? colors[i] ?? ''\n colorSvs[i]!.value = resolveTransition(cfg, target) as string\n }\n // colorSvs / colors are stable across renders by the length-lock above.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [colorsKey, reduce, transition])\n\n useEffect(() => {\n if (!animateStart) return\n const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'start')\n startX.value = resolveTransition(cfg, animateStart.x) as number\n startY.value = resolveTransition(cfg, animateStart.y) as number\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [startKey, reduce, transition])\n\n useEffect(() => {\n if (!animateEnd) return\n const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'end')\n endX.value = resolveTransition(cfg, animateEnd.x) as number\n endY.value = resolveTransition(cfg, animateEnd.y) as number\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [endKey, reduce, transition])\n\n useEffect(() => {\n if (!animateLocations) return\n const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'locations')\n for (let i = 0; i < locationSvs.length; i++) {\n const target = animateLocations[i]\n if (target !== undefined) {\n locationSvs[i]!.value = resolveTransition(cfg, target) as number\n }\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [locationsKey, reduce, transition])\n\n const animatedProps = useAnimatedProps(() => {\n 'worklet'\n const colorsOut = new Array<string>(colorSvs.length)\n for (let i = 0; i < colorSvs.length; i++) colorsOut[i] = colorSvs[i]!.value\n const out: {\n colors: string[]\n start: GradientPoint\n end: GradientPoint\n locations?: number[]\n } = {\n colors: colorsOut,\n start: { x: startX.value, y: startY.value },\n end: { x: endX.value, y: endY.value },\n }\n if (hasLocationsRef.current) {\n const locsOut = new Array<number>(locationSvs.length)\n for (let i = 0; i < locationSvs.length; i++)\n locsOut[i] = locationSvs[i]!.value\n out.locations = locsOut\n }\n return out\n })\n\n return (\n <AnimatedLinearGradient\n // `animatedProps` overrides the static `colors` / `start` / `end` /\n // `locations` each frame; the static props below are the first-render\n // seeds so the gradient renders before the first effect tick. The cast\n // sheds Reanimated's strict-tuple constraint that the worklet's return\n // type can't express — the runtime value is the same shape.\n animatedProps={animatedProps as never}\n colors={colors}\n start={start}\n end={end}\n locations={locations as never}\n {...rest}\n />\n )\n}\n\ndeclare const __DEV__: boolean\n"]}
package/llms.txt ADDED
@@ -0,0 +1,95 @@
1
+ > @rootnative/inertia-gradients — adapter package for @rootnative/inertia.
2
+ > This file is generated from the matching docs page by scripts/build-llms.mjs — do not edit by hand.
3
+
4
+ > Full docs: https://rootnative.github.io/inertia/
5
+ > Core overview: see @rootnative/inertia/llms.txt (or docs/static/llms.txt in the repo)
6
+ > Source: https://github.com/rootnative/inertia
7
+
8
+ ---
9
+ # Gradients
10
+
11
+ `@rootnative/inertia-gradients` adds an animatable linear gradient built on `expo-linear-gradient`. It is an **optional** sibling package — install it only when you need to animate gradient stops. The core library has no required `expo-linear-gradient` dependency.
12
+
13
+ `MotionLinearGradient` accepts the same `initial` / `animate` / `transition` shape as the core `Motion.*` primitives, with animatable keys for `colors`, `start`, `end`, and `locations`.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ yarn add @rootnative/inertia-gradients expo-linear-gradient
19
+ ```
20
+
21
+ `expo-linear-gradient` works in bare React Native projects as well as Expo, but it is an Expo Module — bare projects need the Expo modules runtime installed (see [Installing Expo modules](https://docs.expo.dev/bare/installing-expo-modules/)).
22
+
23
+ ## Usage
24
+
25
+ ```tsx
26
+ import { MotionLinearGradient } from '@rootnative/inertia-gradients'
27
+
28
+ function Hero() {
29
+ return (
30
+ <MotionLinearGradient
31
+ colors={['#0f172a', '#1e293b']}
32
+ animate={{ colors: ['#7c3aed', '#0ea5e9'] }}
33
+ transition={{ type: 'timing', duration: 600 }}
34
+ style={StyleSheet.absoluteFill}
35
+ />
36
+ )
37
+ }
38
+ ```
39
+
40
+ The static `colors` prop is required — it sets the visual on first render and **locks the slot count** for the lifetime of the component. To resize the gradient (e.g. swap a 2-stop for a 3-stop), remount via `key={...}`. The component throws in dev if `colors.length` changes between renders.
41
+
42
+ ## Animatable props
43
+
44
+ | Key | Shape | Notes |
45
+ | ----------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
46
+ | `colors` | `readonly string[]` | Element-wise color interpolation via Reanimated's color setter. Length must match the static `colors` prop. |
47
+ | `start` | `{ x: number, y: number }` | Normalized `[0, 1]` coordinates. `x` and `y` animate independently — useful for rotating gradient direction. |
48
+ | `end` | `{ x: number, y: number }` | Same shape as `start`. |
49
+ | `locations` | `readonly number[]` | Optional stop positions. If supplied at mount, must remain supplied and same-length as `colors` for the component's lifetime. |
50
+
51
+ Per-property transitions work just like the core primitives:
52
+
53
+ ```tsx
54
+ <MotionLinearGradient
55
+ colors={['#000', '#000']}
56
+ animate={{
57
+ colors: ['#7c3aed', '#0ea5e9'],
58
+ start: { x: 0, y: 0 },
59
+ end: { x: 1, y: 1 },
60
+ }}
61
+ transition={{
62
+ colors: { type: 'timing', duration: 600 },
63
+ start: { type: 'spring', tension: 80, friction: 14 },
64
+ end: { type: 'spring', tension: 80, friction: 14 },
65
+ }}
66
+ />
67
+ ```
68
+
69
+ ## `initial`
70
+
71
+ Pass `initial` to override the mount-frame values (so the component starts somewhere other than the static props), or `initial={false}` to start at the `animate` target with no mount animation.
72
+
73
+ ```tsx
74
+ <MotionLinearGradient
75
+ colors={['#111', '#222']}
76
+ initial={{ colors: ['#000', '#000'] }} // fade up from black
77
+ animate={{ colors: ['#7c3aed', '#0ea5e9'] }}
78
+ />
79
+
80
+ <MotionLinearGradient
81
+ colors={['#111', '#222']}
82
+ initial={false} // skip the mount animation
83
+ animate={{ colors: ['#7c3aed', '#0ea5e9'] }}
84
+ />
85
+ ```
86
+
87
+ ## Reduced motion
88
+
89
+ `MotionLinearGradient` participates in [`<MotionConfig reducedMotion>`](./motion-config.md) the same way the core primitives do — when the OS reduce-motion setting is on (or you pass `reducedMotion="always"`), transitions resolve as direct assignment instead of `withSpring` / `withTiming`.
90
+
91
+ ## What this primitive doesn't do (v0.2)
92
+
93
+ - **Radial / conic gradients** — linear-only for v0.2. Radial lands in v0.3 once the linear API is validated.
94
+ - **Slot-count resize** — the colors array length is locked at mount. To change it, remount via `key={...}`.
95
+ - **Per-stop sequence keyframes** — `animate.colors` accepts a single target array, not a nested array of arrays. For chained gradient transitions, drive the target through state and let React re-render.
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "@rootnative/inertia-gradients",
3
+ "version": "0.0.0-alpha.0",
4
+ "description": "Animated linear gradient primitive for @rootnative/inertia, built on expo-linear-gradient.",
5
+ "license": "MIT",
6
+ "author": "RootNative",
7
+ "homepage": "https://github.com/rootnative/inertia",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/rootnative/inertia.git",
11
+ "directory": "packages/gradients"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/rootnative/inertia/issues"
15
+ },
16
+ "keywords": [
17
+ "react-native",
18
+ "reanimated",
19
+ "gradient",
20
+ "linear-gradient",
21
+ "animation",
22
+ "inertia",
23
+ "expo"
24
+ ],
25
+ "sideEffects": false,
26
+ "main": "./dist/index.js",
27
+ "module": "./dist/index.mjs",
28
+ "types": "./dist/index.d.ts",
29
+ "react-native": "./src/index.ts",
30
+ "source": "./src/index.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "react-native": "./src/index.ts",
35
+ "source": "./src/index.ts",
36
+ "import": "./dist/index.mjs",
37
+ "require": "./dist/index.js"
38
+ },
39
+ "./package.json": "./package.json"
40
+ },
41
+ "files": [
42
+ "dist",
43
+ "src",
44
+ "llms.txt",
45
+ "README.md",
46
+ "LICENSE",
47
+ "CHANGELOG.md",
48
+ "!**/__tests__",
49
+ "!**/*.test.*"
50
+ ],
51
+ "peerDependencies": {
52
+ "@rootnative/inertia": ">=0.0.0-alpha.0",
53
+ "expo-linear-gradient": ">=14.0.0",
54
+ "react": ">=19.0.0",
55
+ "react-native": ">=0.81.0",
56
+ "react-native-reanimated": ">=4.0.0"
57
+ },
58
+ "devDependencies": {
59
+ "@react-native/babel-preset": "^0.81.5",
60
+ "@size-limit/preset-small-lib": "^11.1.0",
61
+ "@testing-library/react-native": "^13.3.3",
62
+ "@types/jest": "^29.5.14",
63
+ "@types/react": "^19.1.0",
64
+ "expo-linear-gradient": "~14.0.2",
65
+ "jest": "^29.7.0",
66
+ "react": "19.1.0",
67
+ "react-native": "0.81.5",
68
+ "react-native-reanimated": "~4.1.1",
69
+ "react-test-renderer": "19.1.0",
70
+ "size-limit": "^11.1.0",
71
+ "tsup": "^8.3.5",
72
+ "typescript": "^5.7.3",
73
+ "@rootnative/inertia": "0.0.0-alpha.0"
74
+ },
75
+ "publishConfig": {
76
+ "access": "public"
77
+ },
78
+ "scripts": {
79
+ "build": "tsup",
80
+ "dev": "tsup --watch",
81
+ "typecheck": "tsc --noEmit",
82
+ "test": "jest",
83
+ "size": "size-limit",
84
+ "size:why": "size-limit --why",
85
+ "lint": "eslint .",
86
+ "clean": "rm -rf dist .turbo *.tsbuildinfo"
87
+ }
88
+ }
@@ -0,0 +1,261 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import { LinearGradient, type LinearGradientProps } from 'expo-linear-gradient'
3
+ import Animated, {
4
+ useAnimatedProps,
5
+ useSharedValue,
6
+ type SharedValue,
7
+ } from 'react-native-reanimated'
8
+ import {
9
+ resolveTransition,
10
+ useShouldReduceMotion,
11
+ type TransitionConfig,
12
+ } from '@rootnative/inertia'
13
+ import type {
14
+ GradientPoint,
15
+ LinearGradientAnimate,
16
+ LinearGradientPerPropertyTransition,
17
+ LinearGradientTransition,
18
+ } from './types'
19
+
20
+ const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient)
21
+
22
+ const NO_ANIMATION: TransitionConfig = { type: 'no-animation' }
23
+ const DEFAULT_START: GradientPoint = { x: 0, y: 0 }
24
+ const DEFAULT_END: GradientPoint = { x: 1, y: 0 }
25
+
26
+ /**
27
+ * Extract the per-key transition for `key`, falling back to the top-level
28
+ * transition if the user passed `transition` as a single `TransitionConfig`
29
+ * rather than a per-property map. Mirrors the precedence rule used by the
30
+ * core factory: per-key wins, top-level fills, library default below that.
31
+ */
32
+ function pickTransition(
33
+ per: LinearGradientTransition | undefined,
34
+ key: keyof LinearGradientPerPropertyTransition,
35
+ ): TransitionConfig | undefined {
36
+ if (!per) return undefined
37
+ if ('type' in per) return per as TransitionConfig
38
+ return (per as LinearGradientPerPropertyTransition)[key]
39
+ }
40
+
41
+ type AtLeastTwoStrings = readonly [string, string, ...string[]]
42
+
43
+ export interface MotionLinearGradientProps extends Omit<
44
+ LinearGradientProps,
45
+ 'colors' | 'start' | 'end' | 'locations'
46
+ > {
47
+ /**
48
+ * Initial color stops, in order. At least two are required. The array's
49
+ * length is **locked at first render** — to change the number of stops,
50
+ * remount with a new `key`.
51
+ */
52
+ colors: AtLeastTwoStrings
53
+ /** Start point in normalized `[0, 1]` coordinates. Defaults to `{x:0,y:0}`. */
54
+ start?: GradientPoint
55
+ /** End point in normalized `[0, 1]` coordinates. Defaults to `{x:1,y:0}`. */
56
+ end?: GradientPoint
57
+ /**
58
+ * Optional stop positions. If supplied at mount, must remain supplied (and
59
+ * the same length as `colors`) for the lifetime of the component.
60
+ */
61
+ locations?: readonly number[]
62
+ /**
63
+ * Initial frame override. When present, the component mounts displaying
64
+ * these values, then animates to `animate` on the next effect. Pass `false`
65
+ * to skip the initial-mount animation entirely.
66
+ */
67
+ initial?: LinearGradientAnimate | false
68
+ /** Target animation state. */
69
+ animate?: LinearGradientAnimate
70
+ /**
71
+ * Transition config — either a single `TransitionConfig` applied to every
72
+ * animated dimension, or a per-property map (`{ colors, start, end,
73
+ * locations }`). Per-property entries win over the top-level transition.
74
+ */
75
+ transition?: LinearGradientTransition
76
+ }
77
+
78
+ /**
79
+ * Animatable `LinearGradient`. Wraps `expo-linear-gradient`'s `LinearGradient`
80
+ * with declarative `initial` / `animate` / `transition` props.
81
+ *
82
+ * Animatable dimensions:
83
+ * - `colors` — array of color strings, element-wise interpolated. Slot count
84
+ * is locked at mount.
85
+ * - `start` / `end` — `{ x, y }` points; x and y animate independently.
86
+ * - `locations` — array of stop positions, element-wise interpolated. Locked
87
+ * to the same length as `colors` (and to its presence at mount).
88
+ *
89
+ * Example:
90
+ * ```tsx
91
+ * <MotionLinearGradient
92
+ * colors={['#0f172a', '#1e293b']}
93
+ * animate={{ colors: ['#7c3aed', '#0ea5e9'] }}
94
+ * transition={{ type: 'timing', duration: 600 }}
95
+ * style={StyleSheet.absoluteFill}
96
+ * />
97
+ * ```
98
+ */
99
+ export function MotionLinearGradient(props: MotionLinearGradientProps) {
100
+ const {
101
+ colors,
102
+ start = DEFAULT_START,
103
+ end = DEFAULT_END,
104
+ locations,
105
+ initial,
106
+ animate,
107
+ transition,
108
+ ...rest
109
+ } = props
110
+
111
+ // Slot count is locked at first render; subsequent renders must keep
112
+ // `colors.length` constant so the hook order (one `useSharedValue` per slot
113
+ // below) stays stable. `locations` presence is similarly locked because we
114
+ // allocate its shared-value table on the same path.
115
+ const slotCountRef = useRef(colors.length)
116
+ const hasLocationsRef = useRef(locations !== undefined)
117
+
118
+ if (__DEV__) {
119
+ if (slotCountRef.current !== colors.length) {
120
+ throw new Error(
121
+ `[inertia-gradients] colors length changed from ${slotCountRef.current} to ${colors.length} — colors length is locked at mount; remount via key={...} to resize.`,
122
+ )
123
+ }
124
+ if (hasLocationsRef.current !== (locations !== undefined)) {
125
+ throw new Error(
126
+ `[inertia-gradients] locations presence changed — locations must be either always present or always absent (locked at mount).`,
127
+ )
128
+ }
129
+ if (locations !== undefined && locations.length !== slotCountRef.current) {
130
+ throw new Error(
131
+ `[inertia-gradients] locations length (${locations.length}) must match colors length (${slotCountRef.current}).`,
132
+ )
133
+ }
134
+ }
135
+
136
+ // `initial: false` means "start at the animate target — no mount animation".
137
+ // `initial: {...}` overrides the static prop with explicit initial values.
138
+ // `initial: undefined` (default) seeds from the static props.
139
+ const seedSource = initial === false ? animate : (initial ?? undefined)
140
+ const seedColors = seedSource?.colors ?? colors
141
+ const seedStart = seedSource?.start ?? start
142
+ const seedEnd = seedSource?.end ?? end
143
+ const seedLocations = seedSource?.locations ?? locations
144
+
145
+ // Loop-of-hooks pattern: safe because slotCountRef enforces a constant
146
+ // length. ESLint can't see the invariant, so we suppress per call site.
147
+ const colorSvs: SharedValue<string>[] = []
148
+ for (let i = 0; i < slotCountRef.current; i++) {
149
+ // Slot `i` is in-bounds for `colors` by the length lock, but TS sees a
150
+ // generic `readonly string[]` index so coerce via a non-null fallback.
151
+ const seed = seedColors[i] ?? colors[i] ?? ''
152
+ // eslint-disable-next-line react-hooks/rules-of-hooks
153
+ colorSvs.push(useSharedValue<string>(seed))
154
+ }
155
+
156
+ const startX = useSharedValue(seedStart.x)
157
+ const startY = useSharedValue(seedStart.y)
158
+ const endX = useSharedValue(seedEnd.x)
159
+ const endY = useSharedValue(seedEnd.y)
160
+
161
+ const locationSvs: SharedValue<number>[] = []
162
+ for (let i = 0; i < slotCountRef.current; i++) {
163
+ // eslint-disable-next-line react-hooks/rules-of-hooks
164
+ locationSvs.push(useSharedValue<number>(seedLocations?.[i] ?? 0))
165
+ }
166
+
167
+ const reduce = useShouldReduceMotion()
168
+
169
+ // Serialize targets into scalar keys so effects re-run on value change, not
170
+ // on every parent re-render (a fresh `animate` literal each render is the
171
+ // common case in callers).
172
+ const colorsKey = animate?.colors ? animate.colors.join('|') : ''
173
+ const startKey = animate?.start ? `${animate.start.x},${animate.start.y}` : ''
174
+ const endKey = animate?.end ? `${animate.end.x},${animate.end.y}` : ''
175
+ const locationsKey = animate?.locations ? animate.locations.join('|') : ''
176
+
177
+ const animateColors = animate?.colors
178
+ const animateStart = animate?.start
179
+ const animateEnd = animate?.end
180
+ const animateLocations = animate?.locations
181
+
182
+ useEffect(() => {
183
+ if (!animateColors) return
184
+ const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'colors')
185
+ for (let i = 0; i < colorSvs.length; i++) {
186
+ const target = animateColors[i] ?? colors[i] ?? ''
187
+ colorSvs[i]!.value = resolveTransition(cfg, target) as string
188
+ }
189
+ // colorSvs / colors are stable across renders by the length-lock above.
190
+ // eslint-disable-next-line react-hooks/exhaustive-deps
191
+ }, [colorsKey, reduce, transition])
192
+
193
+ useEffect(() => {
194
+ if (!animateStart) return
195
+ const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'start')
196
+ startX.value = resolveTransition(cfg, animateStart.x) as number
197
+ startY.value = resolveTransition(cfg, animateStart.y) as number
198
+ // eslint-disable-next-line react-hooks/exhaustive-deps
199
+ }, [startKey, reduce, transition])
200
+
201
+ useEffect(() => {
202
+ if (!animateEnd) return
203
+ const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'end')
204
+ endX.value = resolveTransition(cfg, animateEnd.x) as number
205
+ endY.value = resolveTransition(cfg, animateEnd.y) as number
206
+ // eslint-disable-next-line react-hooks/exhaustive-deps
207
+ }, [endKey, reduce, transition])
208
+
209
+ useEffect(() => {
210
+ if (!animateLocations) return
211
+ const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'locations')
212
+ for (let i = 0; i < locationSvs.length; i++) {
213
+ const target = animateLocations[i]
214
+ if (target !== undefined) {
215
+ locationSvs[i]!.value = resolveTransition(cfg, target) as number
216
+ }
217
+ }
218
+ // eslint-disable-next-line react-hooks/exhaustive-deps
219
+ }, [locationsKey, reduce, transition])
220
+
221
+ const animatedProps = useAnimatedProps(() => {
222
+ 'worklet'
223
+ const colorsOut = new Array<string>(colorSvs.length)
224
+ for (let i = 0; i < colorSvs.length; i++) colorsOut[i] = colorSvs[i]!.value
225
+ const out: {
226
+ colors: string[]
227
+ start: GradientPoint
228
+ end: GradientPoint
229
+ locations?: number[]
230
+ } = {
231
+ colors: colorsOut,
232
+ start: { x: startX.value, y: startY.value },
233
+ end: { x: endX.value, y: endY.value },
234
+ }
235
+ if (hasLocationsRef.current) {
236
+ const locsOut = new Array<number>(locationSvs.length)
237
+ for (let i = 0; i < locationSvs.length; i++)
238
+ locsOut[i] = locationSvs[i]!.value
239
+ out.locations = locsOut
240
+ }
241
+ return out
242
+ })
243
+
244
+ return (
245
+ <AnimatedLinearGradient
246
+ // `animatedProps` overrides the static `colors` / `start` / `end` /
247
+ // `locations` each frame; the static props below are the first-render
248
+ // seeds so the gradient renders before the first effect tick. The cast
249
+ // sheds Reanimated's strict-tuple constraint that the worklet's return
250
+ // type can't express — the runtime value is the same shape.
251
+ animatedProps={animatedProps as never}
252
+ colors={colors}
253
+ start={start}
254
+ end={end}
255
+ locations={locations as never}
256
+ {...rest}
257
+ />
258
+ )
259
+ }
260
+
261
+ declare const __DEV__: boolean
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `@rootnative/inertia-gradients` — animated gradient primitives for
3
+ * `@rootnative/inertia`.
4
+ *
5
+ * v0.2 surface:
6
+ * - `MotionLinearGradient` — animatable linear gradient over
7
+ * `expo-linear-gradient`. Animates `colors`, `start`, `end`, and
8
+ * `locations` with the same `initial` / `animate` / `transition` shape
9
+ * as the core Motion primitives.
10
+ *
11
+ * Radial / conic gradients land in v0.3 once the linear API is validated.
12
+ */
13
+ export { MotionLinearGradient } from './MotionLinearGradient'
14
+ export type { MotionLinearGradientProps } from './MotionLinearGradient'
15
+ export type {
16
+ GradientPoint,
17
+ LinearGradientAnimate,
18
+ LinearGradientPerPropertyTransition,
19
+ LinearGradientStateShape,
20
+ LinearGradientTransition,
21
+ } from './types'
package/src/types.ts ADDED
@@ -0,0 +1,54 @@
1
+ import type { TransitionConfig } from '@rootnative/inertia'
2
+
3
+ /**
4
+ * A 2D point on the gradient's `[0, 1]` square. `{ x: 0, y: 0 }` is the
5
+ * top-left corner; `{ x: 1, y: 1 }` is the bottom-right.
6
+ */
7
+ export interface GradientPoint {
8
+ x: number
9
+ y: number
10
+ }
11
+
12
+ /**
13
+ * Animatable target snapshot for a linear gradient. Every field is optional —
14
+ * include only the dimensions you want to animate; the rest fall back to the
15
+ * static props on the component.
16
+ *
17
+ * `colors` and `locations` arrays must keep the same length as the static
18
+ * `colors` prop. Slot count is locked at first render so the shared-value
19
+ * table is stable across the animation's lifetime.
20
+ */
21
+ export interface LinearGradientAnimate {
22
+ colors?: readonly string[]
23
+ start?: GradientPoint
24
+ end?: GradientPoint
25
+ locations?: readonly number[]
26
+ }
27
+
28
+ /**
29
+ * The four animatable dimensions of a linear gradient. Per-key transitions on
30
+ * `transition` are keyed against this shape.
31
+ */
32
+ export interface LinearGradientStateShape {
33
+ colors: readonly string[]
34
+ start: GradientPoint
35
+ end: GradientPoint
36
+ locations: readonly number[]
37
+ }
38
+
39
+ /**
40
+ * Per-property transition map. Top-level entries on `transition` apply to all
41
+ * properties unless overridden by a per-key entry here.
42
+ */
43
+ export type LinearGradientPerPropertyTransition = {
44
+ [K in keyof LinearGradientStateShape]?: TransitionConfig
45
+ }
46
+
47
+ /**
48
+ * Transition shape accepted by `MotionLinearGradient`. Either a single
49
+ * top-level transition applied to every animated dimension, or a per-property
50
+ * map.
51
+ */
52
+ export type LinearGradientTransition =
53
+ | TransitionConfig
54
+ | LinearGradientPerPropertyTransition