import { useEffect, useRef } from 'react';
import { useSharedValue } from 'react-native-reanimated';
import type { InternalControllerState, UseAnimationState, UseAnimationStateConfig, Variants } from './types';
/**
* Creates a variant-based animation state machine for driving Moti components.
*
* Define a set of named style variants and call `transitionTo(name)` to animate
* between them. Pass the returned controller's `__state` to a Moti component's
* `state` prop — the component will animate to whichever variant is active.
*
* ## Variant shape
*
* Each variant key maps to a style object with an optional per-variant
* `transition`:
*
* ```ts
* const state = useAnimationState({
* from: { opacity: 0, scale: 0.9 },
* to: { opacity: 1, scale: 1 },
* big: { opacity: 1, scale: 1.2, transition: { type: 'spring', damping: 10 } },
* })
*
* // Drive a MotiView
*
*
* // Animate between variants
* state.transitionTo('big')
* ```
*
* ## Reserved keys
*
* - `from` — the initial variant (applied on mount when no `to` is given).
* - `to` — if provided, `transitionTo('to')` is called automatically on mount.
*
* @param _variants - A record of named style variants. The keys `from` and `to`
* have special meaning (see above).
* @param config - Optional `from`/`to` key overrides (defaults to `'from'` and
* `'to'`).
* @returns A stable controller with `current` (the active variant key), `__state`
* (a Reanimated shared value to pass to a Moti component's `state` prop), and
* `transitionTo(key | fn)` to switch variants.
*/
export default function useAnimationState>(
_variants: V,
// biome-ignore lint/plugin: 'from'/'to' string literals can't be proven to be keyof V for an arbitrary generic V; these are moti's documented default variant keys
{ from = 'from' as keyof V, to = 'to' as keyof V }: UseAnimationStateConfig = {},
) {
const controller = useRef>(null);
const __state = useSharedValue>(from ? _variants[from] : 0);
const selectedVariant = useRef(from);
const variants = useRef(_variants);
// biome-ignore lint/plugin: variants ref must stay in sync with the latest prop without triggering re-renders — a ref update is a side effect
useEffect(
function updateVariantsRef() {
variants.current = _variants;
},
[_variants],
);
if (controller.current === null)
controller.current = {
__state,
transitionTo(nextStateOrFunction) {
const runTransition = (nextStateKey: keyof V) => {
selectedVariant.current = nextStateKey;
const value = variants.current[nextStateKey];
if (value) __state.value = value;
};
// biome-ignore lint/plugin: UseAnimationState types `current` as `keyof V | null`; runTransition needs the non-null key and the getter guarantees it
if (typeof nextStateOrFunction === 'function') runTransition(nextStateOrFunction(this.current as keyof V));
else runTransition(nextStateOrFunction);
},
get current(): keyof V {
return selectedVariant.current;
},
};
// biome-ignore lint/plugin: initial transition on mount must fire as a side effect after the controller ref is set up — cannot be expressed as derived state
useEffect(
function maybeTransitionOnMount() {
if (variants.current[to]) {
if (variants.current[from]) controller.current?.transitionTo(to);
else
console.error(
`🐼 [moti]: Called useAnimationState with a "to" variant, but you are missing a "from" variant. A "from" variant is required if you are using "to". Instead, you passed these variants: "${Object.keys(
variants.current,
).join(
', ',
)}". If you want to just use the "to" value without "from", you shouldn't use this hook. Instead, just pass your values to a Moti component's "animate" prop.`,
);
}
},
[from, to],
);
// biome-ignore lint/plugin: controller.current is guaranteed non-null by the lazy-init block above, but TS can't track that across the ref; the cast preserves the hook's non-null return type
return controller.current as UseAnimationState;
}