morphicons 1.4.1 → 1.4.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/README.md CHANGED
@@ -65,7 +65,7 @@ ref.current?.morphTo(Check); // animates
65
65
  ref.current?.set(X); // jumps without animating
66
66
  ```
67
67
 
68
- Drop-in replacement for lucide-react: `size`, `strokeWidth`, `absoluteStrokeWidth`, `color`, `className` and the rest of the `<svg>` props pass straight through. Correct accessibility by default: `aria-hidden` unless you pass `label` (→ `role="img"` + `<title>`). Clean SSR: the server emits the exact static SVG (zero flash, zero layout shift); the runtime is born on hydration. `prefers-reduced-motion` degrades to an instant swap.
68
+ Drop-in replacement for lucide-react: `size`, `strokeWidth`, `absoluteStrokeWidth`, `color`, `className` and the rest of the `<svg>` props pass straight through. Correct accessibility by default: `aria-hidden` unless you pass `label` (→ `role="img"` + `<title>`). Clean SSR: the server emits the exact static SVG (zero flash, zero layout shift); the runtime is born on hydration. Morphs play regardless of the OS reduce-motion setting by default; opt into honoring it with `reducedMotion="user"` (see [Reduced motion](#reduced-motion-all-four-bindings)).
69
69
 
70
70
  ### Vue — same three modes
71
71
 
@@ -139,7 +139,7 @@ ref.current?.morphTo(Check); // animates
139
139
  ref.current?.set(X); // jumps without animating
140
140
  ```
141
141
 
142
- The DOM-free core beyond the browser: the dom driver is reused verbatim as the engine (React Native has a global `requestAnimationFrame`, and `PathEl` is structural), so the whole platform difference is a shim that forwards the per-frame `d` write to `Path.setNativeProps` of react-native-svg — outside the React render, exactly like the web mutation. Same surface as the React binding (`size`, `strokeWidth`, `absoluteStrokeWidth`, `color`, plus the native `Svg` props: `testID`, touch handlers…), same accessibility defaults (`aria-hidden` unless you pass `label` → `role="img"` + `aria-label`). Reduced motion comes from `AccessibilityInfo` (best-effort: the query is async; a `reduceMotionChanged` subscription keeps it exact from then on). Requires Metro with package `exports` resolution — default since React Native 0.79; on older versions enable `unstable_enablePackageExports`.
142
+ The DOM-free core beyond the browser: the dom driver is reused verbatim as the engine (React Native has a global `requestAnimationFrame`, and `PathEl` is structural), so the whole platform difference is a shim that forwards the per-frame `d` write to `Path.setNativeProps` of react-native-svg — outside the React render, exactly like the web mutation. Same surface as the React binding (`size`, `strokeWidth`, `absoluteStrokeWidth`, `color`, plus the native `Svg` props: `testID`, touch handlers…), same accessibility defaults (`aria-hidden` unless you pass `label` → `role="img"` + `aria-label`). With `reducedMotion="user"`, the OS setting comes from `AccessibilityInfo` (best-effort: the query is async; a `reduceMotionChanged` subscription keeps it exact from then on). Requires Metro with package `exports` resolution — default since React Native 0.79; on older versions enable `unstable_enablePackageExports`.
143
143
 
144
144
  ### Lifecycle contract (all four bindings)
145
145
 
@@ -149,6 +149,20 @@ The four components share one contract, pinned by mirrored client-mount tests:
149
149
  - **Controlled wins.** While `from` AND `to` are both present, the pair owns the path: `icon` changes are ignored, no spring fires. Drop the pair and the current `icon` takes over (animated). Mixing the modes is not an error — the precedence is just explicit.
150
150
  - **Clean re-entry.** Any exit from controlled mode (an imperative call or an icon takeover) invalidates the frozen pair, so returning to the same `from`/`to` re-bases on `from` and renders exactly like a clean mount at that `progress`.
151
151
 
