@tamagui/animations-motion 2.0.0-rc.9 → 2.0.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.
Files changed (40) hide show
  1. package/dist/cjs/createAnimations.cjs +559 -299
  2. package/dist/cjs/createAnimations.native.js +653 -318
  3. package/dist/cjs/createAnimations.native.js.map +1 -1
  4. package/dist/cjs/index.cjs +7 -5
  5. package/dist/cjs/index.native.js +21 -13
  6. package/dist/cjs/index.native.js.map +1 -1
  7. package/dist/cjs/polyfill.cjs +3 -1
  8. package/dist/cjs/polyfill.native.js +3 -1
  9. package/dist/cjs/polyfill.native.js.map +1 -1
  10. package/dist/esm/createAnimations.mjs +529 -272
  11. package/dist/esm/createAnimations.mjs.map +1 -1
  12. package/dist/esm/createAnimations.native.js +623 -291
  13. package/dist/esm/createAnimations.native.js.map +1 -1
  14. package/dist/esm/index.js +1 -2
  15. package/dist/esm/index.js.map +1 -6
  16. package/dist/esm/index.mjs +0 -1
  17. package/dist/esm/index.mjs.map +1 -1
  18. package/dist/esm/index.native.js +9 -3
  19. package/dist/esm/index.native.js.map +1 -1
  20. package/dist/esm/polyfill.mjs +3 -1
  21. package/dist/esm/polyfill.mjs.map +1 -1
  22. package/dist/esm/polyfill.native.js +3 -1
  23. package/dist/esm/polyfill.native.js.map +1 -1
  24. package/package.json +8 -7
  25. package/src/createAnimations.tsx +469 -351
  26. package/types/createAnimations.d.ts +1 -0
  27. package/types/createAnimations.d.ts.map +4 -4
  28. package/types/index.d.ts.map +2 -2
  29. package/types/index.native.d.ts.map +2 -2
  30. package/types/polyfill.d.ts.map +2 -2
  31. package/dist/cjs/createAnimations.js +0 -412
  32. package/dist/cjs/createAnimations.js.map +0 -6
  33. package/dist/cjs/index.js +0 -16
  34. package/dist/cjs/index.js.map +0 -6
  35. package/dist/cjs/polyfill.js +0 -2
  36. package/dist/cjs/polyfill.js.map +0 -6
  37. package/dist/esm/createAnimations.js +0 -416
  38. package/dist/esm/createAnimations.js.map +0 -6
  39. package/dist/esm/polyfill.js +0 -2
  40. package/dist/esm/polyfill.js.map +0 -6
@@ -2,6 +2,7 @@ import { type AnimationDriver } from "@tamagui/web";
2
2
  import { type ValueTransition } from "motion/react";
3
3
  type AnimationConfig = ValueTransition;
4
4
  export declare function createAnimations<A extends Record<string, AnimationConfig>>(animations: A): AnimationDriver<A>;
5
+ export declare const disableAnimationProps: Set<string>;
5
6
  export {};
6
7
 
7
8
  //# sourceMappingURL=createAnimations.d.ts.map
