@tamagui/animations-motion 2.0.0-1768636514428 → 2.0.0-1768696252732

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.
@@ -1,11 +1,11 @@
1
1
  {
2
- "mappings": "AACA,cAEO,uBAYA;AAEP,cAOO,uBACA;KAYF,kBAAkB;AA0BvB,OAAO,iBAAS,iBAAiB,UAAU,eAAe,kBACxD,gBAAgB,IACf,gBAAgB",
2
+ "mappings": "AACA,cAEO,uBAYA;AAEP,cAOO,uBACA;KAYF,kBAAkB;AAwBvB,OAAO,iBAAS,iBAAiB,UAAU,eAAe,kBACxD,gBAAgB,IACf,gBAAgB",
3
3
  "names": [],
4
4
  "sources": [
5
5
  "src/createAnimations.tsx"
6
6
  ],
7
7
  "sourcesContent": [
8
- "import { normalizeTransition } from '@tamagui/animation-helpers'\nimport {\n type AnimatedNumberStrategy,\n type AnimationDriver,\n type TransitionProp,\n fixStyles,\n getSplitStyles,\n hooks,\n styleToCSS,\n Text,\n type UniversalAnimatedNumber,\n useComposedRefs,\n useIsomorphicLayoutEffect,\n useThemeWithState,\n View,\n} from '@tamagui/core'\nimport { ResetPresence, usePresence } from '@tamagui/use-presence'\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 minTimeBetweenAnimations = 1000 / 60\n\nconst MotionValueStrategy = new WeakMap<MotionValue, AnimatedNumberStrategy>()\n\ntype AnimationProps = {\n doAnimate?: Record<string, unknown>\n dontAnimate?: Record<string, unknown>\n animationOptions?: AnimationOptions\n}\n\nexport function createAnimations<A extends Record<string, AnimationConfig>>(\n animationsProp: A\n): AnimationDriver<A> {\n // normalize, it doesn't assume type: 'spring' even if damping etc there so we do that\n // which also matches the moti driver\n // @ts-expect-error avoid doing a spread for no reason, sub-constraint type issue\n const animations: A = {}\n for (const key in animationsProp) {\n animations[key] = {\n type: 'spring',\n ...animationsProp[key],\n }\n }\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 needsWebStyles: true,\n avoidReRenders: true,\n animations,\n usePresence,\n ResetPresence,\n\n useAnimations: (animationProps) => {\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 isHydrating = componentState.unmounted === true\n const disableAnimation = isHydrating || !animationKey\n const isExiting = presence?.[0] === false\n const sendExitComplete = presence?.[1]\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 )\n return motionAnimationState\n }, [isExiting, animationKey, styleKey])\n\n const debugId = process.env.NODE_ENV === 'development' ? useId() : ''\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>>(firstRenderStyle)\n\n useLayoutEffect(() => {\n return () => {\n disposed.current = true\n }\n }, [])\n\n // const runAnimation = (props: AnimationProps) => {\n // const waitForNextAnimationFrame = () => {\n // if (disposed.current) return\n // // we just skip to the last one\n // const queue = animationsQueue.current\n // const last = queue[queue.length - 1]\n // animationsQueue.current = []\n\n // if (!last) {\n // console.error(`Should never hit`)\n // return\n // }\n\n // if (!props) return\n\n // if (scope.current) {\n // flushAnimation(props)\n // } else {\n // // frame.postRender(waitForNextAnimationFrame)\n // requestAnimationFrame(waitForNextAnimationFrame)\n // }\n // }\n\n // const hasQueue = animationsQueue.current.length\n // const shouldWait =\n // hasQueue ||\n // (lastAnimateAt.current &&\n // Date.now() - lastAnimateAt.current > minTimeBetweenAnimations)\n\n // if (isExiting || isFirstRender.current || (scope.current && !shouldWait)) {\n // flushAnimation(props)\n // } else {\n // animationsQueue.current.push(props)\n // if (!hasQueue) {\n // waitForNextAnimationFrame()\n // }\n // }\n // }\n\n const updateFirstAnimationStyle = () => {\n const node = stateRef.current.host\n\n if (!(node instanceof HTMLElement)) {\n return false\n }\n\n if (!lastDoAnimate.current) {\n lastAnimateAt.current = Date.now()\n lastDoAnimate.current = doAnimate || {}\n animate(scope.current, doAnimate || {}, {\n type: false,\n })\n // scope.animations = []\n\n if (shouldDebug) {\n console.groupCollapsed(`[motion] ${debugId} 🌊 FIRST`)\n console.info(doAnimate)\n console.groupEnd()\n }\n return true\n }\n\n return false\n }\n\n const flushAnimation = ({\n doAnimate = {},\n animationOptions = {},\n dontAnimate,\n }: AnimationProps) => {\n try {\n const node = stateRef.current.host\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 removeRemovedStyles(prevDont, dontAnimate, node)\n const changed = getDiff(prevDont, dontAnimate)\n if (changed) {\n Object.assign(node.style, changed as any)\n }\n }\n }\n\n if (doAnimate) {\n if (updateFirstAnimationStyle()) {\n return\n }\n\n // bugfix: going from non-animated to animated in motion -\n // motion batches things so the above removal can happen a frame before casuing 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 if (prevDont) {\n for (const key in prevDont) {\n if (key in doAnimate) {\n node.style[key] = prevDont[key]\n }\n }\n }\n\n const lastAnimated = lastDoAnimate.current\n if (lastAnimated) {\n removeRemovedStyles(lastAnimated, doAnimate, node)\n }\n\n const diff = getDiff(lastDoAnimate.current, doAnimate)\n if (diff) {\n controls.current = animate(scope.current, diff, animationOptions)\n lastAnimateAt.current = Date.now()\n }\n }\n\n lastDontAnimate.current = dontAnimate || {}\n lastDoAnimate.current = 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 )\n\n flushAnimation(animationProps)\n })\n\n const animateKey = JSON.stringify(style)\n\n useIsomorphicLayoutEffect(() => {\n if (isFirstRender.current) {\n updateFirstAnimationStyle()\n isFirstRender.current = false\n lastDontAnimate.current = dontAnimate\n lastDoAnimate.current = doAnimate || {}\n return\n }\n\n // always clear queue if we re-render\n // animationsQueue.current = []\n\n // don't ever queue on a render\n flushAnimation({\n doAnimate,\n dontAnimate,\n animationOptions,\n })\n }, [animateKey, isExiting])\n\n if (shouldDebug) {\n console.groupCollapsed(`[motion] 🌊 render`)\n console.info({\n style,\n doAnimate,\n dontAnimate,\n animateKey,\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 tag: '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 ): AnimationProps {\n if (disable) {\n return {\n dontAnimate: style,\n }\n }\n\n const animationOptions = transitionPropToAnimationConfig(props.transition)\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 // half works in chrome but janky and stops working after first animation\n // if (\n // typeof doAnimate?.opacity !== 'undefined' &&\n // typeof dontAnimate?.backdropFilter === 'string'\n // ) {\n // if (!dontAnimate.backdropFilter.includes('opacity(')) {\n // dontAnimate.backdropFilter += ` opacity(${doAnimate.opacity})`\n // dontAnimate.WebkitBackdropFilter += ` opacity(${doAnimate.opacity})`\n // dontAnimate.transition = 'backdrop-filter ease-in 1000ms'\n // }\n // }\n\n return {\n dontAnimate,\n doAnimate,\n animationOptions,\n }\n }\n\n function transitionPropToAnimationConfig(\n transitionProp: TransitionProp | null\n ): TransitionAnimationOptions {\n const normalized = normalizeTransition(transitionProp)\n\n // If no animation defined, return empty config\n if (!normalized.default && Object.keys(normalized.properties).length === 0) {\n return {}\n }\n\n const defaultConfig = normalized.default ? animations[normalized.default] : 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 // Build the animation options\n const result: TransitionAnimationOptions = {}\n\n // Set default animation config\n if (defaultConfig) {\n result.default = delay ? { ...defaultConfig, delay } : defaultConfig\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] = animations[animationNameOrConfig]\n } else if (animationNameOrConfig && typeof animationNameOrConfig === 'object') {\n const baseConfig = animationNameOrConfig.type\n ? animations[animationNameOrConfig.type]\n : defaultConfig\n result[propName] = {\n ...baseConfig,\n ...animationNameOrConfig,\n }\n }\n }\n\n return result\n }\n}\n\nfunction removeRemovedStyles(prev: Object, next: Object, node: HTMLElement) {\n for (const key in prev) {\n if (!(key in next)) {\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.tag || 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, tag = 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 = tag || 'div'\n const transformedProps = hooks.usePropsTransform?.(tag, 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"
8
+ "import { normalizeTransition } from '@tamagui/animation-helpers'\nimport {\n type AnimatedNumberStrategy,\n type AnimationDriver,\n type TransitionProp,\n fixStyles,\n getSplitStyles,\n hooks,\n styleToCSS,\n Text,\n type UniversalAnimatedNumber,\n useComposedRefs,\n useIsomorphicLayoutEffect,\n useThemeWithState,\n View,\n} from '@tamagui/core'\nimport { ResetPresence, usePresence } from '@tamagui/use-presence'\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\ntype AnimationProps = {\n doAnimate?: Record<string, unknown>\n dontAnimate?: Record<string, unknown>\n animationOptions?: AnimationOptions\n}\n\nexport function createAnimations<A extends Record<string, AnimationConfig>>(\n animationsProp: A\n): AnimationDriver<A> {\n // normalize animation configs\n // @ts-expect-error avoid doing a spread for no reason, sub-constraint type issue\n const animations: A = {}\n for (const key in animationsProp) {\n const config = animationsProp[key]\n // If config only has duration (timing-based), use 'tween' type\n // Otherwise default to 'spring' which matches the moti driver\n const isTimingBased =\n config.duration !== undefined &&\n config.damping === undefined &&\n config.stiffness === undefined &&\n config.mass === undefined\n animations[key] = {\n type: isTimingBased ? 'tween' : 'spring',\n // Convert duration from ms to seconds for motion library\n ...(isTimingBased && config.duration\n ? { ...config, duration: config.duration / 1000 }\n : config),\n }\n }\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 needsWebStyles: true,\n avoidReRenders: true,\n animations,\n usePresence,\n ResetPresence,\n\n useAnimations: (animationProps) => {\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 isHydrating = componentState.unmounted === true\n const isMounting = componentState.unmounted === 'should-enter'\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 = isHydrating || isMounting || !animationKey\n const isExiting = presence?.[0] === false\n const sendExitComplete = presence?.[1]\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 )\n return motionAnimationState\n }, [isExiting, animationKey, styleKey])\n\n const debugId = process.env.NODE_ENV === 'development' ? useId() : ''\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>>(firstRenderStyle)\n\n useLayoutEffect(() => {\n return () => {\n disposed.current = true\n }\n }, [])\n\n // const runAnimation = (props: AnimationProps) => {\n // const waitForNextAnimationFrame = () => {\n // if (disposed.current) return\n // // we just skip to the last one\n // const queue = animationsQueue.current\n // const last = queue[queue.length - 1]\n // animationsQueue.current = []\n\n // if (!last) {\n // console.error(`Should never hit`)\n // return\n // }\n\n // if (!props) return\n\n // if (scope.current) {\n // flushAnimation(props)\n // } else {\n // // frame.postRender(waitForNextAnimationFrame)\n // requestAnimationFrame(waitForNextAnimationFrame)\n // }\n // }\n\n // const hasQueue = animationsQueue.current.length\n // const shouldWait =\n // hasQueue ||\n // (lastAnimateAt.current &&\n // Date.now() - lastAnimateAt.current > minTimeBetweenAnimations)\n\n // if (isExiting || isFirstRender.current || (scope.current && !shouldWait)) {\n // flushAnimation(props)\n // } else {\n // animationsQueue.current.push(props)\n // if (!hasQueue) {\n // waitForNextAnimationFrame()\n // }\n // }\n // }\n\n const updateFirstAnimationStyle = () => {\n const node = stateRef.current.host\n\n if (!(node instanceof HTMLElement)) {\n return false\n }\n\n if (!lastDoAnimate.current) {\n lastAnimateAt.current = Date.now()\n lastDoAnimate.current = doAnimate || {}\n animate(scope.current, doAnimate || {}, {\n type: false,\n })\n // scope.animations = []\n\n if (shouldDebug) {\n console.groupCollapsed(`[motion] ${debugId} 🌊 FIRST`)\n console.info(doAnimate)\n console.groupEnd()\n }\n return true\n }\n\n return false\n }\n\n const flushAnimation = ({\n doAnimate = {},\n animationOptions = {},\n dontAnimate,\n }: AnimationProps) => {\n try {\n const node = stateRef.current.host\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 removeRemovedStyles(prevDont, dontAnimate, node)\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 if (updateFirstAnimationStyle()) {\n return\n }\n\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 if (prevDont) {\n for (const key in prevDont) {\n if (key in doAnimate) {\n node.style[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 }\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 controls.current = animate(scope.current, diff, animationOptions)\n lastAnimateAt.current = Date.now()\n }\n }\n\n lastDontAnimate.current = dontAnimate || {}\n lastDoAnimate.current = 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 )\n\n flushAnimation(animationProps)\n })\n\n const animateKey = JSON.stringify(style)\n\n useIsomorphicLayoutEffect(() => {\n if (isFirstRender.current) {\n updateFirstAnimationStyle()\n isFirstRender.current = false\n lastDontAnimate.current = dontAnimate\n lastDoAnimate.current = doAnimate || {}\n return\n }\n\n // always clear queue if we re-render\n // animationsQueue.current = []\n\n // don't ever queue on a render\n flushAnimation({\n doAnimate,\n dontAnimate,\n animationOptions,\n })\n }, [animateKey, isExiting])\n\n if (shouldDebug) {\n console.groupCollapsed(`[motion] 🌊 render`)\n console.info({\n style,\n doAnimate,\n dontAnimate,\n animateKey,\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 ): AnimationProps {\n if (disable) {\n return {\n dontAnimate: style,\n }\n }\n\n const animationOptions = transitionPropToAnimationConfig(props.transition)\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 // half works in chrome but janky and stops working after first animation\n // if (\n // typeof doAnimate?.opacity !== 'undefined' &&\n // typeof dontAnimate?.backdropFilter === 'string'\n // ) {\n // if (!dontAnimate.backdropFilter.includes('opacity(')) {\n // dontAnimate.backdropFilter += ` opacity(${doAnimate.opacity})`\n // dontAnimate.WebkitBackdropFilter += ` opacity(${doAnimate.opacity})`\n // dontAnimate.transition = 'backdrop-filter ease-in 1000ms'\n // }\n // }\n\n return {\n dontAnimate,\n doAnimate,\n animationOptions,\n }\n }\n\n function transitionPropToAnimationConfig(\n transitionProp: TransitionProp | null\n ): TransitionAnimationOptions {\n const normalized = normalizeTransition(transitionProp)\n\n // If no animation defined, return empty config\n if (!normalized.default && Object.keys(normalized.properties).length === 0) {\n return {}\n }\n\n const defaultConfig = normalized.default ? animations[normalized.default] : 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 // Build the animation options\n const result: TransitionAnimationOptions = {}\n\n // Set default animation config\n if (defaultConfig) {\n result.default = delay ? { ...defaultConfig, delay } : defaultConfig\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] = animations[animationNameOrConfig]\n } else if (animationNameOrConfig && typeof animationNameOrConfig === 'object') {\n const baseConfig = animationNameOrConfig.type\n ? animations[animationNameOrConfig.type]\n : defaultConfig\n result[propName] = {\n ...baseConfig,\n ...animationNameOrConfig,\n }\n }\n }\n\n return result\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 // 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"
9
9
  ],
10
10
  "version": 3
11
11
  }