152
+ ### Reduced motion (all four bindings)
153
+
154
+ Icon morphs are small, short, communicative micro-transitions: the kind of motion the reduce-motion guidance considers generally acceptable, unlike parallax or full-screen movement. Auto-degrading them made the library look broken to every user with the OS setting on, so since 1.4.2 they play by default and the policy is an explicit prop (the same default Motion, formerly Framer Motion, ships):
155
+
156
+ ```tsx
157
+ <MorphIcon icon={open ? X : Menu} reducedMotion="user" />
158
+ ```
159
+
160
+ - `"never"` (default): morphs always animate; the OS setting is ignored.
161
+ - `"user"`: while the OS reduce-motion setting is on, every `morphTo` degrades to an instant swap (the pre-1.4.2 behavior). The web bindings read `prefers-reduced-motion`; React Native reads `AccessibilityInfo`, and only touches the accessibility bridge when some instance opts in.
162
+ - `"always"`: every `morphTo` jumps without animating (tests, screenshots).
163
+
164
+ The prop is live: changing it governs the next morph. The vanilla driver takes the same policy as an option, `createMorph(el, icon, { reducedMotion: "user" })`, and exposes it as a mutable `morph.reducedMotion`. Apps that must strictly follow the user's system preference should pass `reducedMotion="user"` across the board.
165
+
152
166
  ### Vanilla (no React)
153
167
 
154
168
  ```ts
@@ -159,6 +173,7 @@ m.morphTo(X, "snappy"); // preset or { stiffness, damping }
159
173
  m.set(Check); // jump without animating (canonical d)
160
174
  m.seek(X, 0.4); // morph frozen at t — scrubbing, gestures
161
175
  m.progress = 0.4; // sugar: seek on the active target
176
+ m.reducedMotion = "user"; // live policy: "never" (default) | "user" | "always"
162
177
  m.destroy();
163
178
  ```
164
179
 