@@ -1,11 +1,11 @@
1
1
  {
2
- "mappings": "AAEA,cAEO,uBAaA;AACP,cAOO,uBACA;KAYF,kBAAkB;AAgCvB,OAAO,iBAAS,iBAAiB,UAAU,eAAe,kBACxD,YAAY,IACX,gBAAgB",
2
+ "mappings": "AAEA,cAEO,uBAaA;AACP,cAOO,uBACA;KAuBF,kBAAkB;AAgEvB,OAAO,iBAAS,iBAAiB,UAAU,eAAe,kBACxD,YAAY,IACX,gBAAgB;AAsrBnB,OAAO,cAAM,uBAAuB",
3
3
  "names": [],
4
4
  "sources": [
5
5
  "src/createAnimations.tsx"
6
6
  ],
7
+ "version": 3,
7
8
  "sourcesContent": [
8
- "import { getEffectiveAnimation, normalizeTransition } from '@tamagui/animation-helpers'\nimport { ResetPresence, usePresence } from '@tamagui/use-presence'\nimport {\n type AnimatedNumberStrategy,\n type AnimationDriver,\n fixStyles,\n getConfig,\n getSplitStyles,\n hooks,\n styleToCSS,\n Text,\n type TransitionProp,\n type UniversalAnimatedNumber,\n useComposedRefs,\n useIsomorphicLayoutEffect,\n useThemeWithState,\n View,\n} from '@tamagui/web'\nimport {\n type AnimationOptions,\n type AnimationPlaybackControlsWithThen,\n type MotionValue,\n useAnimate,\n useMotionValue,\n useMotionValueEvent,\n type ValueTransition,\n} from 'motion/react'\nimport React, {\n forwardRef,\n useEffect,\n useId,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from 'react'\n\ntype MotionAnimatedNumber = MotionValue<number>\ntype AnimationConfig = ValueTransition\n\ntype MotionAnimatedNumberStyle = {\n getStyle: (cur: number) => Record<string, unknown>\n motionValue: MotionValue<number>\n}\n\n/**\n * Animation options with optional default and per-property configs.\n * This extends AnimationOptions to support the default key.\n */\ntype TransitionAnimationOptions = AnimationOptions & {\n default?: ValueTransition\n [propertyName: string]: ValueTransition | undefined\n}\n\nconst MotionValueStrategy = new WeakMap<MotionValue, AnimatedNumberStrategy>()\n\n// regex to detect non-position transform operations (scale, rotate, skew, matrix, perspective)\n// used to identify position-only transforms for the popper animation fix\nconst nonPositionTransformRe = /scale|rotate|skew|matrix|perspective/\n\ntype AnimationProps = {\n doAnimate?: Record<string, unknown>\n dontAnimate?: Record<string, unknown>\n animationOptions?: AnimationOptions\n}\n\n// track if we're still in the initial hydration phase\n// TODO didnt realize claude took the wrong one here - this should uust be isComponentHydrating ideally\n// but this is fine for beta motion driver rc.0 fix in rc.1\n\nexport function createAnimations<A extends Record<string, AnimationConfig>>(\n animations: A\n): AnimationDriver<A> {\n let isHydratingGlobal: boolean | undefined\n const hydratingComponents = new Set<Function>()\n\n return {\n // this is only used by Sheet basically for now to pass result of useAnimatedStyle to\n View: MotionView,\n Text: MotionText,\n isReactNative: false,\n supportsCSS: true,\n inputStyle: 'css',\n outputStyle: 'inline',\n needsWebStyles: true,\n avoidReRenders: true,\n animations,\n usePresence,\n ResetPresence,\n\n onMount() {\n isHydratingGlobal = false\n hydratingComponents.forEach((cb) => cb())\n },\n\n useAnimations: (animationProps) => {\n if (isHydratingGlobal === undefined && !getConfig().settings.disableSSR) {\n isHydratingGlobal = true\n }\n\n const { props, style, componentState, stateRef, useStyleEmitter, presence } =\n animationProps\n\n const animationKey = Array.isArray(props.transition)\n ? props.transition[0]\n : props.transition\n\n const isComponentHydrating = componentState.unmounted === true\n const isMounting = componentState.unmounted === 'should-enter'\n const isEntering = !!componentState.unmounted\n const isExiting = presence?.[0] === false\n const sendExitComplete = presence?.[1]\n\n // Track if we just finished entering (transition from entering to not entering)\n const wasEnteringRef = useRef(isEntering)\n const justFinishedEntering = wasEnteringRef.current && !isEntering\n useEffect(() => {\n wasEnteringRef.current = isEntering\n })\n\n // Determine animation state for enter/exit transitions\n // Use 'enter' if we're mounting OR if we just finished entering\n const animationState: 'enter' | 'exit' | 'default' = isExiting\n ? 'exit'\n : isMounting || justFinishedEntering\n ? 'enter'\n : 'default'\n\n // Disable animation during hydration AND during mounting (should-enter phase)\n // This prevents the \"flying across the page\" effect on initial render\n const disableAnimation = isComponentHydrating || isMounting || !animationKey\n\n const isFirstRender = useRef(true)\n const [scope, animate] = useAnimate()\n const lastDoAnimate = useRef<Record<string, unknown> | null>(null)\n const controls = useRef<AnimationPlaybackControlsWithThen | null>(null)\n const styleKey = JSON.stringify(style)\n\n // until fully stable allow debugging in prod to help debugging prod issues\n const shouldDebug =\n // process.env.NODE_ENV === 'development' &&\n props['debug'] && props['debug'] !== 'profile'\n\n const {\n dontAnimate = {},\n doAnimate,\n animationOptions,\n } = useMemo(() => {\n const motionAnimationState = getMotionAnimatedProps(\n props as any,\n style,\n disableAnimation,\n animationState\n )\n return motionAnimationState\n }, [isExiting, animationKey, styleKey, animationState, disableAnimation])\n\n const id = useId()\n const debugId = process.env.NODE_ENV === 'development' ? id : ''\n const lastAnimateAt = useRef(0)\n const disposed = useRef(false)\n const [firstRenderStyle] = useState(style)\n\n // avoid first render returning wrong styles - always render all, after that we can just mutate\n const lastDontAnimate = useRef<Record<string, unknown> | null>(firstRenderStyle)\n const [isHydrating, setIsHydrating] = useState(isHydratingGlobal)\n\n useLayoutEffect(() => {\n if (isHydratingGlobal) {\n hydratingComponents.add(() => {\n setIsHydrating(false)\n })\n }\n return () => {\n disposed.current = true\n }\n }, [])\n\n const flushAnimation = ({\n doAnimate = {},\n animationOptions = {},\n dontAnimate,\n }: AnimationProps) => {\n try {\n const node = stateRef.current.host\n\n // on first render, reset stale animation refs - they can persist if component\n // instance is reused (e.g. AnimatePresence keepChildrenMounted)\n if (isFirstRender.current) {\n lastDontAnimate.current = null\n lastDoAnimate.current = null\n }\n\n if (shouldDebug) {\n console.groupCollapsed(\n `[motion] ${debugId} 🌊 animate (${JSON.stringify(getDiff(lastDoAnimate.current, doAnimate), null, 2)})`\n )\n console.info({\n props,\n componentState,\n doAnimate,\n dontAnimate,\n animationOptions,\n animationProps,\n lastDoAnimate: { ...lastDoAnimate.current },\n lastDontAnimate: { ...lastDontAnimate.current },\n isExiting,\n style,\n node,\n })\n console.groupCollapsed(`trace >`)\n console.trace()\n console.groupEnd()\n console.groupEnd()\n }\n\n if (!(node instanceof HTMLElement)) {\n return\n }\n\n // handle case where dontAnimate changes\n // we just set it onto animate + set options to not actually animate\n const prevDont = lastDontAnimate.current\n if (dontAnimate) {\n if (prevDont) {\n // Pass doAnimate as preserve to prevent clearing styles that moved to doAnimate\n removeRemovedStyles(prevDont, dontAnimate, node, doAnimate)\n const changed = getDiff(prevDont, dontAnimate)\n if (changed) {\n Object.assign(node.style, changed as any)\n }\n } else {\n // First time - apply directly without diff check\n Object.assign(node.style, dontAnimate as any)\n }\n }\n\n if (doAnimate) {\n // bugfix: going from non-animated to animated in motion -\n // motion batches things so the above removal can happen a frame before causing flickering\n // we see this with tooltips, this is not an ideal solution though, ideally we can remove/update\n // in the same batch/frame as motion\n // Also sync motion's internal state for properties moving from dontAnimate to doAnimate\n if (prevDont) {\n const movedToAnimate: Record<string, unknown> = {}\n for (const key in prevDont) {\n if (key in doAnimate) {\n node.style[key] = prevDont[key]\n movedToAnimate[key] = prevDont[key]\n // Also update lastDoAnimate to include the previous value\n // This prevents animating from undefined to the current value\n // when a property transitions from dontAnimate to doAnimate\n if (lastDoAnimate.current) {\n lastDoAnimate.current[key] = prevDont[key]\n }\n }\n }\n // Sync motion's internal state for moved properties\n if (Object.keys(movedToAnimate).length > 0) {\n animate(scope.current, { ...movedToAnimate }, { duration: 0 })\n }\n }\n\n const lastAnimated = lastDoAnimate.current\n if (lastAnimated) {\n // Pass dontAnimate as third arg to prevent clearing styles that moved to dontAnimate\n removeRemovedStyles(lastAnimated, doAnimate, node, dontAnimate)\n }\n\n const diff = getDiff(lastDoAnimate.current, doAnimate)\n if (diff) {\n // FIX: Handle animation interruption for position-only animations\n // Only apply this fix when:\n // 1. There's a running animation\n // 2. The transform change is POSITION-ONLY (just translate, no scale/rotate/skew)\n // 3. animatePosition is being used (Popper/Tooltip pattern)\n // This fixes tooltip position jumping without breaking AnimatePresence scale/rotate animations\n // NOTE: We check for animatePosition to avoid this fix causing jitter\n // on components like the TAMAGUI logo dot indicator which also use translate-only transforms\n\n const isRunning =\n /**\n * TypeError: Cannot read properties of undefined (reading 'state')\n * at GroupAnimationWithThen.getAll (http://localhost:8081/node_modules/.vite/deps/@tamagui_config_v5-motion.js?v=d717d926:2374:30)\n * at get state (http://localhost:8081/node_modules/.vite/deps/@tamagui_config_v5-motion.js?v=d717d926:2403:17)\n * at flushAnimation (http://localhost:8081/node_modules/.vite/deps/@tamagui_config_v5-motion.js?v=d717d926:9686:49)\n **/\n // @ts-expect-error it is there, and for some crazy reason in ~/chat pretty often i get errors ^\n controls.current?.animations?.length === 0\n ? false\n : controls.current?.state === 'running'\n const targetTransform =\n typeof diff.transform === 'string' ? diff.transform : null\n\n // only apply position fix for translate-only transforms\n const isPositionOnlyTransform =\n targetTransform &&\n targetTransform.includes('translate') &&\n !nonPositionTransformRe.test(targetTransform)\n\n // Position fix for Popper/Tooltip elements with animatePosition.\n // Only apply when:\n // 1. Animation is actively running\n // 2. Transform is position-only (translate without scale/rotate/etc)\n // 3. Element has data-popper-animate-position attribute (set by Popper when\n // animatePosition is true)\n //\n // The issue: when a Popper animation is interrupted mid-flight, motion's\n // animate() may start from wrong position causing jumps to origin.\n //\n // Why check data-popper-animate-position: This attribute is ONLY set on Popper\n // elements that explicitly use animatePosition. Regular\n // components like the logo Circle don't have this attribute, so they won't\n // get this fix applied (which would cause jitter due to getComputedStyle\n // overhead on rapid updates).\n\n // check if this is a Popper element with animated position\n const isPopperElement = node.hasAttribute('data-popper-animate-position')\n\n // also apply fix for AnimatePresence children that just finished entering\n // this fixes roving tabs indicator jumping when rapidly switching\n const isEnteringPresenceChild = presence && justFinishedEntering\n\n if (\n isRunning &&\n controls.current &&\n isPositionOnlyTransform &&\n (isPopperElement || isEnteringPresenceChild)\n ) {\n const currentTransform = getComputedStyle(node).transform\n\n if (currentTransform && currentTransform !== 'none') {\n const matrixMatch = currentTransform.match(\n /matrix\\([^,]+,\\s*[^,]+,\\s*[^,]+,\\s*[^,]+,\\s*([^,]+),\\s*([^)]+)\\)/\n )\n\n if (matrixMatch) {\n // stop animation and preserve current position\n controls.current.stop()\n node.style.transform = currentTransform\n\n // animate from current matrix position to target\n const currentX = Number.parseFloat(matrixMatch[1])\n const currentY = Number.parseFloat(matrixMatch[2])\n const keyframeDiff = {\n ...diff,\n transform: [\n `translateX(${currentX}px) translateY(${currentY}px)`,\n targetTransform,\n ],\n }\n\n controls.current = animate(\n scope.current,\n keyframeDiff,\n animationOptions\n )\n lastAnimateAt.current = Date.now()\n lastDontAnimate.current = dontAnimate ? { ...dontAnimate } : {}\n lastDoAnimate.current = doAnimate ? { ...doAnimate } : {}\n return\n }\n }\n }\n\n // IMPORTANT: Spread to create mutable copy - style objects may be frozen\n // fix transparent colors to use rgba for motion.dev compatibility\n const fixedDiff = fixTransparentColors({ ...diff }, lastDoAnimate.current)\n\n controls.current = animate(scope.current, fixedDiff, animationOptions)\n lastAnimateAt.current = Date.now()\n }\n }\n\n // IMPORTANT: Spread to create mutable copies - objects may be frozen\n lastDontAnimate.current = dontAnimate ? { ...dontAnimate } : {}\n lastDoAnimate.current = doAnimate ? { ...doAnimate } : {}\n } finally {\n if (isExiting) {\n if (controls.current) {\n controls.current.finished.then(() => {\n sendExitComplete?.()\n })\n } else {\n sendExitComplete?.()\n }\n }\n }\n }\n\n useStyleEmitter?.((nextStyle) => {\n const animationProps = getMotionAnimatedProps(\n props as any,\n nextStyle,\n disableAnimation,\n animationState\n )\n\n flushAnimation(animationProps)\n })\n\n useIsomorphicLayoutEffect(() => {\n if (isFirstRender.current) {\n isFirstRender.current = false\n\n // during hydration, use full sync logic to prevent flash\n // doing this - will fix some of the enter (accordion) glitches but breaks animatepresence\n // isHydrating || (isMounting && !isEntering)\n if (isHydrating) {\n const node = stateRef.current.host\n\n if (node instanceof HTMLElement) {\n // IMPORTANT: On first render, we need to:\n // 1. Apply dontAnimate styles to the DOM (enterStyle values like scale(0))\n // 2. Tell motion about these styles so it knows the starting state\n // This ensures AnimatePresence enter animations work correctly.\n\n if (dontAnimate) {\n // Apply initial styles to DOM\n Object.assign(node.style, dontAnimate as any)\n\n // Tell motion about the initial state by animating instantly to dontAnimate\n // This syncs motion's internal state with what's actually on the DOM\n // IMPORTANT: Spread to create mutable copy - React/Tamagui style objects may be frozen\n animate(scope.current, { ...dontAnimate }, { duration: 0 })\n }\n\n // If there are styles to animate, set them up (but animation is disabled on first render)\n if (doAnimate && Object.keys(doAnimate).length > 0) {\n // IMPORTANT: Spread to create mutable copy - objects may be frozen\n lastDoAnimate.current = { ...doAnimate }\n animate(scope.current, { ...doAnimate }, { duration: 0 })\n } else {\n // doAnimate is empty, so track dontAnimate as the initial animated state\n // This way on next render, getDiff will detect the change\n // IMPORTANT: Spread to create mutable copy - objects may be frozen\n lastDoAnimate.current = dontAnimate ? { ...dontAnimate } : {}\n }\n }\n\n // IMPORTANT: Spread to create mutable copy - objects may be frozen\n lastDontAnimate.current = dontAnimate ? { ...dontAnimate } : {}\n lastAnimateAt.current = Date.now()\n return\n }\n\n // after hydration, use simpler logic\n lastDontAnimate.current = dontAnimate ? { ...dontAnimate } : {}\n lastDoAnimate.current = doAnimate ? { ...doAnimate } : {}\n return\n }\n\n // don't ever queue on a render\n flushAnimation({\n doAnimate,\n dontAnimate,\n animationOptions,\n })\n }, [styleKey, isExiting, disableAnimation])\n\n if (shouldDebug) {\n console.groupCollapsed(`[motion] 🌊 render`)\n console.info({\n style,\n doAnimate,\n dontAnimate,\n styleKey,\n scope,\n animationOptions,\n isExiting,\n isFirstRender: isFirstRender.current,\n animationProps,\n })\n console.groupEnd()\n }\n\n return {\n // we never change this, after first render on\n style: firstRenderStyle,\n ref: scope,\n render: 'div',\n }\n },\n\n useAnimatedNumber(initial): UniversalAnimatedNumber<MotionAnimatedNumber> {\n const motionValue = useMotionValue(initial)\n\n return React.useMemo(\n () => ({\n getInstance() {\n return motionValue\n },\n getValue() {\n return motionValue.get()\n },\n setValue(next, config = { type: 'spring' }, onFinish) {\n if (config.type === 'direct') {\n MotionValueStrategy.set(motionValue, {\n type: 'direct',\n })\n motionValue.set(next)\n onFinish?.()\n } else {\n MotionValueStrategy.set(motionValue, config)\n\n if (onFinish) {\n const unsubscribe = motionValue.on('change', (value) => {\n if (Math.abs(value - next) < 0.01) {\n unsubscribe()\n onFinish()\n }\n })\n }\n\n motionValue.set(next)\n // Motion doesn't have a direct onFinish callback, so we simulate it\n }\n },\n stop() {\n motionValue.stop()\n },\n }),\n [motionValue]\n )\n },\n\n useAnimatedNumberReaction({ value }, onValue) {\n const instance = value.getInstance() as MotionValue<number>\n useMotionValueEvent(instance, 'change', onValue)\n },\n\n useAnimatedNumberStyle(val, getStyleProp) {\n const motionValue = val.getInstance() as MotionValue<number>\n const getStyleRef = useRef<typeof getStyleProp>(getStyleProp)\n\n // we need to change useAnimatedNumberStyle to have dep args to be concurrent safe\n getStyleRef.current = getStyleProp\n\n // never changes\n return useMemo(() => {\n return {\n getStyle: (cur) => {\n return getStyleRef.current(cur)\n },\n motionValue,\n } satisfies MotionAnimatedNumberStyle\n }, [])\n },\n }\n\n function getMotionAnimatedProps(\n props: { transition: TransitionProp | null; animateOnly?: string[] },\n style: Record<string, unknown>,\n disable: boolean,\n animationState: 'enter' | 'exit' | 'default' = 'default'\n ): AnimationProps {\n if (disable) {\n return {\n dontAnimate: style,\n }\n }\n\n const animationOptions = getAnimationOptions(props.transition, animationState)\n\n let dontAnimate: Record<string, unknown> | undefined\n let doAnimate: Record<string, unknown> | undefined\n\n const animateOnly = props.animateOnly as string[] | undefined\n for (const key in style) {\n const value = style[key]\n if (disableAnimationProps.has(key) || (animateOnly && !animateOnly.includes(key))) {\n dontAnimate ||= {}\n dontAnimate[key] = value\n } else {\n doAnimate ||= {}\n doAnimate[key] = value\n }\n }\n\n return {\n dontAnimate,\n doAnimate,\n animationOptions,\n }\n }\n\n function getAnimationOptions(\n transitionProp: TransitionProp | null,\n animationState: 'enter' | 'exit' | 'default' = 'default'\n ): TransitionAnimationOptions {\n const normalized = normalizeTransition(transitionProp)\n\n // Get the effective animation key based on enter/exit/default state\n const effectiveKey = getEffectiveAnimation(normalized, animationState)\n\n // If no animation defined, return empty config\n if (!effectiveKey && Object.keys(normalized.properties).length === 0) {\n return {}\n }\n\n const defaultConfig = effectiveKey ? withInferredType(animations[effectiveKey]) : null\n\n // Framer Motion uses seconds, so convert from ms\n const delay =\n typeof normalized.delay === 'number' ? normalized.delay / 1000 : undefined\n\n // Convert global config overrides from ms to seconds where needed\n let globalConfigOverride: Record<string, unknown> | undefined\n if (normalized.config) {\n globalConfigOverride = { ...normalized.config }\n if (typeof normalized.config.duration === 'number') {\n globalConfigOverride.duration = normalized.config.duration / 1000\n }\n }\n\n // Build the animation options\n // Framer Motion's animate() expects default config at the TOP LEVEL, not nested under 'default'\n // Per-property configs can be nested under property names like { scale: { duration: 1 } }\n const result: TransitionAnimationOptions = {}\n\n // Apply base config from preset at top level (this is what animate() reads as default)\n if (defaultConfig) {\n Object.assign(result, defaultConfig)\n }\n\n // Apply global spring config overrides at top level\n if (globalConfigOverride) {\n Object.assign(result, globalConfigOverride)\n }\n\n // Apply delay at top level\n if (delay) {\n result.delay = delay\n }\n\n // Also set the 'default' key for backwards compatibility with per-property fallback logic\n if (defaultConfig || globalConfigOverride || delay) {\n result.default = {\n ...defaultConfig,\n ...globalConfigOverride,\n ...(delay ? { delay } : null),\n }\n }\n\n // Add property-specific animations\n for (const [propName, animationNameOrConfig] of Object.entries(\n normalized.properties\n )) {\n if (typeof animationNameOrConfig === 'string') {\n result[propName] = withInferredType(animations[animationNameOrConfig])\n } else if (animationNameOrConfig && typeof animationNameOrConfig === 'object') {\n const baseConfig = animationNameOrConfig.type\n ? withInferredType(animations[animationNameOrConfig.type])\n : defaultConfig\n\n // @ts-expect-error\n result[propName] = {\n ...baseConfig,\n ...animationNameOrConfig,\n }\n }\n }\n\n // we standardize to ms across drivers, motion expects s\n // apply to default and each per-property config\n convertMsToS(result.default)\n for (const key in result) {\n if (key !== 'default') convertMsToS(result[key])\n }\n\n return result\n }\n}\n\n// infer type from config shape if not explicitly set, always returns a copy\nfunction withInferredType(config: AnimationConfig): AnimationConfig {\n const isTimingBased =\n config.duration !== undefined &&\n config.damping === undefined &&\n config.stiffness === undefined &&\n config.mass === undefined\n return { type: isTimingBased ? 'tween' : 'spring', ...config }\n}\n\n// convert tween duration/delay from ms to s (motion expects seconds)\nfunction convertMsToS(config: ValueTransition | undefined) {\n if (!config || config.type !== 'tween') return\n if (typeof config.duration === 'number') config.duration = config.duration / 1000\n if (typeof config.delay === 'number') config.delay = config.delay / 1000\n}\n\nfunction removeRemovedStyles(\n prev: object,\n next: object,\n node: HTMLElement,\n dontClearIfIn?: object\n) {\n for (const key in prev) {\n if (!(key in next)) {\n // Don't clear if the style is now in dontAnimate (moved from animated to non-animated)\n if (dontClearIfIn && key in dontClearIfIn) {\n continue\n }\n node.style[key] = ''\n }\n }\n}\n\n// sort of temporary\nconst disableAnimationProps = new Set<string>([\n 'alignContent',\n 'alignItems',\n 'aspectRatio',\n 'backdropFilter',\n 'boxSizing',\n 'contain',\n 'containerType',\n 'display',\n 'flexBasis',\n 'flexDirection',\n 'flexGrow',\n 'flexShrink',\n 'fontFamily',\n 'justifyContent',\n 'marginBottom',\n 'marginLeft',\n 'marginRight',\n 'marginTop',\n 'maxHeight',\n 'maxWidth',\n 'minHeight',\n 'minWidth',\n 'overflow',\n 'overflowX',\n 'overflowY',\n 'pointerEvents',\n 'position',\n 'textWrap',\n 'transformOrigin',\n 'userSelect',\n 'WebkitBackdropFilter',\n 'zIndex',\n])\n\nconst MotionView = createMotionView('div')\nconst MotionText = createMotionView('span')\n\nfunction createMotionView(defaultTag: string) {\n // return forwardRef((props: any, ref) => {\n // console.info('rendering?', props)\n // const Element = motion[props.render || defaultTag]\n // return <Element ref={ref} {...props} />\n // })\n const isText = defaultTag === 'span'\n\n const Component = forwardRef((propsIn: any, ref) => {\n const { forwardedRef, animation, render = defaultTag, style, ...propsRest } = propsIn\n const [scope, animate] = useAnimate()\n const hostRef = useRef<HTMLElement>(null)\n const composedRefs = useComposedRefs(forwardedRef, ref, hostRef, scope)\n\n const stateRef = useRef<any>(null)\n if (!stateRef.current) {\n stateRef.current = {\n get host() {\n return hostRef.current\n },\n }\n }\n\n const [_, state] = useThemeWithState({})\n\n const styles = Array.isArray(style) ? style : [style]\n\n // we can assume just one animatedStyle max for now\n const [animatedStyle, nonAnimatedStyles] = (() => {\n return [\n styles.find((x) => x.getStyle) as MotionAnimatedNumberStyle | undefined,\n styles.filter((x) => !x.getStyle),\n ] as const\n })()\n\n function getProps(props: any) {\n const out = getSplitStyles(\n props,\n isText ? Text.staticConfig : View.staticConfig,\n state?.theme,\n state?.name,\n {\n unmounted: false,\n },\n {\n isAnimated: false,\n noClass: true,\n // noMergeStyle: true,\n resolveValues: 'auto',\n }\n )\n\n if (!out) {\n return {}\n }\n\n // we can definitely get rid of this here\n if (out.viewProps.style) {\n fixStyles(out.viewProps.style)\n styleToCSS(out.viewProps.style)\n }\n\n return out.viewProps\n }\n\n const props = getProps({ ...propsRest, style: nonAnimatedStyles })\n const Element = render || 'div'\n const transformedProps = hooks.usePropsTransform?.(render, props, stateRef, false)\n\n useEffect(() => {\n if (!animatedStyle) return\n\n return animatedStyle.motionValue.on('change', (value) => {\n const nextStyle = animatedStyle.getStyle(value)\n const animationConfig = MotionValueStrategy.get(animatedStyle.motionValue)\n const node = hostRef.current\n\n const webStyle = getProps({ style: nextStyle }).style\n\n if (webStyle && node instanceof HTMLElement) {\n const motionAnimationConfig =\n animationConfig?.type === 'timing'\n ? {\n type: 'tween',\n duration: (animationConfig?.duration || 0) / 1000,\n }\n : animationConfig?.type === 'direct'\n ? { type: 'tween', duration: 0 }\n : {\n type: 'spring',\n ...(animationConfig as any),\n }\n\n animate(node, webStyle as any, motionAnimationConfig)\n }\n })\n }, [animatedStyle])\n\n return <Element {...transformedProps} ref={composedRefs} />\n })\n\n Component['acceptTagProp'] = true\n\n return Component\n}\n\nfunction getDiff<T extends Record<string, unknown>>(\n previous: T | null,\n next: T\n): Record<string, unknown> | null {\n if (!previous) {\n return next\n }\n\n let diff: Record<string, unknown> | null = null\n for (const key in next) {\n if (next[key] !== previous[key]) {\n diff ||= {}\n diff[key] = next[key]\n }\n }\n return diff\n}\n\n// motion.dev can't animate to \"transparent\" - convert it to rgba based on previous value\n// if previous was rgba, use same rgb with alpha 0, otherwise use rgba(0,0,0,0)\nfunction fixTransparentColors(\n diff: Record<string, unknown>,\n previous: Record<string, unknown> | null\n): Record<string, unknown> {\n let result = diff\n for (const key in diff) {\n if (diff[key] === 'transparent') {\n const prev = previous?.[key]\n let fixed = 'rgba(0, 0, 0, 0)'\n if (typeof prev === 'string') {\n // match rgba(r, g, b, a) or rgb(r, g, b)\n const rgbaMatch = prev.match(/^rgba?\\(([^,]+),\\s*([^,]+),\\s*([^,)]+)/)\n if (rgbaMatch) {\n fixed = `rgba(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]}, 0)`\n }\n }\n if (result === diff) {\n result = { ...diff }\n }\n result[key] = fixed\n }\n }\n return result\n}\n"
9
- ],
10
- "version": 3
9
+ "import { getEffectiveAnimation, normalizeTransition } from '@tamagui/animation-helpers'\nimport { ResetPresence, usePresence } from '@tamagui/use-presence'\nimport {\n type AnimatedNumberStrategy,\n type AnimationDriver,\n fixStyles,\n getConfig,\n getSplitStyles,\n hooks,\n styleToCSS,\n Text,\n TransitionProp,\n type UniversalAnimatedNumber,\n useComposedRefs,\n useIsomorphicLayoutEffect,\n useThemeWithState,\n View,\n} from '@tamagui/web'\nimport {\n type AnimationOptions,\n type AnimationPlaybackControlsWithThen,\n type MotionValue,\n useAnimate,\n useMotionValue,\n useMotionValueEvent,\n type ValueTransition,\n} from 'motion/react'\nimport React, {\n forwardRef,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from 'react'\n\nconst isServer = typeof window === 'undefined'\n\n// SSR-safe wrapper: framer-motion's useAnimate imports its own React copy in\n// Vite SSR bundles which causes \"Invalid hook call\" errors. during SSR we\n// don't need animations so we return a no-op scope/animate pair.\nfunction useAnimateSSRSafe() {\n if (isServer) {\n return [useRef(null), (() => {}) as any] as ReturnType<typeof useAnimate>\n }\n return useAnimate()\n}\n\ntype MotionAnimatedNumber = MotionValue<number>\ntype AnimationConfig = ValueTransition\n\ntype MotionAnimatedNumberStyle = {\n getStyle: (...args: any[]) => Record<string, unknown>\n motionValue?: MotionValue<number>\n motionValues?: MotionValue<number>[]\n}\n\n/**\n * Animation options with optional default and per-property configs.\n * This extends AnimationOptions to support the default key.\n */\ntype TransitionAnimationOptions = AnimationOptions & {\n default?: ValueTransition\n [propertyName: string]: ValueTransition | undefined\n}\n\nconst MotionValueStrategy = new WeakMap<MotionValue, AnimatedNumberStrategy>()\n\n// pending setValue onFinish callbacks, keyed by motion value. setValue stores\n// the callback here; the change handler in the animated component's useEffect\n// consumes it by chaining to the DOM-level animate() controls so onFinish\n// fires when the *visible* animation actually completes.\nconst PendingMotionOnFinish = new WeakMap<MotionValue, () => void>()\n\nfunction settlePendingMotionOnFinish(\n mv: MotionValue,\n controls: AnimationPlaybackControlsWithThen\n) {\n const onFinish = PendingMotionOnFinish.get(mv)\n if (!onFinish) return\n PendingMotionOnFinish.delete(mv)\n // chain to the DOM animation's completion. settle on both resolve and\n // reject — a rejection means the animation was cancelled by a later\n // setValue, and the caller still needs a completion signal. use the\n // real Promise interface (.then().catch()) because framer-motion types\n // the .then() callbacks as VoidFunction with no error arg.\n controls.then(() => onFinish()).catch(() => onFinish())\n}\n\ntype AnimationProps = {\n doAnimate?: Record<string, unknown>\n dontAnimate?: Record<string, unknown>\n animationOptions?: AnimationOptions\n}\n\n// internal refs consolidated into a single object\ntype MotionRefs = {\n isFirstRender: boolean\n lastDoAnimate: Record<string, unknown> | null\n lastDontAnimate: Record<string, unknown> | null\n controls: AnimationPlaybackControlsWithThen | null\n lastAnimateAt: number\n disposed: boolean\n wasExiting: boolean\n isExiting: boolean\n sendExitComplete: (() => void) | null | undefined\n animationState: 'enter' | 'exit' | 'default'\n frozenExitTarget: Record<string, unknown> | null\n exitCompleteScheduled: boolean\n wasEntering: boolean\n wasDisabled: boolean\n}\n\nexport function createAnimations<A extends Record<string, AnimationConfig>>(\n animations: A\n): AnimationDriver<A> {\n let isHydratingGlobal: boolean | undefined\n const hydratingComponents = new Set<Function>()\n\n return {\n View: MotionView,\n Text: MotionText,\n isReactNative: false,\n inputStyle: 'css',\n outputStyle: 'inline',\n avoidReRenders: true,\n animations,\n usePresence,\n ResetPresence,\n\n onMount() {\n isHydratingGlobal = false\n hydratingComponents.forEach((cb) => cb())\n },\n\n useAnimations: (animationProps) => {\n if (isHydratingGlobal === undefined && !getConfig().settings.disableSSR) {\n isHydratingGlobal = true\n }\n\n const { props, style, componentState, stateRef, useStyleEmitter, presence } =\n animationProps\n\n const animationKey = Array.isArray(props.transition)\n ? props.transition[0]\n : props.transition\n\n const isComponentHydrating = componentState.unmounted === true\n const isMounting = componentState.unmounted === 'should-enter'\n const isEntering = !!componentState.unmounted\n const isExiting = presence?.[0] === false\n const sendExitComplete = presence?.[1]\n\n // single consolidated ref with lazy init\n const refs = useRef<MotionRefs>(null!)\n if (!refs.current) {\n refs.current = {\n isFirstRender: true,\n lastDoAnimate: null,\n lastDontAnimate: null,\n controls: null,\n lastAnimateAt: 0,\n disposed: false,\n wasExiting: false,\n isExiting: false,\n sendExitComplete: undefined,\n animationState: 'default',\n frozenExitTarget: null,\n exitCompleteScheduled: false,\n wasEntering: false,\n wasDisabled: false,\n }\n }\n\n // track entering state transitions\n const justFinishedEntering = refs.current.wasEntering && !isEntering\n useEffect(() => {\n refs.current.wasEntering = isEntering\n })\n\n // determine animation state for enter/exit transitions\n const animationState: 'enter' | 'exit' | 'default' = isExiting\n ? 'exit'\n : isMounting || justFinishedEntering\n ? 'enter'\n : 'default'\n\n // disable animation during hydration and mounting (prevents \"flying across the page\")\n const disableAnimation = isComponentHydrating || isMounting || !animationKey\n\n const [scope, animate] = useAnimateSSRSafe()\n\n // sync ref values for reliable access from callbacks\n refs.current.isExiting = isExiting\n refs.current.sendExitComplete = sendExitComplete\n refs.current.animationState = animationState\n\n // detect transition into exiting state\n const justStartedExiting = isExiting && !refs.current.wasExiting\n const justStoppedExiting = !isExiting && refs.current.wasExiting\n\n // freeze exit animation target so direction changes don't reverse mid-exit\n if (justStartedExiting || justStoppedExiting) {\n refs.current.frozenExitTarget = null\n refs.current.exitCompleteScheduled = false\n }\n\n // track previous exiting state\n useEffect(() => {\n refs.current.wasExiting = isExiting\n })\n\n const {\n dontAnimate = {},\n doAnimate,\n animationOptions,\n } = getMotionAnimatedProps(props as any, style, disableAnimation, animationState)\n\n const [firstRenderStyle] = useState(style)\n\n // avoid first render returning wrong styles - always render all, after that we can just mutate\n if (refs.current.isFirstRender) {\n refs.current.lastDontAnimate = firstRenderStyle\n }\n const [isHydrating, setIsHydrating] = useState(isHydratingGlobal)\n\n useLayoutEffect(() => {\n if (isHydratingGlobal) {\n hydratingComponents.add(() => {\n setIsHydrating(false)\n })\n }\n return () => {\n refs.current.disposed = true\n }\n }, [])\n\n const flushAnimation = ({\n doAnimate: doAnimateRaw = {},\n animationOptions: passedOptions = {},\n dontAnimate,\n }: AnimationProps) => {\n // track whether THIS flush starts a new animation (vs using stale controls)\n let startedControls: AnimationPlaybackControlsWithThen | null = null\n\n // read current state from refs (closure variables can be stale)\n const isCurrentlyExiting = refs.current.isExiting\n const currentSendExitComplete = refs.current.sendExitComplete\n\n // freeze exit target: once the first exit animation starts, subsequent\n // renders (e.g. direction change) should not reverse the exit animation.\n let doAnimate = doAnimateRaw\n if (isCurrentlyExiting && refs.current.frozenExitTarget) {\n doAnimate = refs.current.frozenExitTarget\n }\n\n // only recompute animation options for exit animations to avoid stale state.\n const animationOptions =\n isCurrentlyExiting && currentSendExitComplete\n ? getAnimationOptions(props.transition ?? null, 'exit')\n : passedOptions\n\n try {\n const node = stateRef.current.host\n\n // on first render, reset stale animation refs - they can persist if component\n // instance is reused (e.g. AnimatePresence keepChildrenMounted)\n if (refs.current.isFirstRender) {\n refs.current.lastDontAnimate = null\n refs.current.lastDoAnimate = null\n }\n\n if (process.env.NODE_ENV === 'development') {\n if (props['debug'] && props['debug'] !== 'profile') {\n console.groupCollapsed(\n `[motion] animate (${JSON.stringify(getDiff(refs.current.lastDoAnimate, doAnimate), null, 2)})`\n )\n console.info({\n props,\n componentState,\n doAnimate,\n dontAnimate,\n animationOptions,\n animationProps,\n lastDoAnimate: { ...refs.current.lastDoAnimate },\n lastDontAnimate: { ...refs.current.lastDontAnimate },\n isExiting,\n style,\n node,\n })\n console.groupCollapsed(`trace >`)\n console.trace()\n console.groupEnd()\n console.groupEnd()\n }\n }\n\n if (!(node instanceof HTMLElement)) {\n return\n }\n\n // handle case where dontAnimate changes\n const prevDont = refs.current.lastDontAnimate\n if (dontAnimate) {\n if (prevDont) {\n removeRemovedStyles(prevDont, dontAnimate, node, doAnimate)\n const changed = getDiff(prevDont, dontAnimate)\n if (changed) {\n Object.assign(node.style, changed as any)\n }\n } else {\n Object.assign(node.style, dontAnimate as any)\n }\n }\n\n if (doAnimate) {\n // when a property moves from dontAnimate to doAnimate, preserve\n // the current inline style value so WAAPI starts from the right place\n if (prevDont) {\n for (const key in prevDont) {\n if (key in doAnimate) {\n node.style[key] = prevDont[key]\n if (refs.current.lastDoAnimate) {\n refs.current.lastDoAnimate[key] = prevDont[key]\n }\n }\n }\n }\n\n const lastAnimated = refs.current.lastDoAnimate\n if (lastAnimated) {\n removeRemovedStyles(lastAnimated, doAnimate, node, dontAnimate)\n }\n\n const diff = getDiff(refs.current.lastDoAnimate, doAnimate)\n\n if (diff) {\n // capture frozen exit target on first exit diff\n if (isCurrentlyExiting && !refs.current.frozenExitTarget) {\n refs.current.frozenExitTarget = { ...doAnimate }\n }\n\n // capture mid-flight values so we can provide explicit [from, to]\n // keyframes to WAAPI, ensuring smooth interpolation from the\n // current visual state.\n //\n // only stop() during exit — for non-exit cases, WAAPI\n // naturally replaces only conflicting property animations,\n // letting non-conflicting ones (like an in-flight enter\n // opacity animation) continue to completion.\n const isPopperPosition = node.hasAttribute('data-popper-animate-position')\n let midFlightValues: Record<string, string> | null = null\n if (refs.current.controls) {\n try {\n const computed = getComputedStyle(node)\n midFlightValues = {}\n for (const key in diff) {\n const val = (computed as any)[key]\n if (val !== undefined && val !== '') {\n midFlightValues[key] = val\n }\n }\n } catch {\n // getComputedStyle can fail on detached nodes\n }\n\n if (isCurrentlyExiting) {\n refs.current.controls.stop()\n }\n\n // write mid-flight values to inline so the 1-frame gap\n // (while motion resolves keyframes) shows the correct\n // position instead of stale inline styles\n if (midFlightValues) {\n for (const key in midFlightValues) {\n ;(node.style as any)[key] = midFlightValues[key]\n }\n }\n\n // for popper position elements, cancel WAAPI animations\n // directly so motion.dev's internal stop() sees \"idle\" state\n // and skips commitStyles. without this, commitStyles writes\n // a mid-flight transform that's visible for 1 frame before\n // the new animation starts, causing a flash toward (0,0).\n if (isPopperPosition) {\n const anims = node.getAnimations()\n for (const anim of anims) {\n anim.cancel()\n }\n }\n }\n\n const fixedDiff = fixTransparentColors(\n diff,\n refs.current.lastDoAnimate,\n doAnimate\n )\n\n // provide explicit [from, to] keyframe for transforms during\n // mid-flight interruption so motion starts from the right place\n if (midFlightValues?.transform && fixedDiff.transform) {\n fixedDiff.transform = [midFlightValues.transform, fixedDiff.transform]\n }\n\n startedControls = animate(scope.current, fixedDiff, animationOptions)\n refs.current.controls = startedControls\n refs.current.lastAnimateAt = Date.now()\n }\n }\n\n refs.current.lastDontAnimate = dontAnimate ? { ...dontAnimate } : {}\n refs.current.lastDoAnimate = doAnimate ? { ...doAnimate } : {}\n } finally {\n // exit completion: notify AnimatePresence when exit animation finishes\n if (isCurrentlyExiting && currentSendExitComplete) {\n if (startedControls) {\n // new animation started — attach completion handler\n refs.current.exitCompleteScheduled = true\n startedControls.finished\n .then(() => {\n // guard: only complete if still exiting (prevents stale promise\n // from calling sendExitComplete after a re-entry cancels the exit)\n if (refs.current.isExiting) {\n currentSendExitComplete()\n }\n })\n .catch(() => {\n if (refs.current.isExiting) {\n currentSendExitComplete()\n }\n })\n } else if (!refs.current.exitCompleteScheduled) {\n // no animation started AND none previously scheduled (e.g. diff=null\n // on re-render mid-exit because frozenExitTarget matches lastDoAnimate)\n // — complete immediately only if we've never started an exit animation\n currentSendExitComplete()\n }\n // else: exit animation already scheduled via a previous flush,\n // its .finished promise will call sendExitComplete when done\n }\n }\n }\n\n useStyleEmitter?.((nextStyle, effectiveTransition) => {\n const animationProps = getMotionAnimatedProps(\n props as any,\n nextStyle,\n disableAnimation,\n refs.current.animationState,\n effectiveTransition\n )\n\n flushAnimation(animationProps)\n })\n\n useIsomorphicLayoutEffect(() => {\n if (refs.current.isFirstRender) {\n refs.current.isFirstRender = false\n refs.current.wasDisabled = disableAnimation\n\n // during hydration, skip inline style writes entirely — SSR CSS\n // already has the correct values. writing them again as inline\n // styles triggers browser style recalc that causes visible font\n // flashes (fontWeight, fontSize, letterSpacing, lineHeight).\n // we only need to track refs for future animation diffing.\n if (isHydrating) {\n if (doAnimate && Object.keys(doAnimate).length > 0) {\n refs.current.lastDoAnimate = { ...doAnimate }\n } else {\n refs.current.lastDoAnimate = dontAnimate ? { ...dontAnimate } : {}\n }\n\n refs.current.lastDontAnimate = dontAnimate ? { ...dontAnimate } : {}\n refs.current.lastAnimateAt = Date.now()\n return\n }\n\n // after hydration, use simpler logic\n refs.current.lastDontAnimate = dontAnimate ? { ...dontAnimate } : {}\n refs.current.lastDoAnimate = doAnimate ? { ...doAnimate } : {}\n return\n }\n\n // when animations first turn on after the mount/hydration handoff, the\n // element is already at its resting position (SSR atomic class, or the\n // dontAnimate inline styles). animating now would spring from the lost\n // \"from\" value — which for a transform reads as 0 and flashes the\n // element across the screen (e.g. progress bar flashing full, #4011).\n // jump straight to the resolved styles instead, so it renders at the\n // right place with no enter animation. only real changes after this\n // animate. components with an explicit enter animation still animate.\n const justEnabled = refs.current.wasDisabled && !disableAnimation\n refs.current.wasDisabled = disableAnimation\n if (justEnabled && animationState !== 'enter') {\n const node = stateRef.current.host\n if (node instanceof HTMLElement) {\n if (dontAnimate) Object.assign(node.style, dontAnimate)\n if (doAnimate) Object.assign(node.style, doAnimate)\n }\n refs.current.lastDontAnimate = dontAnimate ? { ...dontAnimate } : {}\n refs.current.lastDoAnimate = doAnimate ? { ...doAnimate } : {}\n return\n }\n\n flushAnimation({\n doAnimate,\n dontAnimate,\n animationOptions,\n })\n }, [style, isExiting, disableAnimation])\n\n if (process.env.NODE_ENV === 'development') {\n if (props['debug'] && props['debug'] !== 'profile') {\n console.groupCollapsed(`[motion] render`)\n console.info({\n style,\n doAnimate,\n dontAnimate,\n scope,\n animationOptions,\n isExiting,\n isFirstRender: refs.current.isFirstRender,\n animationProps,\n })\n console.groupEnd()\n }\n }\n\n return {\n style: firstRenderStyle,\n ref: scope,\n render: 'div',\n }\n },\n\n useAnimatedNumber(initial): UniversalAnimatedNumber<MotionAnimatedNumber> {\n const motionValue = useMotionValue(initial)\n\n return React.useMemo(\n () => ({\n getInstance() {\n return motionValue\n },\n getValue() {\n return motionValue.get()\n },\n setValue(next, config = { type: 'spring' }, onFinish) {\n if (config.type === 'direct') {\n MotionValueStrategy.set(motionValue, { type: 'direct' })\n motionValue.set(next)\n onFinish?.()\n return\n }\n\n MotionValueStrategy.set(motionValue, config)\n\n // we intentionally DO NOT animate the motion value itself here\n // (via framer-motion's imperative animate(motionValue, next)).\n // doing so drives the JS value over time, which fires a 'change'\n // event per frame, and each change event kicks off a new DOM\n // animate(node, ...) that cancels the previous one — the DOM\n // never reaches the target (double-animation stall).\n //\n // instead we jump the motion value to `next` synchronously. the\n // animated component's change handler receives a single change\n // event, computes the final webStyle, and drives the visible\n // animation via DOM animate(node, webStyle, springConfig). that\n // DOM animation is the real timing source.\n //\n // to make `onFinish` resolve when the VISIBLE animation finishes\n // (not synchronously on the change event), we stash it in\n // PendingMotionOnFinish here and the change handler chains it to\n // the DOM animate() controls.\n if (onFinish) {\n // if a previous setValue is still pending on this motion value,\n // fire it now — the new setValue will cancel the prior DOM\n // animation, and the caller is still owed a completion signal.\n const prior = PendingMotionOnFinish.get(motionValue)\n if (prior) {\n PendingMotionOnFinish.delete(motionValue)\n prior()\n }\n PendingMotionOnFinish.set(motionValue, onFinish)\n }\n\n motionValue.set(next)\n },\n stop() {\n motionValue.stop()\n },\n }),\n [motionValue]\n )\n },\n\n useAnimatedNumberReaction({ value }, onValue) {\n const instance = value.getInstance() as MotionValue<number>\n useMotionValueEvent(instance, 'change', onValue)\n },\n\n useAnimatedNumberStyle(val, getStyleProp) {\n const motionValue = val.getInstance() as MotionValue<number>\n const getStyleRef = useRef<typeof getStyleProp>(getStyleProp)\n\n // we need to change useAnimatedNumberStyle to have dep args to be concurrent safe\n getStyleRef.current = getStyleProp\n\n return useMemo(() => {\n return {\n getStyle: (cur) => {\n return getStyleRef.current(cur)\n },\n motionValue,\n } satisfies MotionAnimatedNumberStyle\n }, [])\n },\n\n useAnimatedNumbersStyle(vals, getStyleProp) {\n const motionValues = vals.map((v) => v.getInstance() as MotionValue<number>)\n const getStyleRef = useRef<typeof getStyleProp>(getStyleProp)\n getStyleRef.current = getStyleProp\n\n return useMemo(() => {\n return {\n getStyle: (...currentValues: number[]) => getStyleRef.current(...currentValues),\n motionValues,\n } satisfies MotionAnimatedNumberStyle\n }, [])\n },\n }\n\n function getMotionAnimatedProps(\n props: { transition?: TransitionProp | null; animateOnly?: string[] },\n style: Record<string, unknown>,\n disable: boolean,\n animationState: 'enter' | 'exit' | 'default' = 'default',\n transitionOverride?: TransitionProp | null\n ): AnimationProps {\n if (disable) {\n return {\n dontAnimate: style,\n }\n }\n\n const animationOptions = getAnimationOptions(\n transitionOverride ?? props.transition ?? null,\n animationState\n )\n\n let dontAnimate: Record<string, unknown> | undefined\n let doAnimate: Record<string, unknown> | undefined\n\n const animateOnly = props.animateOnly as string[] | undefined\n for (const key in style) {\n const value = style[key]\n if (disableAnimationProps.has(key) || (animateOnly && !animateOnly.includes(key))) {\n dontAnimate ||= {}\n dontAnimate[key] = value\n } else {\n doAnimate ||= {}\n doAnimate[key] = value\n }\n }\n\n return {\n dontAnimate,\n doAnimate,\n animationOptions,\n }\n }\n\n function getAnimationOptions(\n transitionProp: TransitionProp | null,\n animationState: 'enter' | 'exit' | 'default' = 'default'\n ): TransitionAnimationOptions {\n const normalized = normalizeTransition(transitionProp)\n\n let effectiveKey = getEffectiveAnimation(normalized, animationState)\n\n // fallback: if we have enter/exit defined but state is 'default' and no default key,\n // use enter timing as fallback to avoid empty animation options\n if (!effectiveKey && animationState === 'default') {\n effectiveKey = normalized.enter || normalized.exit || null\n }\n\n const globalConfigOverride: Record<string, unknown> | undefined = normalized.config\n ? { ...normalized.config }\n : undefined\n\n if (\n !effectiveKey &&\n Object.keys(normalized.properties).length === 0 &&\n !globalConfigOverride\n ) {\n return {}\n }\n\n const defaultConfig = effectiveKey ? withInferredType(animations[effectiveKey]) : null\n\n const delay = normalized.delay\n\n // framer motion's animate() expects default config at the TOP LEVEL\n const result: TransitionAnimationOptions = {}\n\n if (defaultConfig) {\n Object.assign(result, defaultConfig)\n }\n\n if (globalConfigOverride) {\n Object.assign(result, globalConfigOverride)\n if (\n result.type === undefined &&\n result.duration !== undefined &&\n result.damping === undefined &&\n result.stiffness === undefined &&\n result.mass === undefined\n ) {\n result.type = 'tween'\n }\n }\n\n if (delay) {\n result.delay = delay\n }\n\n if (defaultConfig || globalConfigOverride || delay) {\n result.default = {\n ...defaultConfig,\n ...globalConfigOverride,\n ...(delay ? { delay } : null),\n }\n }\n\n for (const [propName, animationNameOrConfig] of Object.entries(\n normalized.properties\n )) {\n if (typeof animationNameOrConfig === 'string') {\n result[propName] = withInferredType(animations[animationNameOrConfig])\n } else if (animationNameOrConfig && typeof animationNameOrConfig === 'object') {\n const baseConfig = animationNameOrConfig.type\n ? withInferredType(animations[animationNameOrConfig.type])\n : defaultConfig\n\n result[propName] = {\n ...baseConfig,\n ...animationNameOrConfig,\n } as ValueTransition\n }\n }\n\n // we standardize to ms across drivers, motion expects s\n convertMsToS(result as ValueTransition)\n convertMsToS(result.default)\n for (const key in result) {\n if (key !== 'default' && typeof result[key] === 'object') {\n convertMsToS(result[key])\n }\n }\n\n return result\n }\n}\n\nfunction withInferredType(config: AnimationConfig | undefined): AnimationConfig {\n if (!config) {\n return { type: 'spring' }\n }\n const isTimingBased =\n config.duration !== undefined &&\n config.damping === undefined &&\n config.stiffness === undefined &&\n config.mass === undefined\n return { type: isTimingBased ? 'tween' : 'spring', ...config }\n}\n\nfunction convertMsToS(config: ValueTransition | undefined) {\n if (!config) return\n if (typeof config.delay === 'number') config.delay = config.delay / 1000\n if (typeof config.duration === 'number') {\n const isTimingBased =\n config.type === 'tween' ||\n (config.type === undefined &&\n config.damping === undefined &&\n config.stiffness === undefined &&\n config.mass === undefined)\n if (isTimingBased) {\n config.duration = config.duration / 1000\n }\n }\n}\n\nfunction removeRemovedStyles(\n prev: object,\n next: object,\n node: HTMLElement,\n dontClearIfIn?: object\n) {\n for (const key in prev) {\n if (!(key in next)) {\n if (dontClearIfIn && key in dontClearIfIn) {\n continue\n }\n node.style[key] = ''\n }\n }\n}\n\n// truly non-animatable CSS properties (discrete, keyword-based, no interpolation)\n// properties like margin, maxHeight, zIndex, etc are animatable and intentionally excluded\nexport const disableAnimationProps: Set<string> = new Set<string>([\n 'alignContent',\n 'alignItems',\n 'boxSizing',\n 'contain',\n 'containerType',\n 'display',\n 'flexBasis',\n 'flexDirection',\n 'fontFamily',\n 'justifyContent',\n 'overflow',\n 'overflowX',\n 'overflowY',\n 'pointerEvents',\n 'position',\n 'textWrap',\n 'userSelect',\n])\n\nconst MotionView = createMotionView('div')\nconst MotionText = createMotionView('span')\n\nfunction createMotionView(defaultTag: string) {\n const isText = defaultTag === 'span'\n\n const Component = forwardRef((propsIn: any, ref) => {\n const { forwardedRef, animation, render = defaultTag, style, ...propsRest } = propsIn\n const [scope, animate] = useAnimateSSRSafe()\n const hostRef = useRef<HTMLElement>(null)\n const composedRefs = useComposedRefs(forwardedRef, ref, hostRef, scope)\n\n const stateRef = useRef<any>(null)\n if (!stateRef.current) {\n stateRef.current = {\n get host() {\n return hostRef.current\n },\n }\n }\n\n const [_, state] = useThemeWithState({})\n\n const styles = Array.isArray(style) ? style : [style]\n\n const [animatedStyle, nonAnimatedStyles] = (() => {\n let animatedStyle: MotionAnimatedNumberStyle | undefined\n const nonAnimatedStyles: typeof styles = []\n for (const style of styles) {\n if (style.getStyle) {\n animatedStyle = style as MotionAnimatedNumberStyle\n } else {\n nonAnimatedStyles.push(style)\n }\n }\n return [animatedStyle, nonAnimatedStyles] as const\n })()\n\n function getProps(props: any) {\n const out = getSplitStyles(\n props,\n isText ? Text.staticConfig : View.staticConfig,\n state?.theme,\n state?.name,\n {\n unmounted: false,\n },\n {\n isAnimated: false,\n noClass: true,\n resolveValues: 'auto',\n }\n )\n\n if (!out) {\n return {}\n }\n\n if (out.viewProps.style) {\n fixStyles(out.viewProps.style)\n styleToCSS(out.viewProps.style)\n }\n\n return out.viewProps\n }\n\n const props = getProps({ ...propsRest, style: nonAnimatedStyles })\n const Element = render || 'div'\n const transformedProps = hooks.usePropsTransform?.(render, props, stateRef, false)\n\n useEffect(() => {\n if (!animatedStyle) return\n\n // multi-value path: subscribe to all motion values\n if (animatedStyle.motionValues) {\n const mvs = animatedStyle.motionValues\n const unsubs = mvs.map((mv) =>\n mv.on('change', () => {\n const currentValues = mvs.map((v) => v.get())\n const nextStyle = animatedStyle.getStyle(...currentValues)\n const animationConfig = MotionValueStrategy.get(mv)\n const node = hostRef.current\n\n const webStyle = getProps({ style: nextStyle }).style\n\n if (webStyle && node instanceof HTMLElement) {\n const motionAnimationConfig =\n animationConfig?.type === 'timing'\n ? { type: 'tween', duration: (animationConfig?.duration || 0) / 1000 }\n : animationConfig?.type === 'direct'\n ? { type: 'tween', duration: 0 }\n : { type: 'spring', ...(animationConfig as any) }\n\n const controls = animate(node, webStyle as any, motionAnimationConfig)\n settlePendingMotionOnFinish(mv, controls)\n }\n })\n )\n return () => unsubs.forEach((fn) => fn())\n }\n\n // single-value path\n if (!animatedStyle.motionValue) return\n\n return animatedStyle.motionValue.on('change', (value) => {\n const nextStyle = animatedStyle.getStyle(value)\n const animationConfig = MotionValueStrategy.get(animatedStyle.motionValue!)\n const node = hostRef.current\n\n const webStyle = getProps({ style: nextStyle }).style\n\n if (webStyle && node instanceof HTMLElement) {\n const motionAnimationConfig =\n animationConfig?.type === 'timing'\n ? {\n type: 'tween',\n duration: (animationConfig?.duration || 0) / 1000,\n }\n : animationConfig?.type === 'direct'\n ? { type: 'tween', duration: 0 }\n : {\n type: 'spring',\n ...(animationConfig as any),\n }\n\n const controls = animate(node, webStyle as any, motionAnimationConfig)\n settlePendingMotionOnFinish(animatedStyle.motionValue!, controls)\n }\n })\n }, [animatedStyle])\n\n return <Element {...transformedProps} ref={composedRefs} />\n })\n\n Component['acceptRenderProp'] = true\n\n return Component\n}\n\nfunction getDiff<T extends Record<string, unknown>>(\n previous: T | null,\n next: T\n): Record<string, unknown> | null {\n if (!previous) {\n return next\n }\n\n let diff: Record<string, unknown> | null = null\n for (const key in next) {\n if (next[key] !== previous[key]) {\n diff ||= {}\n diff[key] = next[key]\n }\n }\n return diff\n}\n\n// motion.dev can't animate to \"transparent\" - convert it to rgba\n// try to extract RGB from previous or next value for smooth color transitions\nfunction fixTransparentColors(\n diff: Record<string, unknown>,\n previous: Record<string, unknown> | null,\n next?: Record<string, unknown> | null\n): Record<string, unknown> {\n let result = diff\n for (const key in diff) {\n if (diff[key] === 'transparent') {\n let fixed = 'rgba(0, 0, 0, 0)'\n const candidates = [previous?.[key], next?.[key]]\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate !== 'transparent') {\n const rgbaMatch = candidate.match(/^rgba?\\(([^,]+),\\s*([^,]+),\\s*([^,)]+)/)\n if (rgbaMatch) {\n fixed = `rgba(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]}, 0)`\n break\n }\n }\n }\n if (result === diff) {\n result = { ...diff }\n }\n result[key] = fixed\n }\n }\n return result\n}\n"
10
+ ]
11
11
  }
