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; }