@@ -25,6 +25,7 @@ let {
25
25
  to,
26
26
  progress,
27
27
  spring,
28
+ reducedMotion,
28
29
  size = 24,
29
30
  color = "currentColor",
30
31
  strokeWidth = 2,
@@ -40,17 +41,17 @@ const initialD = computeInitialD({ icon, from, to, progress });
40
41
  let pathEl: SVGPathElement | undefined = $state();
41
42
  // svelte-ignore state_referenced_locally — init-time values on purpose: they
42
43
  // seed the controller's watch baselines so the first $effect run is a no-op.
43
- const ctrl = createController({ icon, from, to, progress });
44
+ const ctrl = createController({ icon, from, to, progress, reducedMotion });
44
45
 
45
46
  onMount(() => {
46
- if (pathEl) ctrl.mount(pathEl, { icon, from, to, progress });
47
+ if (pathEl) ctrl.mount(pathEl, { icon, from, to, progress, reducedMotion });
47
48
  return () => ctrl.destroy();
48
49
  });
49
50
 
50
51
  // Prop changes: one watcher owns the mode logic (controlled wins — change
51
52
  // detection and precedence live in shared.ts).
52
53
  $effect(() => {
53
- const next = { icon, from, to, progress, spring };
54
+ const next = { icon, from, to, progress, spring, reducedMotion };
54
55
  untrack(() => ctrl.watch(next));
55
56
  });
56
57
 
package/dist/dom.d.ts CHANGED
@@ -10,6 +10,15 @@ interface MorphOptions {
10
10
  stiffness?: number;
11
11
  damping?: number;
12
12
  }
13
+ /** Reduced-motion policy. `"never"` (default): morphs always animate,
14
+ * ignoring the OS setting. `"user"`: while the OS reduce-motion setting is
15
+ * on, `morphTo` degrades to an instant `set`. `"always"`: every `morphTo`
16
+ * jumps (tests, screenshots). */
17
+ type ReducedMotionMode = "never" | "user" | "always";
18
+ interface CreateMorphOptions {
19
+ /** Reduced-motion policy; also live via the `reducedMotion` property. */
20
+ reducedMotion?: ReducedMotionMode;
21
+ }
13
22
  interface Morph {
14
23
  /** Animates toward the icon with spring physics. Interruptible: mid-flight
15
24
  * it re-plans from the intermediate shape while preserving velocity. */
@@ -23,6 +32,8 @@ interface Morph {
23
32
  /** Current progress (t of the last frame; 1 at rest). Assigning it is
24
33
  * equivalent to seek(target, t) on the active target. */
25
34
  progress: number;
35
+ /** Reduced-motion policy, live: assigning it applies to the next morphTo. */
36
+ reducedMotion: ReducedMotionMode;
26
37
  /** Unregisters the instance; later calls are no-ops. */
27
38
  destroy(): void;
28
39
  }
@@ -32,6 +43,6 @@ interface Morph {
32
43
  * is what a binding renders at SSR/rest before any runtime exists. */
33
44
  declare function canonicalD(icon: IconInput): string;
34
45
  /** Creates the morph instance over a `<path>` and paints the initial icon. */
35
- declare function createMorph(el: PathEl, icon: IconInput): Morph;
46
+ declare function createMorph(el: PathEl, icon: IconInput, options?: CreateMorphOptions): Morph;
36
47
  //#endregion
37
- export { Morph, MorphOptions, PathEl, canonicalD, createMorph };
48
+ export { CreateMorphOptions, Morph, MorphOptions, PathEl, ReducedMotionMode, canonicalD, createMorph };
package/dist/dom.js CHANGED
@@ -76,8 +76,9 @@ function resolveSpring(s) {
76
76
  };
77
77
  }
78
78
  /** Creates the morph instance over a `<path>` and paints the initial icon. */
79
- function createMorph(el, icon) {
79
+ function createMorph(el, icon, options) {
80
80
  const spring = new Spring();
81
+ let reducedMotion = options?.reducedMotion ?? "never";
81
82
  let target = icon;
82
83
  let rest = true;
83
84
  let plan = null;
@@ -142,6 +143,13 @@ function createMorph(el, icon) {
142
143
  target = icon;
143
144
  settle();
144
145
  };
146
+ /** True when the policy says this morphTo must jump instead of flying. */
147
+ const motionOff = () => {
148
+ if (reducedMotion === "always") return true;
149
+ if (reducedMotion !== "user") return false;
150
+ if (typeof matchMedia === "undefined") return false;
151
+ return matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
152
+ };
145
153
  const seek = (icon, tt) => {
146
154
  if (dead) return;
147
155
  const reuse = !rest && plan !== null && icon === target;
@@ -154,11 +162,9 @@ function createMorph(el, icon) {
154
162
  morphTo(icon, sp) {
155
163
  if (dead) return;
156
164
  if (icon === target && (rest || flying)) return;
157
- if (typeof matchMedia !== "undefined") {
158
- if (matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
159
- setNow(icon);
160
- return;
161
- }
165
+ if (motionOff()) {
166
+ setNow(icon);
167
+ return;
162
168
  }
163
169
  const { k, c } = resolveSpring(sp);
164
170
  spring.config(k, c);
@@ -180,6 +186,12 @@ function createMorph(el, icon) {
180
186
  set progress(v) {
181
187
  if (!dead) seek(target, v);
182
188
  },
189
+ get reducedMotion() {
190
+ return reducedMotion;
191
+ },
192
+ set reducedMotion(v) {
193
+ reducedMotion = v;
194
+ },
183
195
  destroy() {
184
196
  stop();
185
197
  dead = true;
@@ -1,5 +1,5 @@
1
1
  import { a as Sampled, c as SpringPreset, n as IconInput, r as IconNode } from "./types-JAF3s76Y.js";
2
- import { Morph, MorphOptions, PathEl } from "./dom.js";
2
+ import { Morph, MorphOptions, PathEl, ReducedMotionMode } from "./dom.js";
3
3
  import { SvgProps } from "react-native-svg";
4
4
  //#region src/react-native/index.d.ts
5
5
  /** Imperative surface exposed via ref. */
@@ -18,6 +18,11 @@ interface MorphIconProps extends Omit<SvgProps, "from" | "to"> {
18
18
  progress?: number;
19
19
  /** Physics for uncontrolled/imperative mode: preset or custom spring. */
20
20
  spring?: SpringPreset | MorphOptions;
21
+ /** Reduced-motion policy: "never" (default) animates regardless of the OS
22
+ * setting, "user" honors AccessibilityInfo's reduce motion (morphs degrade
23
+ * to an instant swap while it is on; best-effort, the query is async),
24
+ * "always" always jumps. */
25
+ reducedMotion?: ReducedMotionMode;
21
26
  size?: number | string;
22
27
  color?: string;
23
28
  strokeWidth?: number | string;
@@ -28,4 +33,4 @@ interface MorphIconProps extends Omit<SvgProps, "from" | "to"> {
28
33
  }
29
34
  declare const MorphIcon: import("react").ForwardRefExoticComponent<MorphIconProps & import("react").RefAttributes<MorphHandle>>;
30
35
  //#endregion
31
- export { type IconInput, type IconNode, type Morph, MorphHandle, MorphIcon, MorphIconProps, type MorphOptions, type PathEl, type Sampled, type SpringPreset };
36
+ export { type IconInput, type IconNode, type Morph, MorphHandle, MorphIcon, MorphIconProps, type MorphOptions, type PathEl, type ReducedMotionMode, type Sampled, type SpringPreset };
@@ -31,7 +31,7 @@ function frozenD(from, to, t) {
31
31
  return serialize(out, plan.items.map((it) => it.closed));
32
32
  }
33
33
  const MorphIcon = forwardRef(function MorphIcon(props, ref) {
34
- const { icon, from, to, progress, spring, size = 24, color = "currentColor", strokeWidth = 2, absoluteStrokeWidth, label, ...rest } = props;
34
+ const { icon, from, to, progress, spring, reducedMotion, size = 24, color = "currentColor", strokeWidth = 2, absoluteStrokeWidth, label, ...rest } = props;
35
35
  const controlled = from !== void 0 && to !== void 0;
36
36
  const initialIcon = icon ?? from ?? to;
37
37
  const [initialD] = useState(() => {
@@ -42,6 +42,8 @@ const MorphIcon = forwardRef(function MorphIcon(props, ref) {
42
42
  const morphRef = useRef(null);
43
43
  const springRef = useRef(spring);
44
44
  springRef.current = spring;
45
+ const rmRef = useRef(reducedMotion);
46
+ rmRef.current = reducedMotion;
45
47
  const prevIcon = useRef(icon);
46
48
  const prevControlled = useRef(controlled);
47
49
  const dead = useRef(false);
@@ -52,9 +54,12 @@ const MorphIcon = forwardRef(function MorphIcon(props, ref) {
52
54
  const el = useRef({ setAttribute: (name, value) => {
53
55
  if (name === "d") pathRef.current?.setNativeProps({ d: value });
54
56
  } });
55
- /** morphTo that honors the OS reduce-motion setting (best-effort). */
57
+ /** morphTo that applies the reduced-motion policy ("user" reads the OS
58
+ * setting via AccessibilityInfo, best-effort). */
56
59
  const fly = (m, i, s) => {
57
- if (reduced) m.set(i);
60
+ const mode = rmRef.current ?? "never";
61
+ if (mode === "user") initReducedMotion();
62
+ if (mode === "always" || mode === "user" && reduced) m.set(i);
58
63
  else m.morphTo(i, s);
59
64
  };
60
65
  const flyRef = useRef(fly);
@@ -71,7 +76,6 @@ const MorphIcon = forwardRef(function MorphIcon(props, ref) {
71
76
  }, []);
72
77
  useIsoLayoutEffect(() => {
73
78
  dead.current = false;
74
- initReducedMotion();
75
79
  if (pathRef.current && initialIcon !== void 0) {
76
80
  const m = createMorph(el.current, controlled ? from : initialIcon);
77
81
  morphRef.current = m;
@@ -94,6 +98,9 @@ const MorphIcon = forwardRef(function MorphIcon(props, ref) {
94
98
  pair.current = null;
95
99
  };
96
100
  }, []);
101
+ useEffect(() => {
102
+ if (reducedMotion === "user") initReducedMotion();
103
+ }, [reducedMotion]);
97
104
  useEffect(() => {
98
105
  const left = prevControlled.current && !controlled;
99
106
  prevControlled.current = controlled;
package/dist/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as Sampled, c as SpringPreset, n as IconInput, r as IconNode } from "./types-JAF3s76Y.js";
2
- import { Morph, MorphOptions, PathEl } from "./dom.js";
2
+ import { Morph, MorphOptions, PathEl, ReducedMotionMode } from "./dom.js";
3
3
  import { SVGProps } from "react";
4
4
  //#region src/react/index.d.ts
5
5
  /** Imperative surface exposed via ref. */
@@ -18,6 +18,10 @@ interface MorphIconProps extends Omit<SVGProps<SVGSVGElement>, "from" | "to" | "
18
18
  progress?: number;
19
19
  /** Physics for uncontrolled/imperative mode: preset or custom spring. */
20
20
  spring?: SpringPreset | MorphOptions;
21
+ /** Reduced-motion policy: "never" (default) animates regardless of the OS
22
+ * setting, "user" honors prefers-reduced-motion (morphs degrade to an
23
+ * instant swap while it is on), "always" always jumps. */
24
+ reducedMotion?: ReducedMotionMode;
21
25
  size?: number | string;
22
26
  color?: string;
23
27
  strokeWidth?: number | string;
@@ -28,4 +32,4 @@ interface MorphIconProps extends Omit<SVGProps<SVGSVGElement>, "from" | "to" | "
28
32
  }
29
33
  declare const MorphIcon: import("react").ForwardRefExoticComponent<MorphIconProps & import("react").RefAttributes<MorphHandle>>;
30
34
  //#endregion
31
- export { type IconInput, type IconNode, type Morph, MorphHandle, MorphIcon, MorphIconProps, type MorphOptions, type PathEl, type Sampled, type SpringPreset };
35
+ export { type IconInput, type IconNode, type Morph, MorphHandle, MorphIcon, MorphIconProps, type MorphOptions, type PathEl, type ReducedMotionMode, type Sampled, type SpringPreset };
package/dist/react.js CHANGED
@@ -15,7 +15,7 @@ function frozenD(from, to, t) {
15
15
  return serialize(out, plan.items.map((it) => it.closed));
16
16
  }
17
17
  const MorphIcon = forwardRef(function MorphIcon(props, ref) {
18
- const { icon, from, to, progress, spring, size = 24, color = "currentColor", strokeWidth = 2, absoluteStrokeWidth, label, ...rest } = props;
18
+ const { icon, from, to, progress, spring, reducedMotion, size = 24, color = "currentColor", strokeWidth = 2, absoluteStrokeWidth, label, ...rest } = props;
19
19
  const controlled = from !== void 0 && to !== void 0;
20
20
  const initialIcon = icon ?? from ?? to;
21
21
  const [initialD] = useState(() => {
@@ -26,6 +26,8 @@ const MorphIcon = forwardRef(function MorphIcon(props, ref) {
26
26
  const morphRef = useRef(null);
27
27
  const springRef = useRef(spring);
28
28
  springRef.current = spring;
29
+ const rmRef = useRef(reducedMotion);
30
+ rmRef.current = reducedMotion;
29
31
  const prevIcon = useRef(icon);
30
32
  const prevControlled = useRef(controlled);
31
33
  const dead = useRef(false);
@@ -39,14 +41,14 @@ const MorphIcon = forwardRef(function MorphIcon(props, ref) {
39
41
  if (morphRef.current) return morphRef.current;
40
42
  const el = pathRef.current;
41
43
  if (dead.current || !el) return null;
42
- morphRef.current = createMorph(el, birth);
44
+ morphRef.current = createMorph(el, birth, { reducedMotion: rmRef.current });
43
45
  return morphRef.current;
44
46
  }, []);
45
47
  useIsoLayoutEffect(() => {
46
48
  dead.current = false;
47
49
  const el = pathRef.current;
48
50
  if (el && initialIcon !== void 0) {
49
- const m = createMorph(el, controlled ? from : initialIcon);
51
+ const m = createMorph(el, controlled ? from : initialIcon, { reducedMotion: rmRef.current });
50
52
  morphRef.current = m;
51
53
  if (controlled) {
52
54
  pair.current = [from, to];
@@ -67,6 +69,10 @@ const MorphIcon = forwardRef(function MorphIcon(props, ref) {
67
69
  pair.current = null;
68
70
  };
69
71
  }, []);
72
+ useEffect(() => {
73
+ const m = morphRef.current;
74
+ if (m) m.reducedMotion = reducedMotion ?? "never";
75
+ }, [reducedMotion]);
70
76
  useEffect(() => {
71
77
  const left = prevControlled.current && !controlled;
72
78
  prevControlled.current = controlled;
@@ -1,5 +1,5 @@
1
1
  import { a as Sampled, c as SpringPreset, n as IconInput, r as IconNode } from "./types-JAF3s76Y.js";
2
- import { Morph, MorphOptions, PathEl } from "./dom.js";
2
+ import { Morph, MorphOptions, PathEl, ReducedMotionMode } from "./dom.js";
3
3
  import { SVGAttributes } from "svelte/elements";
4
4
  //#region src/svelte/shared.d.ts
5
5
  /** Imperative surface exposed on the component instance (`bind:this`). */
@@ -22,6 +22,10 @@ interface MorphIconProps extends Omit<SVGAttributes<SVGSVGElement>, "from" | "to
22
22
  progress?: number;
23
23
  /** Physics for uncontrolled/imperative mode: preset or custom spring. */
24
24
  spring?: SpringPreset | MorphOptions;
25
+ /** Reduced-motion policy: "never" (default) animates regardless of the OS
26
+ * setting, "user" honors prefers-reduced-motion (morphs degrade to an
27
+ * instant swap while it is on), "always" always jumps. */
28
+ reducedMotion?: ReducedMotionMode;
25
29
  size?: number | string;
26
30
  color?: string;
27
31
  strokeWidth?: number | string;
@@ -31,7 +35,8 @@ interface MorphIconProps extends Omit<SVGAttributes<SVGSVGElement>, "from" | "to
31
35
  label?: string;
32
36
  }
33
37
  type ModeProps = Pick<MorphIconProps, "icon" | "from" | "to" | "progress">;
34
- type WatchProps = ModeProps & {
38
+ type CtrlProps = ModeProps & Pick<MorphIconProps, "reducedMotion">;
39
+ type WatchProps = CtrlProps & {
35
40
  spring?: SpringPreset | MorphOptions;
36
41
  };
37
42
  /** The initial d is constant for Svelte: computed once from the mount-time
@@ -54,15 +59,15 @@ declare function computeInitialD({ icon, from, to, progress }: ModeProps): strin
54
59
  * path back to `icon`.
55
60
  * - Every exit from controlled mode (imperative call or icon takeover)
56
61
  * invalidates the frozen pair, so returning to it re-bases on `from`. */
57
- declare function createController({ icon, from, to, progress }: ModeProps): {
58
- mount(mountEl: PathEl, { icon, from, to, progress }: ModeProps): void;
62
+ declare function createController({ icon, from, to, progress, reducedMotion }: CtrlProps): {
63
+ mount(mountEl: PathEl, { icon, from, to, progress, reducedMotion }: CtrlProps): void;
59
64
  destroy(): void;
60
65
  /** Prop watcher: ONE owner decides per run — the controlled pair while it
61
66
  * is fully present, `icon` otherwise (mount doesn't fire thanks to the
62
67
  * init-time baselines). */
63
- watch({ icon, from, to, progress, spring }: WatchProps): void;
68
+ watch({ icon, from, to, progress, spring, reducedMotion }: WatchProps): void;
64
69
  morphTo(icon: IconInput, spring?: SpringPreset | MorphOptions): void;
65
70
  set(icon: IconInput): void;
66
71
  };
67
72
  //#endregion
68
- export { type IconInput, type IconNode, type Morph, MorphHandle, MorphIconProps, type MorphOptions, type PathEl, type Sampled, type SpringPreset, computeInitialD, createController };
73
+ export { type IconInput, type IconNode, type Morph, MorphHandle, MorphIconProps, type MorphOptions, type PathEl, type ReducedMotionMode, type Sampled, type SpringPreset, computeInitialD, createController };
@@ -35,10 +35,11 @@ function computeInitialD({ icon, from, to, progress }) {
35
35
  * path back to `icon`.
36
36
  * - Every exit from controlled mode (imperative call or icon takeover)
37
37
  * invalidates the frozen pair, so returning to it re-bases on `from`. */
38
- function createController({ icon, from, to, progress }) {
38
+ function createController({ icon, from, to, progress, reducedMotion }) {
39
39
  let el = null;
40
40
  let dead = false;
41
41
  let morph = null;
42
+ let rm = reducedMotion ?? "never";
42
43
  let based = false;
43
44
  let pair = null;
44
45
  let prevIcon = icon;
@@ -50,7 +51,7 @@ function createController({ icon, from, to, progress }) {
50
51
  const ensure = (birth) => {
51
52
  if (morph) return morph;
52
53
  if (dead || !el) return null;
53
- morph = createMorph(el, birth);
54
+ morph = createMorph(el, birth, { reducedMotion: rm });
54
55
  return morph;
55
56
  };
56
57
  /** Controlled mode: freeze the pair at `progress` via seek (no spring). */
@@ -77,12 +78,13 @@ function createController({ icon, from, to, progress }) {
77
78
  }
78
79
  };
79
80
  return {
80
- mount(mountEl, { icon, from, to, progress }) {
81
+ mount(mountEl, { icon, from, to, progress, reducedMotion }) {
81
82
  el = mountEl;
83
+ rm = reducedMotion ?? rm;
82
84
  const controlled = from !== void 0 && to !== void 0;
83
85
  const initialIcon = icon ?? from ?? to;
84
86
  if (initialIcon === void 0) return;
85
- const m = createMorph(el, controlled ? from : initialIcon);
87
+ const m = createMorph(el, controlled ? from : initialIcon, { reducedMotion: rm });
86
88
  morph = m;
87
89
  if (controlled) {
88
90
  pair = [from, to];
@@ -106,7 +108,9 @@ function createController({ icon, from, to, progress }) {
106
108
  /** Prop watcher: ONE owner decides per run — the controlled pair while it
107
109
  * is fully present, `icon` otherwise (mount doesn't fire thanks to the
108
110
  * init-time baselines). */
109
- watch({ icon, from, to, progress, spring }) {
111
+ watch({ icon, from, to, progress, spring, reducedMotion }) {
112
+ rm = reducedMotion ?? "never";
113
+ if (morph) morph.reducedMotion = rm;
110
114
  const controlled = from !== void 0 && to !== void 0;
111
115
  const left = prevControlled && !controlled;
112
116
  const iconChanged = icon !== prevIcon;
package/dist/vue.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as Sampled, c as SpringPreset, n as IconInput, r as IconNode } from "./types-JAF3s76Y.js";
2
- import { Morph, MorphOptions, PathEl } from "./dom.js";
2
+ import { Morph, MorphOptions, PathEl, ReducedMotionMode } from "./dom.js";
3
3
  import { PropType } from "vue";
4
4
  //#region src/vue/index.d.ts
5
5
  /** Imperative surface exposed on the component instance (template ref). */
@@ -33,6 +33,13 @@ declare const MorphIcon: import("vue").DefineComponent<import("vue").ExtractProp
33
33
  type: PropType<SpringPreset | MorphOptions>;
34
34
  default: undefined;
35
35
  };
36
+ /** Reduced-motion policy: "never" (default) animates regardless of the OS
37
+ * setting, "user" honors prefers-reduced-motion (morphs degrade to an
38
+ * instant swap while it is on), "always" always jumps. */
39
+ reducedMotion: {
40
+ type: PropType<ReducedMotionMode>;
41
+ default: string;
42
+ };
36
43
  size: {
37
44
  type: (NumberConstructor | StringConstructor)[];
38
45
  default: number;
@@ -83,6 +90,13 @@ declare const MorphIcon: import("vue").DefineComponent<import("vue").ExtractProp
83
90
  type: PropType<SpringPreset | MorphOptions>;
84
91
  default: undefined;
85
92
  };
93
+ /** Reduced-motion policy: "never" (default) animates regardless of the OS
94
+ * setting, "user" honors prefers-reduced-motion (morphs degrade to an
95
+ * instant swap while it is on), "always" always jumps. */
96
+ reducedMotion: {
97
+ type: PropType<ReducedMotionMode>;
98
+ default: string;
99
+ };
86
100
  size: {
87
101
  type: (NumberConstructor | StringConstructor)[];
88
102
  default: number;
@@ -111,6 +125,7 @@ declare const MorphIcon: import("vue").DefineComponent<import("vue").ExtractProp
111
125
  to: IconInput;
112
126
  progress: number;
113
127
  spring: "bouncy" | "smooth" | "snappy" | MorphOptions;
128
+ reducedMotion: ReducedMotionMode;
114
129
  size: string | number;
115
130
  color: string;
116
131
  strokeWidth: string | number;
@@ -118,4 +133,4 @@ declare const MorphIcon: import("vue").DefineComponent<import("vue").ExtractProp
118
133
  label: string;
119
134
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
120
135
  //#endregion
121
- export { type IconInput, type IconNode, type Morph, MorphHandle, MorphIcon, type MorphOptions, type PathEl, type Sampled, type SpringPreset };
136
+ export { type IconInput, type IconNode, type Morph, MorphHandle, MorphIcon, type MorphOptions, type PathEl, type ReducedMotionMode, type Sampled, type SpringPreset };
package/dist/vue.js CHANGED
@@ -40,6 +40,13 @@ const MorphIcon = defineComponent({
40
40
  type: [String, Object],
41
41
  default: void 0
42
42
  },
43
+ /** Reduced-motion policy: "never" (default) animates regardless of the OS
44
+ * setting, "user" honors prefers-reduced-motion (morphs degrade to an
45
+ * instant swap while it is on), "always" always jumps. */
46
+ reducedMotion: {
47
+ type: String,
48
+ default: "never"
49
+ },
43
50
  size: {
44
51
  type: [Number, String],
45
52
  default: 24
@@ -84,7 +91,7 @@ const MorphIcon = defineComponent({
84
91
  if (morph) return morph;
85
92
  const el = pathEl.value;
86
93
  if (dead || !el) return null;
87
- morph = createMorph(el, birth);
94
+ morph = createMorph(el, birth, { reducedMotion: props.reducedMotion });
88
95
  return morph;
89
96
  };
90
97
  onMounted(() => {
@@ -93,7 +100,7 @@ const MorphIcon = defineComponent({
93
100
  const controlled = from !== void 0 && to !== void 0;
94
101
  const initialIcon = icon ?? from ?? to;
95
102
  if (!el || initialIcon === void 0) return;
96
- const m = createMorph(el, controlled ? from : initialIcon);
103
+ const m = createMorph(el, controlled ? from : initialIcon, { reducedMotion: props.reducedMotion });
97
104
  morph = m;
98
105
  if (controlled) {
99
106
  pair = [from, to];
@@ -113,6 +120,9 @@ const MorphIcon = defineComponent({
113
120
  based = false;
114
121
  pair = null;
115
122
  });
123
+ watch(() => props.reducedMotion, (v) => {
124
+ if (morph) morph.reducedMotion = v;
125
+ });
116
126
  watch(() => [
117
127
  props.icon,
118
128
  props.from,
@@ -128,8 +138,10 @@ const MorphIcon = defineComponent({
128
138
  if (icon === void 0 || !changed && !left) return;
129
139
  pair = null;
130
140
  based = false;
131
- if (morph) morph.morphTo(icon, props.spring);
132
- else ensure(icon);
141
+ if (morph) {
142
+ morph.reducedMotion = props.reducedMotion;
143
+ morph.morphTo(icon, props.spring);
144
+ } else ensure(icon);
133
145
  });
134
146
  watch(() => [
135
147
  props.from,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morphicons",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "description": "Universal morphing for stroke-based icons with spring physics — zero runtime dependencies",
5
5
  "license": "MIT",
6
6
  "author": "Guillermo (https://guillermolg.com)",