@@ -4,8 +4,8 @@
4
4
  "sources": [
5
5
  "src/index.ts"
6
6
  ],
7
+ "version": 3,
7
8
  "sourcesContent": [
8
9
  "import './polyfill'\n\nexport * from './createAnimations'\n"
9
- ],
10
- "version": 3
10
+ ]
11
11
  }
@@ -4,8 +4,8 @@
4
4
  "sources": [
5
5
  "src/index.native.ts"
6
6
  ],
7
+ "version": 3,
7
8
  "sourcesContent": [
8
9
  "// native stub - animations-motion only works on web (uses framer-motion/motion library)\n// on native, use @tamagui/animations-react-native or @tamagui/animations-reanimated\n\nimport type { AnimationDriver } from '@tamagui/web'\n\nlet hasWarnedOnce = false\n\nexport function createAnimations<A extends Record<string, any>>(\n _animations: A\n): AnimationDriver<A> {\n if (process.env.NODE_ENV === 'development') {\n if (!hasWarnedOnce) {\n hasWarnedOnce = true\n console.warn(\n '[@tamagui/animations-motion] This animation driver only works on web. On native, use @tamagui/animations-react-native or @tamagui/animations-reanimated instead.'\n )\n }\n }\n\n // return a noop driver\n // @ts-expect-error its an error anyway\n return {\n isReactNative: false,\n animations: _animations,\n View: undefined as any,\n Text: undefined as any,\n }\n}\n"
9
- ],
10
- "version": 3
10
+ ]
11
11
  }
