diff --git a/dev/react/src/examples/Animation-transition-out.tsx b/dev/react/src/examples/Animation-transition-out.tsx
new file mode 100644
index 0000000000..22d1b33f89
--- /dev/null
+++ b/dev/react/src/examples/Animation-transition-out.tsx
@@ -0,0 +1,29 @@
+import { motion } from "framer-motion"
+import { useEffect, useState } from "react"
+
+/**
+ * An example of the tween transition type
+ */
+
+const style = {
+ width: 100,
+ height: 100,
+ background: "white",
+}
+export const App = () => {
+ const [state, setState] = useState(false)
+ useEffect(() => {
+ setTimeout(() => {
+ setState(true)
+ }, 300)
+ }, [state])
+
+ return (
+
+ )
+}
diff --git a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts
index 696afff01d..7ef4fcb0cf 100644
--- a/packages/framer-motion/src/animation/interfaces/visual-element-target.ts
+++ b/packages/framer-motion/src/animation/interfaces/visual-element-target.ts
@@ -5,7 +5,7 @@ import type { TargetAndTransition } from "../../types"
import type { VisualElementAnimationOptions } from "./types"
import { animateMotionValue } from "./motion-value"
import { setTarget } from "../../render/utils/setters"
-import { AnimationPlaybackControls } from "../types"
+import { AnimationPlaybackControls, Transition } from "../types"
import { getValueTransition } from "../utils/get-value-transition"
import { frame } from "../../frameloop"
import { getOptimisedAppearId } from "../optimized-appear/get-appear-id"
@@ -63,11 +63,27 @@ export function animateTarget(
continue
}
- const valueTransition = {
+ let valueTransition = {
delay,
...getValueTransition(transition || {}, key),
}
+ let outTransition: Transition | undefined
+
+ if (type && value.nextTransition) {
+ outTransition = value.nextTransition
+ }
+
+ value.nextTransition = undefined
+
+ if (valueTransition.out) {
+ value.nextTransition = valueTransition
+ }
+
+ if (outTransition) {
+ valueTransition = outTransition
+ }
+
/**
* If this is the first time a value is being animated, check
* to see if we're handling off from an existing animation.
diff --git a/packages/framer-motion/src/animation/types.ts b/packages/framer-motion/src/animation/types.ts
index b28ff7ba7e..cecbe7f32b 100644
--- a/packages/framer-motion/src/animation/types.ts
+++ b/packages/framer-motion/src/animation/types.ts
@@ -44,6 +44,33 @@ export interface Transition
duration?: number
autoplay?: boolean
startTime?: number
+
+ /**
+ * If set to `true`, when the animated value leaves its current
+ * state, this transition will be used.
+ *
+ * For example:
+ *
+ * ```jsx
+ *
+ * ```
+ *
+ * Because `whileHover` has a `transition` where `out` is `true`,
+ * when the hover ends, the `duration: 1` transition will be used,
+ * with no `delay`.
+ *
+ * @default false
+ */
+ out?: boolean
}
export interface ValueAnimationTransition
diff --git a/packages/framer-motion/src/motion/__tests__/transition-out.test.tsx b/packages/framer-motion/src/motion/__tests__/transition-out.test.tsx
new file mode 100644
index 0000000000..c79005d575
--- /dev/null
+++ b/packages/framer-motion/src/motion/__tests__/transition-out.test.tsx
@@ -0,0 +1,100 @@
+import {
+ render,
+ pointerDown,
+ pointerEnter,
+ pointerLeave,
+ pointerUp,
+} from "../../../jest.setup"
+import { motion, motionValue } from "../../"
+import { nextFrame } from "../../gestures/__tests__/utils"
+
+describe("transition.out", () => {
+ it("uses whileHover transition when exiting hover state", async () => {
+ const opacity = motionValue(0)
+
+ const { container } = render(
+
+ )
+
+ // Enter hover
+ pointerEnter(container.firstChild as Element)
+
+ await nextFrame()
+ await nextFrame()
+
+ expect(opacity.get()).toBe(1)
+
+ // Exit hover - should use whileHover transition with type: false
+ pointerLeave(container.firstChild as Element)
+
+ // Wait for animation to complete
+ await nextFrame()
+ await nextFrame()
+
+ expect(opacity.get()).toBe(0)
+ })
+
+ it("uses whileTap out transition when tap ends before hover", async () => {
+ const scale = motionValue(1)
+
+ const { container } = render(
+
+ )
+
+ // Enter hover
+ pointerEnter(container.firstChild as Element)
+
+ await nextFrame()
+
+ // Start tap
+ pointerDown(container.firstChild as Element)
+
+ await nextFrame()
+ await nextFrame()
+
+ expect(scale.get()).toBe(0.9)
+
+ // End tap before ending hover
+ pointerUp(container.firstChild as Element)
+
+ await nextFrame()
+
+ // Wait a frame to ensure animation has completed
+ await new Promise((resolve) => setTimeout(resolve, 100))
+
+ // Should use whileTap out transition which is instant
+ expect(scale.get()).toBe(1.1)
+
+ // Leave hover
+ pointerLeave(container.firstChild as Element)
+
+ await new Promise((resolve) => setTimeout(resolve, 100))
+
+ expect(scale.get()).not.toBe(1)
+ })
+})
diff --git a/packages/framer-motion/src/value/index.ts b/packages/framer-motion/src/value/index.ts
index fbe37990ab..e9941b4502 100644
--- a/packages/framer-motion/src/value/index.ts
+++ b/packages/framer-motion/src/value/index.ts
@@ -2,7 +2,7 @@ import { frame } from "../frameloop"
import { SubscriptionManager } from "../utils/subscription-manager"
import { velocityPerSecond } from "../utils/velocity-per-second"
import { warnOnce } from "../utils/warn-once"
-import { AnimationPlaybackControls } from "../animation/types"
+import { AnimationPlaybackControls, Transition } from "../animation/types"
import { time } from "../frameloop/sync-time"
export type Transformer = (v: T) => T
@@ -99,6 +99,12 @@ export class MotionValue {
*/
prevUpdatedAt: number | undefined
+ /**
+ * A transition that should be applied to the next animation.
+ * This is used to handle the `out` transition option.
+ */
+ nextTransition?: Transition
+
/**
* Add a passive effect to this `MotionValue`.
*