@@ -4,8 +4,8 @@
4
4
  "sources": [
5
5
  "src/polyfill.ts"
6
6
  ],
7
+ "version": 3,
7
8
  "sourcesContent": [
8
9
  "// for SSR\nif (typeof requestAnimationFrame === 'undefined') {\n globalThis['requestAnimationFrame'] = setTimeout\n}\n"
9
- ],
10
- "version": 3
10
+ ]
11
11
  }
@@ -1,412 +0,0 @@
1
- var __create = Object.create;
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __getProtoOf = Object.getPrototypeOf, __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: !0 });
9
- }, __copyProps = (to, from, except, desc) => {
10
- if (from && typeof from == "object" || typeof from == "function")
11
- for (let key of __getOwnPropNames(from))
12
- !__hasOwnProp.call(to, key) && key !== except && __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
- return to;
14
- };
15
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
16
- // If the importer is in node compatibility mode or this is not an ESM
17
- // file that has been converted to a CommonJS file using a Babel-
18
- // compatible transform (i.e. "__esModule" has not been set), then set
19
- // "default" to the CommonJS "module.exports" for node compatibility.
20
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: !0 }) : target,
21
- mod
22
- )), __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: !0 }), mod);
23
- var createAnimations_exports = {};
24
- __export(createAnimations_exports, {
25
- createAnimations: () => createAnimations
26
- });
27
- module.exports = __toCommonJS(createAnimations_exports);
28
- var import_animation_helpers = require("@tamagui/animation-helpers"), import_use_presence = require("@tamagui/use-presence"), import_web = require("@tamagui/web"), import_react = require("motion/react"), import_react2 = __toESM(require("react"), 1), import_jsx_runtime = require("react/jsx-runtime");
29
- const MotionValueStrategy = /* @__PURE__ */ new WeakMap(), nonPositionTransformRe = /scale|rotate|skew|matrix|perspective/;
30
- function createAnimations(animations) {
31
- let isHydratingGlobal;
32
- const hydratingComponents = /* @__PURE__ */ new Set();
33
- return {
34
- // this is only used by Sheet basically for now to pass result of useAnimatedStyle to
35
- View: MotionView,
36
- Text: MotionText,
37
- isReactNative: !1,
38
- supportsCSS: !0,
39
- inputStyle: "css",
40
- outputStyle: "inline",
41
- needsWebStyles: !0,
42
- avoidReRenders: !0,
43
- animations,
44
- usePresence: import_use_presence.usePresence,
45
- ResetPresence: import_use_presence.ResetPresence,
46
- onMount() {
47
- isHydratingGlobal = !1, hydratingComponents.forEach((cb) => cb());
48
- },
49
- useAnimations: (animationProps) => {
50
- isHydratingGlobal === void 0 && !(0, import_web.getConfig)().settings.disableSSR && (isHydratingGlobal = !0);
51
- const { props, style, componentState, stateRef, useStyleEmitter, presence } = animationProps, animationKey = Array.isArray(props.transition) ? props.transition[0] : props.transition, isComponentHydrating = componentState.unmounted === !0, isMounting = componentState.unmounted === "should-enter", isEntering = !!componentState.unmounted, isExiting = presence?.[0] === !1, sendExitComplete = presence?.[1], wasEnteringRef = (0, import_react2.useRef)(isEntering), justFinishedEntering = wasEnteringRef.current && !isEntering;
52
- (0, import_react2.useEffect)(() => {
53
- wasEnteringRef.current = isEntering;
54
- });
55
- const animationState = isExiting ? "exit" : isMounting || justFinishedEntering ? "enter" : "default", disableAnimation = isComponentHydrating || isMounting || !animationKey, isFirstRender = (0, import_react2.useRef)(!0), [scope, animate] = (0, import_react.useAnimate)(), lastDoAnimate = (0, import_react2.useRef)(null), controls = (0, import_react2.useRef)(null), styleKey = JSON.stringify(style), shouldDebug = (
56
- // process.env.NODE_ENV === 'development' &&
57
- props.debug && props.debug !== "profile"
58
- ), {
59
- dontAnimate = {},
60
- doAnimate,
61
- animationOptions
62
- } = (0, import_react2.useMemo)(() => getMotionAnimatedProps(
63
- props,
64
- style,
65
- disableAnimation,
66
- animationState
67
- ), [isExiting, animationKey, styleKey, animationState, disableAnimation]), id = (0, import_react2.useId)(), debugId = process.env.NODE_ENV === "development" ? id : "", lastAnimateAt = (0, import_react2.useRef)(0), disposed = (0, import_react2.useRef)(!1), [firstRenderStyle] = (0, import_react2.useState)(style), lastDontAnimate = (0, import_react2.useRef)(firstRenderStyle), [isHydrating, setIsHydrating] = (0, import_react2.useState)(isHydratingGlobal);
68
- (0, import_react2.useLayoutEffect)(() => (isHydratingGlobal && hydratingComponents.add(() => {
69
- setIsHydrating(!1);
70
- }), () => {
71
- disposed.current = !0;
72
- }), []);
73
- const flushAnimation = ({
74
- doAnimate: doAnimate2 = {},
75
- animationOptions: animationOptions2 = {},
76
- dontAnimate: dontAnimate2
77
- }) => {
78
- try {
79
- const node = stateRef.current.host;
80
- if (isFirstRender.current && (lastDontAnimate.current = null, lastDoAnimate.current = null), shouldDebug && (console.groupCollapsed(
81
- `[motion] ${debugId} \u{1F30A} animate (${JSON.stringify(getDiff(lastDoAnimate.current, doAnimate2), null, 2)})`
82
- ), console.info({
83
- props,
84
- componentState,
85
- doAnimate: doAnimate2,
86
- dontAnimate: dontAnimate2,
87
- animationOptions: animationOptions2,
88
- animationProps,
89
- lastDoAnimate: { ...lastDoAnimate.current },
90
- lastDontAnimate: { ...lastDontAnimate.current },
91
- isExiting,
92
- style,
93
- node
94
- }), console.groupCollapsed("trace >"), console.trace(), console.groupEnd(), console.groupEnd()), !(node instanceof HTMLElement))
95
- return;
96
- const prevDont = lastDontAnimate.current;
97
- if (dontAnimate2)
98
- if (prevDont) {
99
- removeRemovedStyles(prevDont, dontAnimate2, node, doAnimate2);
100
- const changed = getDiff(prevDont, dontAnimate2);
101
- changed && Object.assign(node.style, changed);
102
- } else
103
- Object.assign(node.style, dontAnimate2);
104
- if (doAnimate2) {
105
- if (prevDont) {
106
- const movedToAnimate = {};
107
- for (const key in prevDont)
108
- key in doAnimate2 && (node.style[key] = prevDont[key], movedToAnimate[key] = prevDont[key], lastDoAnimate.current && (lastDoAnimate.current[key] = prevDont[key]));
109
- Object.keys(movedToAnimate).length > 0 && animate(scope.current, { ...movedToAnimate }, { duration: 0 });
110
- }
111
- const lastAnimated = lastDoAnimate.current;
112
- lastAnimated && removeRemovedStyles(lastAnimated, doAnimate2, node, dontAnimate2);
113
- const diff = getDiff(lastDoAnimate.current, doAnimate2);
114
- if (diff) {
115
- const isRunning = (
116
- /**
117
- * TypeError: Cannot read properties of undefined (reading 'state')
118
- * at GroupAnimationWithThen.getAll (http://localhost:8081/node_modules/.vite/deps/@tamagui_config_v5-motion.js?v=d717d926:2374:30)
119
- * at get state (http://localhost:8081/node_modules/.vite/deps/@tamagui_config_v5-motion.js?v=d717d926:2403:17)
120
- * at flushAnimation (http://localhost:8081/node_modules/.vite/deps/@tamagui_config_v5-motion.js?v=d717d926:9686:49)
121
- **/
122
- // @ts-expect-error it is there, and for some crazy reason in ~/chat pretty often i get errors ^
123
- controls.current?.animations?.length === 0 ? !1 : controls.current?.state === "running"
124
- ), targetTransform = typeof diff.transform == "string" ? diff.transform : null, isPositionOnlyTransform = targetTransform && targetTransform.includes("translate") && !nonPositionTransformRe.test(targetTransform), isPopperElement = node.hasAttribute("data-popper-animate-position"), isEnteringPresenceChild = presence && justFinishedEntering;
125
- if (isRunning && controls.current && isPositionOnlyTransform && (isPopperElement || isEnteringPresenceChild)) {
126
- const currentTransform = getComputedStyle(node).transform;
127
- if (currentTransform && currentTransform !== "none") {
128
- const matrixMatch = currentTransform.match(
129
- /matrix\([^,]+,\s*[^,]+,\s*[^,]+,\s*[^,]+,\s*([^,]+),\s*([^)]+)\)/
130
- );
131
- if (matrixMatch) {
132
- controls.current.stop(), node.style.transform = currentTransform;
133
- const currentX = Number.parseFloat(matrixMatch[1]), currentY = Number.parseFloat(matrixMatch[2]), keyframeDiff = {
134
- ...diff,
135
- transform: [
136
- `translateX(${currentX}px) translateY(${currentY}px)`,
137
- targetTransform
138
- ]
139
- };
140
- controls.current = animate(
141
- scope.current,
142
- keyframeDiff,
143
- animationOptions2
144
- ), lastAnimateAt.current = Date.now(), lastDontAnimate.current = dontAnimate2 ? { ...dontAnimate2 } : {}, lastDoAnimate.current = doAnimate2 ? { ...doAnimate2 } : {};
145
- return;
146
- }
147
- }
148
- }
149
- const fixedDiff = fixTransparentColors({ ...diff }, lastDoAnimate.current);
150
- controls.current = animate(scope.current, fixedDiff, animationOptions2), lastAnimateAt.current = Date.now();
151
- }
152
- }
153
- lastDontAnimate.current = dontAnimate2 ? { ...dontAnimate2 } : {}, lastDoAnimate.current = doAnimate2 ? { ...doAnimate2 } : {};
154
- } finally {
155
- isExiting && (controls.current ? controls.current.finished.then(() => {
156
- sendExitComplete?.();
157
- }) : sendExitComplete?.());
158
- }
159
- };
160
- return useStyleEmitter?.((nextStyle) => {
161
- const animationProps2 = getMotionAnimatedProps(
162
- props,
163
- nextStyle,
164
- disableAnimation,
165
- animationState
166
- );
167
- flushAnimation(animationProps2);
168
- }), (0, import_web.useIsomorphicLayoutEffect)(() => {
169
- if (isFirstRender.current) {
170
- if (isFirstRender.current = !1, isHydrating) {
171
- const node = stateRef.current.host;
172
- node instanceof HTMLElement && (dontAnimate && (Object.assign(node.style, dontAnimate), animate(scope.current, { ...dontAnimate }, { duration: 0 })), doAnimate && Object.keys(doAnimate).length > 0 ? (lastDoAnimate.current = { ...doAnimate }, animate(scope.current, { ...doAnimate }, { duration: 0 })) : lastDoAnimate.current = dontAnimate ? { ...dontAnimate } : {}), lastDontAnimate.current = dontAnimate ? { ...dontAnimate } : {}, lastAnimateAt.current = Date.now();
173
- return;
174
- }
175
- lastDontAnimate.current = dontAnimate ? { ...dontAnimate } : {}, lastDoAnimate.current = doAnimate ? { ...doAnimate } : {};
176
- return;
177
- }
178
- flushAnimation({
179
- doAnimate,
180
- dontAnimate,
181
- animationOptions
182
- });
183
- }, [styleKey, isExiting, disableAnimation]), shouldDebug && (console.groupCollapsed("[motion] \u{1F30A} render"), console.info({
184
- style,
185
- doAnimate,
186
- dontAnimate,
187
- styleKey,
188
- scope,
189
- animationOptions,
190
- isExiting,
191
- isFirstRender: isFirstRender.current,
192
- animationProps
193
- }), console.groupEnd()), {
194
- // we never change this, after first render on
195
- style: firstRenderStyle,
196
- ref: scope,
197
- render: "div"
198
- };
199
- },
200
- useAnimatedNumber(initial) {
201
- const motionValue = (0, import_react.useMotionValue)(initial);
202
- return import_react2.default.useMemo(
203
- () => ({
204
- getInstance() {
205
- return motionValue;
206
- },
207
- getValue() {
208
- return motionValue.get();
209
- },
210
- setValue(next, config = { type: "spring" }, onFinish) {
211
- if (config.type === "direct")
212
- MotionValueStrategy.set(motionValue, {
213
- type: "direct"
214
- }), motionValue.set(next), onFinish?.();
215
- else {
216
- if (MotionValueStrategy.set(motionValue, config), onFinish) {
217
- const unsubscribe = motionValue.on("change", (value) => {
218
- Math.abs(value - next) < 0.01 && (unsubscribe(), onFinish());
219
- });
220
- }
221
- motionValue.set(next);
222
- }
223
- },
224
- stop() {
225
- motionValue.stop();
226
- }
227
- }),
228
- [motionValue]
229
- );
230
- },
231
- useAnimatedNumberReaction({ value }, onValue) {
232
- const instance = value.getInstance();
233
- (0, import_react.useMotionValueEvent)(instance, "change", onValue);
234
- },
235
- useAnimatedNumberStyle(val, getStyleProp) {
236
- const motionValue = val.getInstance(), getStyleRef = (0, import_react2.useRef)(getStyleProp);
237
- return getStyleRef.current = getStyleProp, (0, import_react2.useMemo)(() => ({
238
- getStyle: (cur) => getStyleRef.current(cur),
239
- motionValue
240
- }), []);
241
- }
242
- };
243
- function getMotionAnimatedProps(props, style, disable, animationState = "default") {
244
- if (disable)
245
- return {
246
- dontAnimate: style
247
- };
248
- const animationOptions = getAnimationOptions(props.transition, animationState);
249
- let dontAnimate, doAnimate;
250
- const animateOnly = props.animateOnly;
251
- for (const key in style) {
252
- const value = style[key];
253
- disableAnimationProps.has(key) || animateOnly && !animateOnly.includes(key) ? (dontAnimate ||= {}, dontAnimate[key] = value) : (doAnimate ||= {}, doAnimate[key] = value);
254
- }
255
- return {
256
- dontAnimate,
257
- doAnimate,
258
- animationOptions
259
- };
260
- }
261
- function getAnimationOptions(transitionProp, animationState = "default") {
262
- const normalized = (0, import_animation_helpers.normalizeTransition)(transitionProp), effectiveKey = (0, import_animation_helpers.getEffectiveAnimation)(normalized, animationState);
263
- if (!effectiveKey && Object.keys(normalized.properties).length === 0)
264
- return {};
265
- const defaultConfig = effectiveKey ? withInferredType(animations[effectiveKey]) : null, delay = typeof normalized.delay == "number" ? normalized.delay / 1e3 : void 0;
266
- let globalConfigOverride;
267
- normalized.config && (globalConfigOverride = { ...normalized.config }, typeof normalized.config.duration == "number" && (globalConfigOverride.duration = normalized.config.duration / 1e3));
268
- const result = {};
269
- defaultConfig && Object.assign(result, defaultConfig), globalConfigOverride && Object.assign(result, globalConfigOverride), delay && (result.delay = delay), (defaultConfig || globalConfigOverride || delay) && (result.default = {
270
- ...defaultConfig,
271
- ...globalConfigOverride,
272
- ...delay ? { delay } : null
273
- });
274
- for (const [propName, animationNameOrConfig] of Object.entries(
275
- normalized.properties
276
- ))
277
- if (typeof animationNameOrConfig == "string")
278
- result[propName] = withInferredType(animations[animationNameOrConfig]);
279
- else if (animationNameOrConfig && typeof animationNameOrConfig == "object") {
280
- const baseConfig = animationNameOrConfig.type ? withInferredType(animations[animationNameOrConfig.type]) : defaultConfig;
281
- result[propName] = {
282
- ...baseConfig,
283
- ...animationNameOrConfig
284
- };
285
- }
286
- convertMsToS(result.default);
287
- for (const key in result)
288
- key !== "default" && convertMsToS(result[key]);
289
- return result;
290
- }
291
- }
292
- function withInferredType(config) {
293
- return { type: config.duration !== void 0 && config.damping === void 0 && config.stiffness === void 0 && config.mass === void 0 ? "tween" : "spring", ...config };
294
- }
295
- function convertMsToS(config) {
296
- !config || config.type !== "tween" || (typeof config.duration == "number" && (config.duration = config.duration / 1e3), typeof config.delay == "number" && (config.delay = config.delay / 1e3));
297
- }
298
- function removeRemovedStyles(prev, next, node, dontClearIfIn) {
299
- for (const key in prev)
300
- if (!(key in next)) {
301
- if (dontClearIfIn && key in dontClearIfIn)
302
- continue;
303
- node.style[key] = "";
304
- }
305
- }
306
- const disableAnimationProps = /* @__PURE__ */ new Set([
307
- "alignContent",
308
- "alignItems",
309
- "aspectRatio",
310
- "backdropFilter",
311
- "boxSizing",
312
- "contain",
313
- "containerType",
314
- "display",
315
- "flexBasis",
316
- "flexDirection",
317
- "flexGrow",
318
- "flexShrink",
319
- "fontFamily",
320
- "justifyContent",
321
- "marginBottom",
322
- "marginLeft",
323
- "marginRight",
324
- "marginTop",
325
- "maxHeight",
326
- "maxWidth",
327
- "minHeight",
328
- "minWidth",
329
- "overflow",
330
- "overflowX",
331
- "overflowY",
332
- "pointerEvents",
333
- "position",
334
- "textWrap",
335
- "transformOrigin",
336
- "userSelect",
337
- "WebkitBackdropFilter",
338
- "zIndex"
339
- ]), MotionView = createMotionView("div"), MotionText = createMotionView("span");
340
- function createMotionView(defaultTag) {
341
- const isText = defaultTag === "span", Component = (0, import_react2.forwardRef)((propsIn, ref) => {
342
- const { forwardedRef, animation, render = defaultTag, style, ...propsRest } = propsIn, [scope, animate] = (0, import_react.useAnimate)(), hostRef = (0, import_react2.useRef)(null), composedRefs = (0, import_web.useComposedRefs)(forwardedRef, ref, hostRef, scope), stateRef = (0, import_react2.useRef)(null);
343
- stateRef.current || (stateRef.current = {
344
- get host() {
345
- return hostRef.current;
346
- }
347
- });
348
- const [_, state] = (0, import_web.useThemeWithState)({}), styles = Array.isArray(style) ? style : [style], [animatedStyle, nonAnimatedStyles] = [
349
- styles.find((x) => x.getStyle),
350
- styles.filter((x) => !x.getStyle)
351
- ];
352
- function getProps(props2) {
353
- const out = (0, import_web.getSplitStyles)(
354
- props2,
355
- isText ? import_web.Text.staticConfig : import_web.View.staticConfig,
356
- state?.theme,
357
- state?.name,
358
- {
359
- unmounted: !1
360
- },
361
- {
362
- isAnimated: !1,
363
- noClass: !0,
364
- // noMergeStyle: true,
365
- resolveValues: "auto"
366
- }
367
- );
368
- return out ? (out.viewProps.style && ((0, import_web.fixStyles)(out.viewProps.style), (0, import_web.styleToCSS)(out.viewProps.style)), out.viewProps) : {};
369
- }
370
- const props = getProps({ ...propsRest, style: nonAnimatedStyles }), Element = render || "div", transformedProps = import_web.hooks.usePropsTransform?.(render, props, stateRef, !1);
371
- return (0, import_react2.useEffect)(() => {
372
- if (animatedStyle)
373
- return animatedStyle.motionValue.on("change", (value) => {
374
- const nextStyle = animatedStyle.getStyle(value), animationConfig = MotionValueStrategy.get(animatedStyle.motionValue), node = hostRef.current, webStyle = getProps({ style: nextStyle }).style;
375
- if (webStyle && node instanceof HTMLElement) {
376
- const motionAnimationConfig = animationConfig?.type === "timing" ? {
377
- type: "tween",
378
- duration: (animationConfig?.duration || 0) / 1e3
379
- } : animationConfig?.type === "direct" ? { type: "tween", duration: 0 } : {
380
- type: "spring",
381
- ...animationConfig
382
- };
383
- animate(node, webStyle, motionAnimationConfig);
384
- }
385
- });
386
- }, [animatedStyle]), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Element, { ...transformedProps, ref: composedRefs });
387
- });
388
- return Component.acceptTagProp = !0, Component;
389
- }
390
- function getDiff(previous, next) {
391
- if (!previous)
392
- return next;
393
- let diff = null;
394
- for (const key in next)
395
- next[key] !== previous[key] && (diff ||= {}, diff[key] = next[key]);
396
- return diff;
397
- }
398
- function fixTransparentColors(diff, previous) {
399
- let result = diff;
400
- for (const key in diff)
401
- if (diff[key] === "transparent") {
402
- const prev = previous?.[key];
403
- let fixed = "rgba(0, 0, 0, 0)";
404
- if (typeof prev == "string") {
405
- const rgbaMatch = prev.match(/^rgba?\(([^,]+),\s*([^,]+),\s*([^,)]+)/);
406
- rgbaMatch && (fixed = `rgba(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]}, 0)`);
407
- }
408
- result === diff && (result = { ...diff }), result[key] = fixed;
409
- }
410
- return result;
411
- }
412
- //# sourceMappingURL=createAnimations.js.map