[go: nahoru, domu]

Introduce Shared Element Transition

Supported:
- Shared content key match, bounds animation
- Derived "visible" state based on AnimatedVisibilityScope
- Rendering in overlay during transition by default
- All animations are added to a (child) transition of
  AnimatedVisiblityScope (if provided) for future seeking
- Bounds animations can be customized via BoundsTransform
- Distinct mental model for different use cases:
  sharedElement vs. sharedBounds
- Error case handling:
     - When there are > 1 targetBounds Providers, use
       the last added provider for bounds
     - When there are 0 targetBounds provider, no
       bounds animation, to fail fast.
- zIndex customization is now supported in overlay
- Clip to shape or clip based on other shared elements'
  bounds
- Skip to lookahead layout for layouts that prefers
  no reflow
- renderInOverlay modifier to render non-shared content
  in overlay to preserve z-order relative to shared
  elements/bounds

RelNote: "
  New experimentals shared element and shared bounds features
await your usage and feedback. These new APIs enable
tagging layouts as shared across layout tree using the
provided modifiers, producing smoothly changing bounds when
one set of shared content exits and the other set enters.
"

Test: New tests added

Change-Id: Icb0b953f1eaff80a582b1edd3f21f9f8031cf8b0
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
index c5494fc..3a99839 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
@@ -55,6 +55,7 @@
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.isActive
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.sync.Mutex
@@ -645,7 +646,7 @@
                                     initialValue = runningAnimation.start,
                                     targetValue = Target1,
                                     initialVelocity =
-                                        runningAnimation.initialVelocity ?: ZeroVelocity
+                                    runningAnimation.initialVelocity ?: ZeroVelocity
                                 )
                             } else if (runningAnimation == null ||
                                 runningAnimation.progressNanos == 0L
@@ -750,21 +751,28 @@
         // The current progress with respect to the animationSpec if it exists or
         // durationNanos if animationSpec is null
         var progressNanos: Long = 0L
+
         // The AnimationSpec used in this animation, or null if it is a linear animation with
         // duration of durationNanos
         var animationSpec: VectorizedAnimationSpec<AnimationVector1D>? = null
+
         // Used by initial value animations to mark when the animation should continue
         var isComplete = false
+
         // The current fraction of the animation
         var value: Float = 0f
+
         // The start value of the animation
         var start: AnimationVector1D = AnimationVector1D(0f)
+
         // The initial velocity of the animation
         var initialVelocity: AnimationVector1D? = null
+
         // The total duration of the transition's animations. This is the totalDurationNanos
         // at the time that this was created for initial value animations. Note that this can
         // be different from the animationSpec's duration.
         var durationNanos: Long = 0L
+
         // The total duration of the animationSpec. This is kept cached because Spring
         // animations can take time to calculate their durations
         var animationSpecDuration: Long = 0L
@@ -773,6 +781,7 @@
     private companion object {
         // AnimationVector1D with 0 value, kept so that we don't have to allocate unnecessarily
         val ZeroVelocity = AnimationVector1D(0f)
+
         // AnimationVector1D with 1 value, used as the target value of 1f
         val Target1 = AnimationVector1D(1f)
     }
@@ -881,7 +890,8 @@
 @Stable
 class Transition<S> internal constructor(
     private val transitionState: TransitionState<S>,
-    private val parentTransition: Transition<*>?,
+    @get:RestrictTo(RestrictTo.Scope.LIBRARY)
+    val parentTransition: Transition<*>?,
     val label: String? = null
 ) {
     @PublishedApi
@@ -1206,12 +1216,7 @@
                     // frame. This is important as this initializes the state.
                     coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
                         val durationScale = coroutineContext.durationScale
-                        withFrameNanos {
-                            if (!isSeeking) {
-                                onFrame(it / AnimationDebugDurationScale, durationScale)
-                            }
-                        }
-                        while (isRunning) {
+                        while (isActive) {
                             withFrameNanos {
                                 // This check is very important, as isSeeking may be changed
                                 // off-band between the last check in composition and this callback
@@ -1334,11 +1339,13 @@
         // Changed during composition, may rollback
         private var targetValue: T by mutableStateOf(initialValue)
 
+        private val defaultSpring = spring<T>()
+
         /**
          * [AnimationSpec] that is used for current animation run. This can change when
          * [targetState] changes.
          */
-        var animationSpec: FiniteAnimationSpec<T> by mutableStateOf(spring())
+        var animationSpec: FiniteAnimationSpec<T> by mutableStateOf(defaultSpring)
             private set
 
         /**
@@ -1726,12 +1733,16 @@
 
 // When a TransitionAnimation doesn't need to be reset
 private const val NoReset = -1f
+
 // When the animation needs to be changed because of a target update
 private const val ResetNoSnap = -2f
+
 // When the animation should be reset to have the same start and end value
 private const val ResetAnimationSnap = -3f
+
 // Snap to the current state and set the initial and target values to the same thing
 private const val ResetAnimationSnapCurrent = -4f
+
 // Snap to the target state and set the initial and target values to the same thing
 private const val ResetAnimationSnapTarget = -5f
 
diff --git a/compose/animation/animation/api/current.txt b/compose/animation/animation/api/current.txt
index c84af4c..f42485a 100644
--- a/compose/animation/animation/api/current.txt
+++ b/compose/animation/animation/api/current.txt
@@ -66,6 +66,10 @@
     method public static androidx.compose.ui.Modifier animateContentSize(androidx.compose.ui.Modifier, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.IntSize,? super androidx.compose.ui.unit.IntSize,kotlin.Unit>? finishedListener);
   }
 
+  @SuppressCompatibility @androidx.compose.animation.ExperimentalSharedTransitionApi public fun interface BoundsTransform {
+    method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.geometry.Rect> transform(androidx.compose.ui.geometry.Rect initialBounds, androidx.compose.ui.geometry.Rect targetBounds);
+  }
+
   public final class ColorVectorConverterKt {
     method public static kotlin.jvm.functions.Function1<androidx.compose.ui.graphics.colorspace.ColorSpace,androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.graphics.Color,androidx.compose.animation.core.AnimationVector4D>> getVectorConverter(androidx.compose.ui.graphics.Color.Companion);
   }
@@ -137,6 +141,55 @@
   @SuppressCompatibility @kotlin.RequiresOptIn(message="This is an experimental animation API.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface ExperimentalAnimationApi {
   }
 
+  @SuppressCompatibility @kotlin.RequiresOptIn(message="This is an experimental shared transition API.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface ExperimentalSharedTransitionApi {
+  }
+
+  @SuppressCompatibility @androidx.compose.animation.ExperimentalSharedTransitionApi @androidx.compose.runtime.Stable public final class SharedTransitionScope implements androidx.compose.ui.layout.LookaheadScope {
+    method public androidx.compose.animation.SharedTransitionScope.OverlayClip OverlayClip(androidx.compose.ui.graphics.Shape clipShape);
+    method public kotlinx.coroutines.CoroutineScope getCoroutineScope();
+    method public boolean isTransitionActive();
+    method @androidx.compose.runtime.Composable public androidx.compose.animation.SharedTransitionScope.SharedContentState rememberSharedContentState(Object key);
+    method public androidx.compose.ui.Modifier renderInSharedTransitionScopeOverlay(androidx.compose.ui.Modifier, optional kotlin.jvm.functions.Function0<java.lang.Boolean> renderInOverlay, optional float zIndexInOverlay, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.LayoutDirection,? super androidx.compose.ui.unit.Density,? extends androidx.compose.ui.graphics.Path?> clipInOverlayDuringTransition);
+    method public androidx.compose.ui.Modifier sharedBounds(androidx.compose.ui.Modifier, androidx.compose.animation.SharedTransitionScope.SharedContentState sharedContentState, androidx.compose.animation.AnimatedVisibilityScope animatedVisibilityScope, optional androidx.compose.animation.EnterTransition enter, optional androidx.compose.animation.ExitTransition exit, optional androidx.compose.animation.BoundsTransform boundsTransform, optional androidx.compose.animation.SharedTransitionScope.PlaceHolderSize placeHolderSize, optional boolean renderInOverlayDuringTransition, optional float zIndexInOverlay, optional androidx.compose.animation.SharedTransitionScope.OverlayClip clipInOverlayDuringTransition);
+    method public androidx.compose.ui.Modifier sharedElement(androidx.compose.ui.Modifier, androidx.compose.animation.SharedTransitionScope.SharedContentState state, androidx.compose.animation.AnimatedVisibilityScope animatedVisibilityScope, optional androidx.compose.animation.BoundsTransform boundsTransform, optional androidx.compose.animation.SharedTransitionScope.PlaceHolderSize placeHolderSize, optional boolean renderInOverlayDuringTransition, optional float zIndexInOverlay, optional androidx.compose.animation.SharedTransitionScope.OverlayClip clipInOverlayDuringTransition);
+    method public androidx.compose.ui.Modifier sharedElementWithCallerManagedVisibility(androidx.compose.ui.Modifier, androidx.compose.animation.SharedTransitionScope.SharedContentState sharedContentState, boolean visible, optional androidx.compose.animation.BoundsTransform boundsTransform, optional androidx.compose.animation.SharedTransitionScope.PlaceHolderSize placeHolderSize, optional boolean renderInOverlayDuringTransition, optional float zIndexInOverlay, optional androidx.compose.animation.SharedTransitionScope.OverlayClip clipInOverlayDuringTransition);
+    method public androidx.compose.ui.Modifier skipToLookaheadSize(androidx.compose.ui.Modifier);
+    property public final kotlinx.coroutines.CoroutineScope coroutineScope;
+    property public final boolean isTransitionActive;
+  }
+
+  public static interface SharedTransitionScope.OverlayClip {
+    method public androidx.compose.ui.graphics.Path? getClipPath(androidx.compose.animation.SharedTransitionScope.SharedContentState state, androidx.compose.ui.geometry.Rect bounds, androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.ui.unit.Density density);
+  }
+
+  public static fun interface SharedTransitionScope.PlaceHolderSize {
+    method public long calculateSize(long contentSize, long animatedSize);
+    field public static final androidx.compose.animation.SharedTransitionScope.PlaceHolderSize.Companion Companion;
+  }
+
+  public static final class SharedTransitionScope.PlaceHolderSize.Companion {
+    method public androidx.compose.animation.SharedTransitionScope.PlaceHolderSize getAnimatedSize();
+    method public androidx.compose.animation.SharedTransitionScope.PlaceHolderSize getContentSize();
+    property public final androidx.compose.animation.SharedTransitionScope.PlaceHolderSize animatedSize;
+    property public final androidx.compose.animation.SharedTransitionScope.PlaceHolderSize contentSize;
+  }
+
+  public static final class SharedTransitionScope.SharedContentState {
+    method public androidx.compose.ui.graphics.Path? getClipPathInOverlay();
+    method public Object getKey();
+    method public androidx.compose.animation.SharedTransitionScope.SharedContentState? getParentSharedContentState();
+    method public boolean isMatchFound();
+    property public final androidx.compose.ui.graphics.Path? clipPathInOverlay;
+    property public final boolean isMatchFound;
+    property public final Object key;
+    property public final androidx.compose.animation.SharedTransitionScope.SharedContentState? parentSharedContentState;
+  }
+
+  public final class SharedTransitionScopeKt {
+    method @SuppressCompatibility @androidx.compose.animation.ExperimentalSharedTransitionApi @androidx.compose.runtime.Composable public static void SharedTransitionLayout(optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.animation.SharedTransitionScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.animation.ExperimentalSharedTransitionApi @androidx.compose.runtime.Composable public static void SharedTransitionScope(kotlin.jvm.functions.Function2<? super androidx.compose.animation.SharedTransitionScope,? super androidx.compose.ui.Modifier,kotlin.Unit> content);
+  }
+
   public final class SingleValueAnimationKt {
     method public static androidx.compose.animation.core.Animatable<androidx.compose.ui.graphics.Color,androidx.compose.animation.core.AnimationVector4D> Animatable(long initialValue);
     method @androidx.compose.runtime.Composable public static androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> animateColorAsState(long targetValue, optional androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.graphics.Color> animationSpec, optional String label, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.Color,kotlin.Unit>? finishedListener);
diff --git a/compose/animation/animation/api/restricted_current.txt b/compose/animation/animation/api/restricted_current.txt
index c84af4c..f42485a 100644
--- a/compose/animation/animation/api/restricted_current.txt
+++ b/compose/animation/animation/api/restricted_current.txt
@@ -66,6 +66,10 @@
     method public static androidx.compose.ui.Modifier animateContentSize(androidx.compose.ui.Modifier, optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.IntSize,? super androidx.compose.ui.unit.IntSize,kotlin.Unit>? finishedListener);
   }
 
+  @SuppressCompatibility @androidx.compose.animation.ExperimentalSharedTransitionApi public fun interface BoundsTransform {
+    method public androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.geometry.Rect> transform(androidx.compose.ui.geometry.Rect initialBounds, androidx.compose.ui.geometry.Rect targetBounds);
+  }
+
   public final class ColorVectorConverterKt {
     method public static kotlin.jvm.functions.Function1<androidx.compose.ui.graphics.colorspace.ColorSpace,androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.graphics.Color,androidx.compose.animation.core.AnimationVector4D>> getVectorConverter(androidx.compose.ui.graphics.Color.Companion);
   }
@@ -137,6 +141,55 @@
   @SuppressCompatibility @kotlin.RequiresOptIn(message="This is an experimental animation API.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface ExperimentalAnimationApi {
   }
 
+  @SuppressCompatibility @kotlin.RequiresOptIn(message="This is an experimental shared transition API.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface ExperimentalSharedTransitionApi {
+  }
+
+  @SuppressCompatibility @androidx.compose.animation.ExperimentalSharedTransitionApi @androidx.compose.runtime.Stable public final class SharedTransitionScope implements androidx.compose.ui.layout.LookaheadScope {
+    method public androidx.compose.animation.SharedTransitionScope.OverlayClip OverlayClip(androidx.compose.ui.graphics.Shape clipShape);
+    method public kotlinx.coroutines.CoroutineScope getCoroutineScope();
+    method public boolean isTransitionActive();
+    method @androidx.compose.runtime.Composable public androidx.compose.animation.SharedTransitionScope.SharedContentState rememberSharedContentState(Object key);
+    method public androidx.compose.ui.Modifier renderInSharedTransitionScopeOverlay(androidx.compose.ui.Modifier, optional kotlin.jvm.functions.Function0<java.lang.Boolean> renderInOverlay, optional float zIndexInOverlay, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.LayoutDirection,? super androidx.compose.ui.unit.Density,? extends androidx.compose.ui.graphics.Path?> clipInOverlayDuringTransition);
+    method public androidx.compose.ui.Modifier sharedBounds(androidx.compose.ui.Modifier, androidx.compose.animation.SharedTransitionScope.SharedContentState sharedContentState, androidx.compose.animation.AnimatedVisibilityScope animatedVisibilityScope, optional androidx.compose.animation.EnterTransition enter, optional androidx.compose.animation.ExitTransition exit, optional androidx.compose.animation.BoundsTransform boundsTransform, optional androidx.compose.animation.SharedTransitionScope.PlaceHolderSize placeHolderSize, optional boolean renderInOverlayDuringTransition, optional float zIndexInOverlay, optional androidx.compose.animation.SharedTransitionScope.OverlayClip clipInOverlayDuringTransition);
+    method public androidx.compose.ui.Modifier sharedElement(androidx.compose.ui.Modifier, androidx.compose.animation.SharedTransitionScope.SharedContentState state, androidx.compose.animation.AnimatedVisibilityScope animatedVisibilityScope, optional androidx.compose.animation.BoundsTransform boundsTransform, optional androidx.compose.animation.SharedTransitionScope.PlaceHolderSize placeHolderSize, optional boolean renderInOverlayDuringTransition, optional float zIndexInOverlay, optional androidx.compose.animation.SharedTransitionScope.OverlayClip clipInOverlayDuringTransition);
+    method public androidx.compose.ui.Modifier sharedElementWithCallerManagedVisibility(androidx.compose.ui.Modifier, androidx.compose.animation.SharedTransitionScope.SharedContentState sharedContentState, boolean visible, optional androidx.compose.animation.BoundsTransform boundsTransform, optional androidx.compose.animation.SharedTransitionScope.PlaceHolderSize placeHolderSize, optional boolean renderInOverlayDuringTransition, optional float zIndexInOverlay, optional androidx.compose.animation.SharedTransitionScope.OverlayClip clipInOverlayDuringTransition);
+    method public androidx.compose.ui.Modifier skipToLookaheadSize(androidx.compose.ui.Modifier);
+    property public final kotlinx.coroutines.CoroutineScope coroutineScope;
+    property public final boolean isTransitionActive;
+  }
+
+  public static interface SharedTransitionScope.OverlayClip {
+    method public androidx.compose.ui.graphics.Path? getClipPath(androidx.compose.animation.SharedTransitionScope.SharedContentState state, androidx.compose.ui.geometry.Rect bounds, androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.ui.unit.Density density);
+  }
+
+  public static fun interface SharedTransitionScope.PlaceHolderSize {
+    method public long calculateSize(long contentSize, long animatedSize);
+    field public static final androidx.compose.animation.SharedTransitionScope.PlaceHolderSize.Companion Companion;
+  }
+
+  public static final class SharedTransitionScope.PlaceHolderSize.Companion {
+    method public androidx.compose.animation.SharedTransitionScope.PlaceHolderSize getAnimatedSize();
+    method public androidx.compose.animation.SharedTransitionScope.PlaceHolderSize getContentSize();
+    property public final androidx.compose.animation.SharedTransitionScope.PlaceHolderSize animatedSize;
+    property public final androidx.compose.animation.SharedTransitionScope.PlaceHolderSize contentSize;
+  }
+
+  public static final class SharedTransitionScope.SharedContentState {
+    method public androidx.compose.ui.graphics.Path? getClipPathInOverlay();
+    method public Object getKey();
+    method public androidx.compose.animation.SharedTransitionScope.SharedContentState? getParentSharedContentState();
+    method public boolean isMatchFound();
+    property public final androidx.compose.ui.graphics.Path? clipPathInOverlay;
+    property public final boolean isMatchFound;
+    property public final Object key;
+    property public final androidx.compose.animation.SharedTransitionScope.SharedContentState? parentSharedContentState;
+  }
+
+  public final class SharedTransitionScopeKt {
+    method @SuppressCompatibility @androidx.compose.animation.ExperimentalSharedTransitionApi @androidx.compose.runtime.Composable public static void SharedTransitionLayout(optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.animation.SharedTransitionScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.animation.ExperimentalSharedTransitionApi @androidx.compose.runtime.Composable public static void SharedTransitionScope(kotlin.jvm.functions.Function2<? super androidx.compose.animation.SharedTransitionScope,? super androidx.compose.ui.Modifier,kotlin.Unit> content);
+  }
+
   public final class SingleValueAnimationKt {
     method public static androidx.compose.animation.core.Animatable<androidx.compose.ui.graphics.Color,androidx.compose.animation.core.AnimationVector4D> Animatable(long initialValue);
     method @androidx.compose.runtime.Composable public static androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> animateColorAsState(long targetValue, optional androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.graphics.Color> animationSpec, optional String label, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.Color,kotlin.Unit>? finishedListener);
diff --git a/compose/animation/animation/build.gradle b/compose/animation/animation/build.gradle
index 9b4a26f..cff8085 100644
--- a/compose/animation/animation/build.gradle
+++ b/compose/animation/animation/build.gradle
@@ -23,6 +23,7 @@
  */
 import androidx.build.LibraryType
 import androidx.build.PlatformIdentifier
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -44,10 +45,11 @@
                 api(project(":compose:animation:animation-core"))
                 api("androidx.compose.foundation:foundation-layout:1.6.0")
                 api(project(":compose:runtime:runtime"))
-                api("androidx.compose.ui:ui:1.6.0")
                 api("androidx.compose.ui:ui-geometry:1.6.0")
 
+                implementation project(':compose:ui:ui')
                 implementation(project(":compose:ui:ui-util"))
+                implementation project(':compose:ui:ui-graphics')
 
                 implementation("androidx.collection:collection:1.4.0")
             }
@@ -127,6 +129,10 @@
     samples(project(":compose:animation:animation:animation-samples"))
 }
 
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
+}
+
 android {
     namespace "androidx.compose.animation"
 }
diff --git a/compose/animation/animation/integration-tests/animation-demos/build.gradle b/compose/animation/animation/integration-tests/animation-demos/build.gradle
index 855469a..a826c38 100644
--- a/compose/animation/animation/integration-tests/animation-demos/build.gradle
+++ b/compose/animation/animation/integration-tests/animation-demos/build.gradle
@@ -31,6 +31,7 @@
     implementation(project(":compose:material:material"))
     implementation(project(":compose:ui:ui-tooling-preview"))
     implementation project(':compose:material3:material3')
+    implementation project(":navigation:navigation-compose")
     debugImplementation(project(":compose:ui:ui-tooling"))
 }
 
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/layoutanimation/AnimateEnterExitDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/layoutanimation/AnimateEnterExitDemo.kt
index 3e6291f..7ee51ee 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/layoutanimation/AnimateEnterExitDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/layoutanimation/AnimateEnterExitDemo.kt
@@ -90,7 +90,7 @@
             ) {
                 Box {
                     Column(Modifier.fillMaxSize()) {
-                        colors.forEachIndexed { index, color ->
+                        summerColors.forEachIndexed { index, color ->
                             // Creates a custom enter/exit animation on scale using
                             // `AnimatedVisibilityScope.transition`
                             val scale by transition.animateFloat { enterExitState ->
@@ -138,7 +138,8 @@
     }
 }
 
-private val colors = listOf(
+@Suppress("PrimitiveInCollection")
+internal val summerColors = listOf(
     Color(0xffff6f69),
     Color(0xffffcc5c),
     Color(0xff2a9d84),
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/CallerManagedVisibilityDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/CallerManagedVisibilityDemo.kt
new file mode 100644
index 0000000..625fd06
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/CallerManagedVisibilityDemo.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalSharedTransitionApi::class)
+
+package androidx.compose.animation.demos.sharedelement
+
+import androidx.compose.animation.BoundsTransform
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionLayout
+import androidx.compose.animation.core.keyframes
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@Preview
+@Composable
+fun SharedElementWithCallerManagedVisibility() {
+    var selectFirst by remember { mutableStateOf(true) }
+    val key = remember { Any() }
+    SharedTransitionLayout(
+        Modifier
+            .fillMaxSize()
+            .padding(10.dp)
+            .clickable {
+                selectFirst = !selectFirst
+            }
+    ) {
+        Box(
+            Modifier
+                .sharedElementWithCallerManagedVisibility(
+                    rememberSharedContentState(key = key),
+                    !selectFirst,
+                    boundsTransform = boundsTransform
+                )
+                .background(Color.Red)
+                .size(100.dp)
+        ) {
+            Text(if (!selectFirst) "false" else "true", color = Color.White)
+        }
+        // TODO: Check isTransitionActive is false in the end. Why are the shared bounds not
+        // two separate entities when transition is finished?
+        Box(
+            Modifier
+                .offset(180.dp, 180.dp)
+                .sharedElementWithCallerManagedVisibility(
+                    rememberSharedContentState(
+                        key = key,
+                    ),
+                    selectFirst,
+                    boundsTransform = boundsTransform
+                )
+                .alpha(0.5f)
+                .background(Color.Blue)
+                .size(180.dp)
+        ) {
+            Text(if (selectFirst) "false" else "true", color = Color.White)
+        }
+    }
+}
+
+private val boundsTransform = BoundsTransform { initial, target ->
+    // Move vertically first then horizontally
+    keyframes {
+        durationMillis = 500
+        initial at 0
+        Rect(initial.left, target.top, initial.left + target.width, target.bottom) at 300
+    }
+}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/ContainerTransformDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/ContainerTransformDemo.kt
new file mode 100644
index 0000000..cd89413
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/ContainerTransformDemo.kt
@@ -0,0 +1,335 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalSharedTransitionApi::class)
+
+package androidx.compose.animation.demos.sharedelement
+
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionLayout
+import androidx.compose.animation.SharedTransitionScope
+import androidx.compose.animation.SharedTransitionScope.PlaceHolderSize.Companion.animatedSize
+import androidx.compose.animation.SizeTransform
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.demos.R
+import androidx.compose.animation.demos.lookahead.SearchBar
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Icon
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Favorite
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.blur
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import kotlinx.coroutines.delay
+
+@Preview
+@Composable
+fun ContainerTransformDemo(model: MyModel = remember { MyModel().apply { selected = items[1] } }) {
+    SharedTransitionLayout {
+        LaunchedEffect(key1 = Unit) {
+            while (true) {
+                delay(2500)
+                if (model.selected == null) {
+                    model.selected = model.items[1]
+                } else {
+                    model.selected = null
+                }
+            }
+        }
+        AnimatedContent(
+            model.selected,
+            transitionSpec = {
+                fadeIn(tween(600)) togetherWith
+                    fadeOut(tween(600)) using SizeTransform { _, _ ->
+                    spring()
+                }
+            },
+            label = ""
+        ) {
+            // TODO: Double check on container transform scrolling
+            if (it != null) {
+                DetailView(model = model, selected = it, model.items[6])
+            } else {
+                GridView(model = model)
+            }
+        }
+    }
+}
+
+context(SharedTransitionScope)
+@Composable
+fun Details(kitty: Kitty) {
+    Column(
+        Modifier
+            .padding(start = 10.dp, end = 10.dp, top = 10.dp)
+            .fillMaxHeight()
+            .wrapContentHeight(Alignment.Top)
+            .fillMaxWidth()
+            .background(Color.White)
+            .padding(start = 10.dp, end = 10.dp)
+    ) {
+        Row(verticalAlignment = Alignment.CenterVertically) {
+            Column {
+                Spacer(Modifier.size(20.dp))
+                Text(
+                    kitty.name,
+                    fontSize = 25.sp,
+                    modifier = Modifier.padding(start = 10.dp)
+                )
+                Text(
+                    kitty.breed,
+                    fontSize = 22.sp,
+                    color = Color.Gray,
+                    modifier = Modifier
+                        .padding(start = 10.dp)
+                )
+                Spacer(Modifier.size(10.dp))
+            }
+            Spacer(Modifier.weight(1f))
+            Icon(
+                Icons.Outlined.Favorite,
+                contentDescription = null,
+                Modifier
+                    .background(Color(0xffffddee), CircleShape)
+                    .padding(10.dp)
+            )
+            Spacer(Modifier.size(10.dp))
+        }
+        Box(
+            modifier = Modifier
+                .padding(bottom = 10.dp)
+                .height(2.dp)
+                .fillMaxWidth()
+                .background(Color(0xffeeeeee))
+        )
+        Text(
+            text =
+            "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent fringilla" +
+                " mollis efficitur. Maecenas sit amet urna eu urna blandit suscipit efficitur" +
+                " eget mauris. Nullam eget aliquet ligula. Nunc id euismod elit. Morbi aliquam" +
+                " enim eros, eget consequat dolor consequat id. Quisque elementum faucibus" +
+                " congue. Curabitur mollis aliquet turpis, ut pellentesque justo eleifend nec.\n" +
+                "\n" +
+                "Suspendisse ac consequat turpis, euismod lacinia quam. Nulla lacinia tellus" +
+                " eu felis tristique ultricies. Vivamus et ultricies dolor. Orci varius" +
+                " natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus." +
+                " Ut gravida porttitor arcu elementum elementum. Phasellus ultrices vel turpis" +
+                " volutpat mollis. Vivamus leo diam, placerat quis leo efficitur, ultrices" +
+                " placerat ex. Nullam mollis et metus ac ultricies. Ut ligula metus, congue" +
+                " gravida metus in, vestibulum posuere velit. Sed et ex nisl. Fusce tempor" +
+                " odio eget sapien pellentesque, sed cursus velit fringilla. Nullam odio" +
+                " ipsum, eleifend non consectetur vitae, congue id libero. Etiam tincidunt" +
+                " mauris at urna dictum ornare.\n" +
+                "\n" +
+                "Etiam at facilisis ex. Sed quis arcu diam. Quisque semper pharetra leo eget" +
+                " fermentum. Nulla dapibus eget mi id porta. Nunc quis sodales nulla, eget" +
+                " commodo sem. Donec lacus enim, pharetra non risus nec, eleifend ultrices" +
+                " augue. Donec sit amet orci porttitor, auctor mauris et, facilisis dolor." +
+                " Nullam mattis luctus orci at pulvinar.\n" +
+                "\n" +
+                "Sed accumsan est massa, ut aliquam nulla dignissim id. Suspendisse in urna" +
+                " condimentum, convallis purus at, molestie nisi. In hac habitasse platea" +
+                " dictumst. Pellentesque id justo quam. Cras iaculis tellus libero, eu" +
+                " feugiat ex pharetra eget. Nunc ultrices, magna ut gravida egestas, mauris" +
+                " justo blandit sapien, eget congue nisi felis congue diam. Mauris at felis" +
+                " vitae erat porta auctor. Pellentesque iaculis sem metus. Phasellus quam" +
+                " neque, congue at est eget, sodales interdum justo. Aenean a pharetra dui." +
+                " Morbi odio nibh, hendrerit vulputate odio eget, sollicitudin egestas ex." +
+                " Fusce nisl ex, fermentum a ultrices id, rhoncus vitae urna. Aliquam quis" +
+                " lobortis turpis.\n" +
+                "\n",
+            modifier = Modifier.skipToLookaheadSize(),
+            color = Color.Gray,
+            fontSize = 15.sp,
+        )
+    }
+}
+
+context(AnimatedVisibilityScope, SharedTransitionScope)
+@Suppress("UNUSED_PARAMETER")
+@Composable
+fun DetailView(
+    model: MyModel,
+    selected: Kitty,
+    next: Kitty?
+) {
+    Column(
+        Modifier
+            .sharedBounds(
+                rememberSharedContentState(key = "container + ${selected.id}"),
+                this@AnimatedVisibilityScope,
+                clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(20.dp))
+            )
+    ) {
+        Row(Modifier.fillMaxHeight(0.5f)) {
+            Image(
+                painter = painterResource(selected.photoResId),
+                contentDescription = null,
+                contentScale = ContentScale.Crop,
+                modifier = Modifier
+                    .padding(10.dp)
+                    .sharedElement(
+                        rememberSharedContentState(key = selected.id),
+                        this@AnimatedVisibilityScope,
+                        placeHolderSize = animatedSize
+                    )
+                    .fillMaxHeight()
+                    .aspectRatio(1f)
+                    .clip(RoundedCornerShape(20.dp))
+            )
+            if (next != null) {
+                Image(
+                    painter = painterResource(next.photoResId),
+                    contentDescription = null,
+                    contentScale = ContentScale.Crop,
+                    modifier = Modifier
+                        .padding(top = 10.dp, bottom = 10.dp, end = 10.dp)
+                        .fillMaxWidth()
+                        .fillMaxHeight()
+                        .clip(RoundedCornerShape(20.dp))
+                        .blur(10.dp)
+                )
+            }
+        }
+        Details(kitty = selected)
+    }
+}
+
+context(AnimatedVisibilityScope, SharedTransitionScope)
+@OptIn(ExperimentalAnimationApi::class)
+@Composable
+fun GridView(model: MyModel) {
+    Box(Modifier.background(lessVibrantPurple)) {
+        Box(
+            Modifier
+                .padding(20.dp)
+                .renderInSharedTransitionScopeOverlay(zIndexInOverlay = 2f)
+                .animateEnterExit(fadeIn(), fadeOut())
+        ) {
+            SearchBar()
+        }
+        LazyVerticalGrid(columns = GridCells.Fixed(2),
+            contentPadding = PaddingValues(top = 90.dp)
+            ) {
+            items(6) {
+                KittyItem(model.items[it])
+            }
+        }
+    }
+}
+
+class MyModel {
+    val items = mutableListOf(
+        Kitty("Waffle", R.drawable.waffle, "American Short Hair", 0),
+        Kitty("油条", R.drawable.yt_profile, "Tabby", 1),
+        Kitty("Cowboy", R.drawable.cowboy, "American Short Hair", 2),
+        Kitty("Pepper", R.drawable.pepper, "Tabby", 3),
+        Kitty("Unknown", R.drawable.question_mark, "Unknown", 4),
+        Kitty("Unknown", R.drawable.question_mark, "Unknown", 5),
+        Kitty("YT", R.drawable.yt_profile2, "Tabby", 6),
+    )
+    var selected: Kitty? by mutableStateOf(null)
+}
+
+context(AnimatedVisibilityScope, SharedTransitionScope)
+@Composable
+fun KittyItem(kitty: Kitty) {
+    Column(
+        Modifier
+            .padding(start = 10.dp, end = 10.dp, bottom = 10.dp)
+            .sharedBounds(
+                rememberSharedContentState(key = "container + ${kitty.id}"),
+                this@AnimatedVisibilityScope
+            )
+            .background(Color.White, RoundedCornerShape(20.dp))
+    ) {
+        Image(
+            painter = painterResource(kitty.photoResId),
+            contentDescription = null,
+            contentScale = ContentScale.Crop,
+            modifier = Modifier
+                .sharedElement(
+                    rememberSharedContentState(key = kitty.id),
+                    this@AnimatedVisibilityScope,
+                    placeHolderSize = animatedSize
+                )
+                .aspectRatio(1f)
+                .clip(RoundedCornerShape(20.dp))
+                .background(Color(0xffaaaaaa))
+        )
+        Spacer(Modifier.size(10.dp))
+        Text(
+            kitty.name,
+            fontSize = 18.sp,
+            modifier = Modifier.padding(start = 10.dp)
+        )
+        Spacer(Modifier.size(5.dp))
+        Text(
+            kitty.breed,
+            fontSize = 15.sp,
+            color = Color.Gray,
+            modifier = Modifier
+                .padding(start = 10.dp)
+        )
+        Spacer(Modifier.size(10.dp))
+    }
+}
+
+data class Kitty(val name: String, val photoResId: Int, val breed: String, val id: Int) {
+    override fun equals(other: Any?): Boolean {
+        return other is Kitty && other.id == id
+    }
+}
+
+private val lessVibrantPurple = Color(0xfff3edf7)
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/ListToDetailsDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/ListToDetailsDemo.kt
new file mode 100644
index 0000000..e3c93c7
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/ListToDetailsDemo.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalSharedTransitionApi::class)
+
+package androidx.compose.animation.demos.sharedelement
+
+import android.annotation.SuppressLint
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionLayout
+import androidx.compose.animation.demos.R
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+sealed class Screen {
+    object List : Screen()
+    data class Details(val item: Int) : Screen()
+}
+
+@SuppressLint("PrimitiveInCollection")
+@Composable
+@Preview
+fun ListToDetailsDemo() {
+    var state by remember {
+        mutableStateOf<Screen>(Screen.List)
+    }
+    val images = listOf(
+        R.drawable.pepper,
+        R.drawable.waffle,
+        R.drawable.yt_profile
+    )
+    SharedTransitionLayout(modifier = Modifier.fillMaxSize()) {
+        AnimatedContent(state, label = "", contentKey = { it.javaClass },
+            transitionSpec = {
+                if (initialState == Screen.List) {
+                    slideInHorizontally { -it } + fadeIn() togetherWith
+                        slideOutHorizontally { it } + fadeOut()
+                } else {
+                    slideInHorizontally { it } + fadeIn() togetherWith
+                        slideOutHorizontally { -it } + fadeOut()
+                }
+            }) {
+            when (it) {
+                Screen.List -> {
+                    LazyColumn {
+                        items(50) { item ->
+                            Row(modifier = Modifier
+                                .clickable(
+                                    interactionSource = remember { MutableInteractionSource() },
+                                    indication = null
+                                ) {
+                                    state = Screen.Details(item)
+                                }
+                                .fillMaxWidth()) {
+                                Image(
+                                    painter = painterResource(images[item % 3]),
+                                    modifier = Modifier
+                                        .size(100.dp)
+                                        .then(
+                                            if (item % 3 < 2) {
+                                                Modifier.sharedElement(
+                                                    rememberSharedContentState(
+                                                        key = "item-image$item"
+                                                    ),
+                                                    this@AnimatedContent,
+                                                )
+                                            } else Modifier
+                                        ),
+                                    contentScale = ContentScale.Crop,
+                                    contentDescription = null
+                                )
+                                Spacer(Modifier.size(15.dp))
+                                Text("Item $item")
+                            }
+                        }
+                    }
+                }
+
+                is Screen.Details -> {
+                    val item = it.item
+                    Column(modifier = Modifier
+                        .fillMaxSize()
+                        .clickable(
+                            interactionSource = remember { MutableInteractionSource() },
+                            indication = null
+                        ) {
+                            state = Screen.List
+                        }) {
+                        Image(
+                            painter = painterResource(images[item % 3]),
+                            modifier = Modifier
+                                .then(
+                                    if (item % 3 < 2) {
+                                        Modifier.sharedElement(
+                                            rememberSharedContentState(key = "item-image$item"),
+                                            this@AnimatedContent,
+                                        )
+                                    } else Modifier
+                                )
+                                .fillMaxWidth(),
+                            contentScale = ContentScale.Crop,
+                            contentDescription = null
+                        )
+                        Text(
+                            "Item $item",
+                            fontSize = 23.sp
+                        )
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/NestedSharedElementDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/NestedSharedElementDemo.kt
new file mode 100644
index 0000000..6b66cce
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/NestedSharedElementDemo.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalSharedTransitionApi::class)
+
+package androidx.compose.animation.demos.sharedelement
+
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.samples.NestedSharedBoundsSample
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.Preview
+
+@Preview
+@Composable
+fun NestedSharedElementDemo() {
+    // Transforming floating "tool bar" into a big edit icon.
+    NestedSharedBoundsSample()
+}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SharedToolBarDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SharedToolBarDemo.kt
new file mode 100644
index 0000000..2faabdf
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SharedToolBarDemo.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalSharedTransitionApi::class)
+
+package androidx.compose.animation.demos.sharedelement
+
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionLayout
+import androidx.compose.animation.samples.R
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Button
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+
+@Preview
+@Composable
+fun SharedToolBarDemo() {
+    val navController = rememberNavController()
+    SharedTransitionLayout {
+        NavHost(navController, startDestination = "first") {
+            composable("first",
+                enterTransition = { slideInHorizontally(initialOffsetX = { -it }) },
+                exitTransition = { slideOutHorizontally(targetOffsetX = { it }) }
+            ) {
+                Column {
+                    TopAppBar(
+                        title = { Text("Text") },
+                        modifier = Modifier.sharedElement(
+                            rememberSharedContentState(key = "appBar"),
+                            this@composable,
+                        )
+                    )
+                    Text(
+                        "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent" +
+                            " fringilla mollis efficitur. Maecenas sit amet urna eu urna blandit" +
+                            " suscipit efficitur eget mauris. Nullam eget aliquet ligula. Nunc" +
+                            "id euismod elit. Morbi aliquam enim eros, eget consequat" +
+                            " dolor consequat id. Quisque elementum faucibus congue. Curabitur" +
+                            " mollis aliquet turpis, ut pellentesque justo eleifend nec.\n",
+                    )
+                    Button( navController.navigate("second") }) {
+                        Text("Navigate to Cat")
+                    }
+                }
+            }
+            composable("second",
+                enterTransition = { slideInHorizontally(initialOffsetX = { -it }) },
+                exitTransition = { slideOutHorizontally(targetOffsetX = { it }) }
+            ) {
+                Column {
+                    TopAppBar(
+                        title = { Text("Cat") },
+                        modifier = Modifier.sharedElement(
+                            rememberSharedContentState(key = "appBar"),
+                            this@composable,
+                        )
+                    )
+                    Image(
+                        painterResource(id = R.drawable.yt_profile),
+                        contentDescription = "cute cat",
+                        contentScale = ContentScale.FillHeight,
+                        modifier = Modifier.clip(shape = RoundedCornerShape(20.dp))
+                    )
+                    Button(>
+                        navController.navigate("third")
+                    }) {
+                        Text("Navigate to Empty Page")
+                    }
+                }
+            }
+            composable("third",
+                enterTransition = { slideInHorizontally(initialOffsetX = { -it }) },
+                exitTransition = { slideOutHorizontally(targetOffsetX = { it }) }
+            ) {
+                Column(Modifier.fillMaxWidth()) {
+                    Text("Nothing to see here. Move on.")
+                    Spacer(Modifier.size(200.dp))
+                    Button( navController.popBackStack("first", false) }) {
+                        Text("Pop back to Text")
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SharedTransitionScopeDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SharedTransitionScopeDemo.kt
new file mode 100644
index 0000000..26ec926
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SharedTransitionScopeDemo.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalSharedTransitionApi::class)
+
+package androidx.compose.animation.demos.sharedelement
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionLayout
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.samples.SharedElementInAnimatedContentSample
+import androidx.compose.animation.samples.SharedElementWithFABInOverlaySample
+import androidx.compose.animation.samples.SharedElementWithMovableContentSample
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.ScrollableTabRow
+import androidx.compose.material.Tab
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@Preview
+@Composable
+fun SharedElementDemos() {
+    var selectedTab by remember { mutableIntStateOf(0) }
+    val list = listOf<Pair<String, @Composable () -> Unit>>(
+        "AnimContent\n List To Details" to { ListToDetailsDemo() },
+        "Nested" to { NestedSharedElementDemo() },
+        "Expanded Card" to { SwitchBetweenCollapsedAndExpanded() },
+        "Container Transform" to { ContainerTransformDemo() },
+        "Shared Element\n Caller Managed Vis" to { SharedElementWithCallerManagedVisibility() },
+        "AnimVis Extension" to { SharedElementScopeWithAnimatedVisibilityScopeDemo() },
+        "Shared Tool Bar" to { SharedToolBarDemo() }
+    )
+
+    Column {
+        ScrollableTabRow(selectedTab) {
+            list.forEachIndexed { index, (text, _) ->
+                Tab(
+                    index == selectedTab,
+                    { selectedTab = index },
+                    modifier = Modifier.padding(5.dp)
+                ) {
+                    Text(text)
+                }
+            }
+        }
+        list[selectedTab].second.invoke()
+    }
+}
+
+@Preview
+@Composable
+fun SharedElementScopeWithAnimatedVisibilityScopeDemo() {
+    var selectFirst by remember { mutableStateOf(true) }
+    val sharedBoundsKey = remember { Any() }
+    SharedTransitionLayout(
+        Modifier
+            .fillMaxSize()
+            .padding(10.dp)
+            .clickable {
+                selectFirst = !selectFirst
+            }
+    ) {
+        Box {
+            val shape =
+                RoundedCornerShape(animateDpAsState(if (selectFirst) 10.dp else 30.dp).value)
+            AnimatedVisibility(
+                selectFirst,
+                enter = slideInHorizontally { -it } + fadeIn(),
+                exit = slideOutHorizontally { -it } + fadeOut()
+            ) {
+                Column {
+                    Box(
+                        Modifier
+                            .layout { m, c ->
+                                m
+                                    .measure(c)
+                                    .run {
+                                        layout(width, height) { place(0, 0) }
+                                    }
+                            }
+                            .sharedBounds(
+                                rememberSharedContentState(sharedBoundsKey),
+                                this@AnimatedVisibility,
+                                clipInOverlayDuringTransition = OverlayClip(clipShape = shape)
+                            )
+                            .background(Color.Red)
+                            .size(100.dp),
+                        contentAlignment = Alignment.Center
+                    ) {
+                        Text(if (!selectFirst) "false" else "true", color = Color.White)
+                    }
+                    Box(
+                        Modifier
+                            .size(100.dp)
+                            .background(Color.Yellow, RoundedCornerShape(10.dp))
+                    )
+                    Box(
+                        Modifier
+                            .size(100.dp)
+                            .background(Color.Magenta, RoundedCornerShape(10.dp))
+                    )
+                }
+            }
+            AnimatedVisibility(
+                !selectFirst,
+                enter = slideInHorizontally { -it } + fadeIn(),
+                exit = slideOutHorizontally { -it } + fadeOut()
+            ) {
+                Row {
+                    Box(
+                        Modifier
+                            .offset(180.dp, 180.dp)
+                            .sharedBounds(
+                                rememberSharedContentState(key = sharedBoundsKey),
+                                this@AnimatedVisibility,
+                                clipInOverlayDuringTransition = OverlayClip(clipShape = shape)
+                            )
+                            .background(Color.Blue)
+                            .size(180.dp),
+                        contentAlignment = Alignment.Center
+                    ) {
+                        Text(if (selectFirst) "false" else "true", color = Color.White)
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Preview
+@Composable
+fun SharedElementWithMovableContent() {
+    SharedElementWithMovableContentSample()
+}
+
+@Preview
+@Composable
+fun SharedElementInAnimatedVisibilityWithFABRenderedInOverlay() {
+    SharedElementWithFABInOverlaySample()
+}
+
+@Preview
+@Composable
+fun SharedElementInAnimatedContent() {
+    SharedElementInAnimatedContentSample()
+}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SwitchBetweenCollapsedAndExpanded.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SwitchBetweenCollapsedAndExpanded.kt
new file mode 100644
index 0000000..e44c49c
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/sharedelement/SwitchBetweenCollapsedAndExpanded.kt
@@ -0,0 +1,394 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalSharedTransitionApi::class)
+
+package androidx.compose.animation.demos.sharedelement
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionLayout
+import androidx.compose.animation.SharedTransitionScope
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.demos.R
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.zIndex
+
+@Preview
+@Composable
+fun SwitchBetweenCollapsedAndExpanded() {
+    var expanded by remember { mutableStateOf(false) }
+    Box(
+        Modifier.clickable(>
+            expanded = !expanded
+        },
+            indication = null,
+            interactionSource = remember { MutableInteractionSource() }
+        )) {
+        SharedTransitionLayout {
+            CollapsedCard(!expanded)
+            AnimatedVisibility(
+                visible = expanded, Modifier.fillMaxSize(),
+                enter = fadeIn(),
+                exit = fadeOut()
+            ) {
+                ExpandedCard()
+            }
+        }
+    }
+}
+
+context(SharedTransitionScope)
+@Composable
+fun CollapsedCard(showSharedElement: Boolean) {
+    Box(
+        Modifier
+            .fillMaxSize()
+            .background(Color.White)
+    ) {
+        Column {
+            SearchBarAndTabs()
+            Box(
+                Modifier
+                    .fillMaxWidth()
+                    .aspectRatio(1.1f)
+            ) {
+                this@Column.AnimatedVisibility(visible = showSharedElement) {
+                    Column(
+                        Modifier
+                            .padding(top = 10.dp, start = 10.dp, end = 10.dp)
+                            .graphicsLayer {
+                                this.alpha = alpha
+                            }
+                            .sharedBounds(
+                                rememberSharedContentState(key = "container"),
+                                this@AnimatedVisibility,
+                                clipInOverlayDuringTransition = OverlayClip(
+                                    RoundedCornerShape(20.dp)
+                                )
+                            )
+                            .clip(shape = RoundedCornerShape(20.dp))
+                            .background(color = Color(0xff333338)),
+                    ) {
+                        Box {
+                            Column {
+                                Image(
+                                    painterResource(R.drawable.quiet_night),
+                                    contentDescription = null,
+                                    modifier =
+                                    Modifier
+                                        .fillMaxWidth()
+                                        .sharedElement(
+                                            rememberSharedContentState(key = "quiet_night"),
+                                            this@AnimatedVisibility,
+                                            zIndexInOverlay = 0.5f,
+                                        ),
+                                    contentScale = ContentScale.FillWidth
+                                )
+                                Text(
+                                    text = longText,
+                                    color = Color.Gray,
+                                    fontSize = 15.sp,
+                                    modifier = Modifier
+                                        .fillMaxWidth()
+                                        .padding(start = 20.dp, end = 20.dp, top = 20.dp)
+                                        .height(14.dp)
+                                        .sharedElement(
+                                            rememberSharedContentState(key = "longText"),
+                                            this@AnimatedVisibility,
+                                        )
+                                        .clipToBounds()
+                                        .wrapContentHeight(align = Alignment.Top, unbounded = true)
+                                        .skipToLookaheadSize(),
+                                )
+                            }
+
+                            Text(
+                                text = title,
+                                fontFamily = FontFamily.Default,
+                                color = Color.White,
+                                fontSize = 20.sp,
+                                modifier = Modifier
+                                    .fillMaxWidth()
+                                    .align(Alignment.BottomCenter)
+                                    .renderInSharedTransitionScopeOverlay(
+                                        zIndexInOverlay = 1f
+                                    )
+                                    .animateEnterExit(
+                                        fadeIn(tween(1000)) + slideInVertically { -it / 3 },
+                                        fadeOut(tween(50)) + slideOutVertically { -it / 3 }
+                                    )
+                                    .skipToLookaheadSize()
+                                    .background(
+                                        Brush.verticalGradient(
+                                            listOf(
+                                                Color.Transparent,
+                                                Color.Black,
+                                                Color.Transparent
+                                            )
+                                        )
+                                    )
+                                    .padding(20.dp),
+                            )
+                        }
+                        InstallBar(
+                            Modifier
+                                .fillMaxWidth()
+                                .zIndex(1f)
+                                .padding(start = 15.dp, end = 15.dp, top = 10.dp, bottom = 10.dp)
+                                .sharedElementWithCallerManagedVisibility(
+                                    rememberSharedContentState(key = "install_bar"),
+                                    showSharedElement,
+                                )
+                        )
+                    }
+                }
+            }
+            Image(
+                painterResource(R.drawable.app_hero_cluster), contentDescription = null,
+                Modifier
+                    .fillMaxWidth()
+                    .wrapContentHeight(align = Alignment.Top, unbounded = true),
+                contentScale = ContentScale.FillWidth
+            )
+        }
+        Image(
+            painterResource(R.drawable.navigation_bar), contentDescription = null,
+            Modifier
+                .fillMaxWidth()
+                .align(Alignment.BottomCenter),
+            contentScale = ContentScale.FillWidth
+        )
+    }
+}
+
+context(SharedTransitionScope, AnimatedVisibilityScope)
+@Composable
+fun ExpandedCard() {
+    Box(
+        Modifier
+            .fillMaxSize()
+            .background(Color(0x55000000))
+    ) {
+        Column(
+            Modifier
+                .align(Alignment.Center)
+                .padding(20.dp)
+                .sharedBounds(
+                    rememberSharedContentState(key = "container"),
+                    this@AnimatedVisibilityScope,
+                    enter = EnterTransition.None,
+                    exit = ExitTransition.None,
+                    clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(20.dp))
+                )
+                .clip(shape = RoundedCornerShape(20.dp))
+                .background(Color(0xff333338))
+        ) {
+            Column(
+                Modifier
+                    .renderInSharedTransitionScopeOverlay(
+                        zIndexInOverlay = 1f
+                    )
+                    .animateEnterExit(
+                        fadeIn() + slideInVertically { it / 3 },
+                        fadeOut() + slideOutVertically { it / 3 }
+                    )
+                    .skipToLookaheadSize()
+                    .background(
+                        Brush.verticalGradient(
+                            listOf(Color.Transparent, Color.Black, Color.Transparent)
+                        )
+                    )
+                    .padding(start = 20.dp, end = 20.dp),
+            ) {
+                Text(
+                    text = "Lorem ipsum",
+                    Modifier
+                        .padding(top = 20.dp, bottom = 10.dp)
+                        .background(Color.LightGray, shape = RoundedCornerShape(15.dp))
+                        .padding(top = 8.dp, bottom = 8.dp, start = 15.dp, end = 15.dp),
+                    color = Color.Black,
+                    fontFamily = FontFamily.Default,
+                    fontWeight = FontWeight.Bold,
+                    fontSize = 15.sp
+                )
+                Text(
+                    text = title,
+                    color = Color.White,
+                    fontSize = 30.sp,
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .padding(bottom = 20.dp)
+                )
+            }
+            Image(
+                painterResource(R.drawable.quiet_night), contentDescription = null,
+                modifier =
+                Modifier
+                    .fillMaxWidth()
+                    .sharedElement(
+                        rememberSharedContentState("quiet_night"),
+                        this@AnimatedVisibilityScope,
+                    ),
+                contentScale = ContentScale.FillWidth
+            )
+
+            Text(
+                text = longText,
+                color = Color.Gray,
+                fontSize = 15.sp,
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .padding(start = 15.dp, end = 10.dp, top = 10.dp)
+                    .height(50.dp)
+                    .sharedElement(
+                        rememberSharedContentState("longText"),
+                        this@AnimatedVisibilityScope,
+                    )
+                    .clipToBounds()
+                    .wrapContentHeight(align = Alignment.Top, unbounded = true)
+                    .skipToLookaheadSize(),
+            )
+
+            InstallBar(
+                Modifier
+                    .fillMaxWidth()
+                    .zIndex(1f)
+                    .background(Color(0xff333338))
+                    .padding(start = 15.dp, end = 15.dp, top = 10.dp, bottom = 10.dp)
+                    .sharedElement(
+                        rememberSharedContentState("install_bar"),
+                        this@AnimatedVisibilityScope,
+                    )
+            )
+        }
+    }
+}
+
+@Composable
+private fun SearchBarAndTabs() {
+    Image(
+        painterResource(R.drawable.search_bar), contentDescription = null,
+        modifier =
+        Modifier
+            .fillMaxWidth(),
+        contentScale = ContentScale.FillWidth
+    )
+    Image(
+        painterResource(R.drawable.tabs), contentDescription = null,
+        modifier =
+        Modifier
+            .fillMaxWidth(),
+        contentScale = ContentScale.FillWidth
+    )
+}
+
+@Composable
+private fun InstallBar(modifier: Modifier) {
+    Row(
+        modifier
+            .fillMaxWidth()
+            .requiredHeight(60.dp)
+    ) {
+        Image(
+            painterResource(R.drawable.quiet_night_thumb), contentDescription = null,
+            Modifier
+                .padding(10.dp)
+                .requiredSize(40.dp)
+                .clip(RoundedCornerShape(10.dp)),
+            contentScale = ContentScale.Crop
+        )
+
+        Column(
+            Modifier
+                .fillMaxHeight()
+                .padding(top = 10.dp, bottom = 10.dp),
+            verticalArrangement = Arrangement.SpaceAround
+        ) {
+            Text("Lorem ipsum dolor", color = Color.LightGray, fontSize = 15.sp)
+            Text("Lorem", color = Color.Gray, fontSize = 12.sp)
+        }
+        Spacer(Modifier.weight(1f))
+        Text(
+            text = "Lorem",
+            Modifier
+                .background(Color.Gray, shape = RoundedCornerShape(15.dp))
+                .align(Alignment.CenterVertically)
+                .padding(top = 8.dp, bottom = 8.dp, start = 15.dp, end = 15.dp),
+            color = Color.White,
+            fontFamily = FontFamily.Default,
+            fontWeight = FontWeight.Bold,
+            fontSize = 15.sp
+        )
+    }
+}
+
+private const val title = "Lorem ipsum dolor sit amet, sed do eiusmod tempor"
+
+private const val longText =
+    "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt" +
+        " ut labore et dolore magna aliqua. Adipiscing enim eu turpis egestas pretium aenean" +
+        " pharetra magna. Consectetur libero id faucibus nisl tincidunt eget. Est placerat in " +
+        "egestas erat imperdiet sed euismod nisi. Mauris a diam maecenas sed. Urna nunc id" +
+        " cursus metus aliquam eleifend mi in nulla. Pellentesque sit amet porttitor eget " +
+        "dolor morbi. A lacus vestibulum sed arcu non odio euismod. Integer enim neque volutpat" +
+        " ac tincidunt vitae. Nunc lobortis mattis aliquam faucibus purus in. In egestas erat " +
+        "imperdiet sed euismod nisi porta lorem. Fermentum leo vel orci porta non."
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/app_hero_cluster.png b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/app_hero_cluster.png
new file mode 100644
index 0000000..197cd09
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/app_hero_cluster.png
Binary files differ
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/cowboy.jpeg b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/cowboy.jpeg
new file mode 100644
index 0000000..c429ea9
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/cowboy.jpeg
Binary files differ
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/navigation_bar.png b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/navigation_bar.png
new file mode 100644
index 0000000..f4ce28e
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/navigation_bar.png
Binary files differ
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/pepper.jpg b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/pepper.jpg
index 1aa03c5..418cd1d 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/pepper.jpg
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/pepper.jpg
Binary files differ
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/question_mark.xml b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/question_mark.xml
new file mode 100644
index 0000000..929ae8a
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/question_mark.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<vector android:height="12dp"
+android:viewportHeight="24" android:viewportWidth="24"
+android:width="12dp" xmlns:android="http://schemas.android.com/apk/res/android">
+<path android:fillColor="#CCCCCC" android:pathData="M11.07,12.85c0.77,-1.39 2.25,-2.21 3.11,-3.44c0.91,-1.29 0.4,-3.7 -2.18,-3.7c-1.69,0 -2.52,1.28 -2.87,2.34L6.54,6.96C7.25,4.83 9.18,3 11.99,3c2.35,0 3.96,1.07 4.78,2.41c0.7,1.15 1.11,3.3 0.03,4.9c-1.2,1.77 -2.35,2.31 -2.97,3.45c-0.25,0.46 -0.35,0.76 -0.35,2.24h-2.89C10.58,15.22 10.46,13.95 11.07,12.85zM14,20c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2S14,18.9 14,20z"/>
+</vector>
\ No newline at end of file
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/quiet_night.jpg b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/quiet_night.jpg
new file mode 100644
index 0000000..4057be1
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/quiet_night.jpg
Binary files differ
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/quiet_night_thumb.jpg b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/quiet_night_thumb.jpg
new file mode 100644
index 0000000..eba2990
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/quiet_night_thumb.jpg
Binary files differ
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/search_bar.png b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/search_bar.png
new file mode 100644
index 0000000..1a5ada6
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/search_bar.png
Binary files differ
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/tabs.png b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/tabs.png
new file mode 100644
index 0000000..9e720f7
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/tabs.png
Binary files differ
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/yt_profile2.jpeg b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/yt_profile2.jpeg
new file mode 100644
index 0000000..b69161c
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/yt_profile2.jpeg
Binary files differ
diff --git a/compose/animation/animation/samples/src/main/java/androidx/compose/animation/samples/SharedTransitionSamples.kt b/compose/animation/animation/samples/src/main/java/androidx/compose/animation/samples/SharedTransitionSamples.kt
new file mode 100644
index 0000000..1724496
--- /dev/null
+++ b/compose/animation/animation/samples/src/main/java/androidx/compose/animation/samples/SharedTransitionSamples.kt
@@ -0,0 +1,436 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionLayout
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredHeightIn
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.FloatingActionButton
+import androidx.compose.material.Icon
+import androidx.compose.material.Surface
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.outlined.Create
+import androidx.compose.material.icons.outlined.Favorite
+import androidx.compose.material.icons.outlined.Share
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.movableContentOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+@Sampled
+@Composable
+fun NestedSharedBoundsSample() {
+    // Nested shared bounds sample.
+    val selectionColor = Color(0xff3367ba)
+    var expanded by remember { mutableStateOf(true) }
+    SharedTransitionLayout(
+        Modifier
+            .fillMaxSize()
+            .clickable {
+                expanded = !expanded
+            }
+            .background(Color(0x88000000))
+    ) {
+        AnimatedVisibility(
+            visible = expanded,
+            enter = EnterTransition.None,
+            exit = ExitTransition.None
+        ) {
+            Box(modifier = Modifier.fillMaxSize()) {
+                Surface(
+                    Modifier
+                        .align(Alignment.BottomCenter)
+                        .padding(20.dp)
+                        .sharedBounds(
+                            rememberSharedContentState(key = "container"),
+                            this@AnimatedVisibility
+                        )
+                        .requiredHeightIn(max = 60.dp),
+                    shape = RoundedCornerShape(50),
+                ) {
+                    Row(
+                        Modifier
+                            .padding(10.dp)
+                            // By using Modifier.skipToLookaheadSize(), we are telling the layout
+                            // system to layout the children of this node as if the animations had
+                            // all finished. This avoid re-laying out the Row with animated width,
+                            // which is _sometimes_ desirable. Try removing this modifier and
+                            // observe the effect.
+                            .skipToLookaheadSize()
+                    ) {
+                        Icon(
+                            Icons.Outlined.Share,
+                            contentDescription = "Share",
+                            modifier = Modifier.padding(
+                                top = 10.dp,
+                                bottom = 10.dp,
+                                start = 10.dp,
+                                end = 20.dp
+                            )
+                        )
+                        Icon(
+                            Icons.Outlined.Favorite,
+                            contentDescription = "Favorite",
+                            modifier = Modifier.padding(
+                                top = 10.dp,
+                                bottom = 10.dp,
+                                start = 10.dp,
+                                end = 20.dp
+                            )
+                        )
+                        Icon(
+                            Icons.Outlined.Create,
+                            contentDescription = "Create",
+                            tint = Color.White,
+                            modifier = Modifier
+                                .sharedBounds(
+                                    rememberSharedContentState(key = "icon_background"),
+                                    this@AnimatedVisibility
+                                )
+                                .background(selectionColor, RoundedCornerShape(50))
+                                .padding(
+                                    top = 10.dp,
+                                    bottom = 10.dp,
+                                    start = 20.dp,
+                                    end = 20.dp
+                                )
+                                .sharedElement(
+                                    rememberSharedContentState(key = "icon"),
+                                    this@AnimatedVisibility
+                                )
+                        )
+                    }
+                }
+            }
+        }
+        AnimatedVisibility(
+            visible = !expanded,
+            enter = EnterTransition.None,
+            exit = ExitTransition.None
+        ) {
+            Box(modifier = Modifier.fillMaxSize()) {
+                Surface(
+                    Modifier
+                        .align(Alignment.BottomEnd)
+                        .padding(30.dp)
+                        .sharedBounds(
+                            rememberSharedContentState(key = "container"),
+                            this@AnimatedVisibility,
+                            enter = EnterTransition.None,
+                        )
+                        .sharedBounds(
+                            rememberSharedContentState(key = "icon_background"),
+                            this@AnimatedVisibility,
+                            enter = EnterTransition.None,
+                            exit = ExitTransition.None
+                        ),
+                    shape = RoundedCornerShape(30.dp),
+                    color = selectionColor
+                ) {
+                    Icon(
+                        Icons.Outlined.Create,
+                        contentDescription = "Create",
+                        tint = Color.White,
+                        modifier = Modifier
+                            .padding(30.dp)
+                            .size(40.dp)
+                            .sharedElement(
+                                rememberSharedContentState(key = "icon"),
+                                this@AnimatedVisibility
+                            )
+                    )
+                }
+            }
+        }
+    }
+}
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+@Sampled
+@Composable
+fun SharedElementWithMovableContentSample() {
+    var showThumbnail by remember {
+        mutableStateOf(true)
+    }
+    val movableContent = remember {
+        movableContentOf {
+            val cornerRadius = animateDpAsState(targetValue = if (!showThumbnail) 20.dp else 5.dp)
+            Image(
+                painterResource(id = R.drawable.yt_profile),
+                contentDescription = "cute cat",
+                contentScale = ContentScale.FillHeight,
+                modifier = Modifier.clip(shape = RoundedCornerShape(cornerRadius.value))
+            )
+        }
+    }
+    SharedTransitionLayout(
+        Modifier
+            .clickable { showThumbnail = !showThumbnail }
+            .fillMaxSize()
+            .padding(10.dp)) {
+        Column {
+            Box(
+                // When using Modifier.sharedElementWithCallerManagedVisibility(), even when
+                // visible == false, the layout will continue to occupy space in its parent layout.
+                // The content will continue to be composed, unless the content is [MovableContent]
+                // like in this example below.
+                Modifier
+                    .sharedElementWithCallerManagedVisibility(
+                        rememberSharedContentState(key = "YT"),
+                        showThumbnail,
+                    )
+                    .size(100.dp)
+            ) {
+                if (showThumbnail) {
+                    movableContent()
+                }
+            }
+            Box(
+                Modifier
+                    .fillMaxWidth()
+                    .height(100.dp)
+                    .background(Color(0xffffcc5c), RoundedCornerShape(5.dp))
+            )
+            Box(
+                Modifier
+                    .fillMaxWidth()
+                    .height(100.dp)
+                    .background(Color(0xff2a9d84), RoundedCornerShape(5.dp))
+            )
+        }
+        Box(
+            Modifier
+                .fillMaxSize()
+                .aspectRatio(1f)
+                .sharedElementWithCallerManagedVisibility(
+                    rememberSharedContentState(key = "YT"),
+                    !showThumbnail
+                )
+        ) {
+            if (!showThumbnail) {
+                movableContent()
+            }
+        }
+    }
+}
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+@Sampled
+@Composable
+fun SharedElementWithFABInOverlaySample() {
+    // Create an Image that will be shared between the two shared elements.
+    @Composable
+    fun Cat(modifier: Modifier = Modifier) {
+        Image(
+            painterResource(id = R.drawable.yt_profile),
+            contentDescription = "cute cat",
+            contentScale = ContentScale.FillHeight,
+            modifier = modifier
+                .clip(shape = RoundedCornerShape(10))
+        )
+    }
+
+    var showThumbnail by remember {
+        mutableStateOf(true)
+    }
+    SharedTransitionLayout(
+        Modifier
+            .clickable { showThumbnail = !showThumbnail }
+            .fillMaxSize()
+            .padding(10.dp)) {
+        Column(Modifier.padding(10.dp)) {
+            // Create an AnimatedVisibility for the shared element, so that the layout siblings
+            // (i.e. the two boxes below) will move in to fill the space during the exit transition.
+            AnimatedVisibility(visible = showThumbnail) {
+                Cat(
+                    Modifier
+                        .size(100.dp)
+                        // Create a shared element, using string as the key
+                        .sharedElement(
+                            rememberSharedContentState(key = "YT"),
+                            this@AnimatedVisibility,
+                        )
+                )
+            }
+            Box(
+                Modifier
+                    .fillMaxWidth()
+                    .height(100.dp)
+                    .background(Color(0xffffcc5c), RoundedCornerShape(5.dp))
+            )
+            Box(
+                Modifier
+                    .fillMaxWidth()
+                    .height(100.dp)
+                    .background(Color(0xff2a9d84), RoundedCornerShape(5.dp))
+            )
+        }
+        Box(modifier = Modifier.fillMaxSize()) {
+            AnimatedVisibility(!showThumbnail) {
+                Cat(
+                    Modifier
+                        .fillMaxSize()
+                        // Create another shared element, and make sure the string key matches
+                        // the other shared element.
+                        .sharedElement(
+                            rememberSharedContentState(key = "YT"),
+                            this@AnimatedVisibility,
+                        )
+                )
+            }
+            FloatingActionButton(
+                modifier = Modifier.padding(20.dp).align(Alignment.BottomEnd)
+                    // During shared element transition, shared elements will be rendered in
+                    // overlay to escape any clipping or layer transform from parents. It also
+                    // means they will render over on top of UI elements such as Floating Action
+                    // Button. Once the transition is finished, they will be dropped from the
+                    // overlay to their own DrawScopes. To help support keeping specific UI
+                    // elements always on top, Modifier.renderInSharedTransitionScopeOverlay
+                    // will temporarily elevate them into the overlay as well. By default,
+                    // this modifier keeps content in overlay during the time when the
+                    // shared transition is active (i.e. SharedTransitionScope#isTransitionActive).
+                    // The duration can be customize via `renderInOverlay` parameter.
+                    .renderInSharedTransitionScopeOverlay(
+                        // zIndexInOverlay by default is 0f for this modifier and for shared
+                        // elements. By overwriting zIndexInOverlay to 1f, we can ensure this
+                        // FAB is rendered on top of the shared elements.
+                        zIndexInOverlay = 1f
+                    ),
+                >
+            ) {
+                Icon(Icons.Default.Favorite, contentDescription = "favorite")
+            }
+        }
+    }
+}
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+@Composable
+@Sampled
+fun SharedElementInAnimatedContentSample() {
+    // This is the Image that we will add shared element modifier on. It's important to make sure
+    // modifiers that are not shared between the two shared elements (such as size modifiers if
+    // the size changes) are the parents (i.e. on the left side) of Modifier.sharedElement.
+    // Meanwhile, the modifiers that are shared between the shared elements (e.g. Modifier.clip
+    // in this case) are on the right side of the Modifier.sharedElement.
+    @Composable
+    fun Cat(modifier: Modifier = Modifier) {
+        Image(
+            painterResource(id = R.drawable.yt_profile),
+            contentDescription = "cute cat",
+            contentScale = ContentScale.FillHeight,
+            modifier = modifier
+                .clip(shape = RoundedCornerShape(10))
+        )
+    }
+
+    // Shared element key is of type `Any`, which means it can be id, string, etc. The only
+    // requirement for the key is that it should be the same for shared elements that you intend
+    // to match. Here we use the image resource id as the key.
+    val sharedElementKey = R.drawable.yt_profile
+    var showLargeImage by remember {
+        mutableStateOf(true)
+    }
+
+    // First, we need to create a SharedTransitionLayout, this Layout will provide the coordinator
+    // space for shared element position animation, as well as an overlay for shared elements to
+    // render in. Children content in this Layout will be able to create shared element transition
+    // using the receiver scope: SharedTransitionScope
+    SharedTransitionLayout(
+        Modifier
+            .clickable { showLargeImage = !showLargeImage }
+            .fillMaxSize()
+            .padding(10.dp)) {
+        // In the SharedTransitionLayout, we will be able to access the receiver scope (i.e.
+        // SharedTransitionScope) in order to create shared element transition.
+        AnimatedContent(targetState = showLargeImage) { showLargeImageMode ->
+            if (showLargeImageMode) {
+                Cat(
+                    Modifier
+                        .fillMaxSize()
+                        .aspectRatio(1f)
+                        // Creating a shared element. Note that this modifier is *after*
+                        // the size modifier and aspectRatio modifier, because those size specs
+                        // are not shared between the two shared elements.
+                        .sharedElement(
+                            rememberSharedContentState(sharedElementKey),
+                            // Using the AnimatedVisibilityScope from the AnimatedContent defined
+                            // above.
+                            this@AnimatedContent,
+                        )
+                )
+            } else {
+                Column {
+                    Cat(
+                        Modifier
+                            .size(100.dp)
+                            // Creating another shared element with the same key.
+                            // Note that this modifier is *after* the size modifier,
+                            // The size changes between these two shared elements, i.e. the size
+                            // is not shared between the two shared elements.
+                            .sharedElement(
+                                rememberSharedContentState(sharedElementKey),
+                                this@AnimatedContent,
+                            )
+                    )
+                    Box(
+                        Modifier
+                            .fillMaxWidth()
+                            .height(100.dp)
+                            .background(Color(0xffffcc5c), RoundedCornerShape(5.dp))
+                    )
+                    Box(
+                        Modifier
+                            .fillMaxWidth()
+                            .height(100.dp)
+                            .background(Color(0xff2a9d84), RoundedCornerShape(5.dp))
+                    )
+                }
+            }
+        }
+    }
+}
diff --git a/compose/animation/animation/samples/src/main/res/drawable/yt_profile.jpg b/compose/animation/animation/samples/src/main/res/drawable/yt_profile.jpg
new file mode 100644
index 0000000..a63f133
--- /dev/null
+++ b/compose/animation/animation/samples/src/main/res/drawable/yt_profile.jpg
Binary files differ
diff --git a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/SharedTransitionTest.kt b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/SharedTransitionTest.kt
new file mode 100644
index 0000000..570f897
--- /dev/null
+++ b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/SharedTransitionTest.kt
@@ -0,0 +1,1870 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalSharedTransitionApi::class, ExperimentalAnimationApi::class)
+
+package androidx.compose.animation
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.SeekableTransitionState
+import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.rememberTransition
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertNotNull
+import junit.framework.TestCase.assertNull
+import junit.framework.TestCase.assertTrue
+import kotlin.math.roundToInt
+import kotlin.math.sqrt
+import kotlin.random.Random
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+class SharedTransitionTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun transitionInterruption() {
+        var visible by mutableStateOf(true)
+        val boundsTransform = BoundsTransform { _, _ -> tween(500, easing = LinearEasing) }
+        val positions = mutableListOf(
+            Offset.Zero, Offset.Zero, Offset.Zero, Offset.Zero
+        )
+        val sizes = mutableListOf(
+            IntSize(-1, -1), IntSize(-1, -1), IntSize.Zero, IntSize.Zero
+        )
+        var transitionScope: SharedTransitionScope? = null
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                SharedTransitionLayout {
+                    transitionScope = this
+                    AnimatedVisibility(visible = visible) {
+                        Column {
+                            Box(Modifier
+                                .sharedElement(
+                                    rememberSharedContentState(key = "cat"),
+                                    this@AnimatedVisibility,
+                                    boundsTransform = boundsTransform
+                                )
+                                .onGloballyPositioned {
+                                    positions[0] = lookaheadRoot.localPositionOf(it, Offset.Zero)
+                                    sizes[0] = it.size
+                                }
+                                .size(200.dp))
+                            Box(Modifier
+                                .sharedBounds(
+                                    rememberSharedContentState(key = "dog"),
+                                    this@AnimatedVisibility,
+                                    boundsTransform = boundsTransform
+                                )
+                                .onGloballyPositioned {
+                                    positions[1] = lookaheadRoot.localPositionOf(it, Offset.Zero)
+                                    sizes[1] = it.size
+                                }
+                                .size(50.dp))
+                        }
+                    }
+                    AnimatedVisibility(visible = !visible) {
+                        Row {
+                            Box(Modifier
+                                .sharedElement(
+                                    rememberSharedContentState(key = "dog"),
+                                    this@AnimatedVisibility,
+                                    boundsTransform = boundsTransform
+                                )
+                                .onGloballyPositioned {
+                                    positions[2] = lookaheadRoot.localPositionOf(it, Offset.Zero)
+                                    sizes[2] = it.size
+                                }
+                                .size(50.dp))
+                            Box(Modifier
+                                .sharedBounds(
+                                    rememberSharedContentState(key = "cat"),
+                                    this@AnimatedVisibility,
+                                    boundsTransform = boundsTransform
+                                )
+                                .onGloballyPositioned {
+                                    positions[3] = lookaheadRoot.localPositionOf(it, Offset.Zero)
+                                    sizes[3] = it.size
+                                }
+                                .size(200.dp))
+                        }
+                    }
+                }
+            }
+        }
+        rule.waitForIdle()
+
+        rule.runOnIdle {
+            assertFalse(transitionScope!!.isTransitionActive)
+            assertEquals(IntSize(200, 200), sizes[0])
+            assertEquals(IntSize(50, 50), sizes[1])
+            assertEquals(Offset(0f, 0f), positions[0])
+            assertEquals(Offset(0f, 200f), positions[1])
+        }
+
+        rule.mainClock.autoAdvance = false
+        visible = false
+
+        repeat(20) {
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+
+        val offsetTolerance = Offset(5f, 5f)
+        val tolerance = IntSize(5, 5)
+
+        assertEquals(positions[0], positions[3], offsetTolerance)
+        assertEquals(positions[1], positions[2], offsetTolerance)
+
+        assertEquals(sizes[0], sizes[3], tolerance)
+        assertEquals(sizes[1], sizes[2], tolerance)
+
+        assertTrue(transitionScope!!.isTransitionActive)
+
+        // Interrupt
+        visible = true
+        val lastSizes = mutableListOf<IntSize>().also { it.addAll(sizes) }
+        val lastPositions = mutableListOf<Offset>().also { it.addAll(positions) }
+
+        while (transitionScope?.isTransitionActive != false) {
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+
+            // Shared bounds are in sync with each other's bounds
+            assertEquals(positions[0], positions[3], offsetTolerance)
+            assertEquals(positions[1], positions[2], offsetTolerance)
+
+            assertEquals(sizes[0], sizes[3], tolerance)
+            assertEquals(sizes[1], sizes[2], tolerance)
+
+            // Expect size[0] to grow and size[1] to shrink from the point of interruption
+            // And that size always changes continuously
+            assertTrue(sizes[0].width >= lastSizes[0].width)
+            assertTrue(sizes[0].height >= lastSizes[0].height)
+            assertEquals(sizes[0], lastSizes[0], IntSize(10, 10))
+
+            assertTrue(sizes[1].width <= lastSizes[1].width)
+            assertTrue(sizes[1].height <= lastSizes[1].height)
+            assertEquals(sizes[1], lastSizes[1], IntSize(10, 10))
+
+            // Expect positions to change gradually.
+            assertEquals(positions[0], lastPositions[0], Offset(20f, 20f))
+            assertEquals(positions[1], lastPositions[1], Offset(40f, 40f))
+            assertEquals(0f, positions[0].y)
+            assertEquals(0f, positions[1].x)
+
+            lastSizes.clear()
+            lastSizes.addAll(sizes)
+            lastPositions.clear()
+            lastPositions.addAll(positions)
+        }
+
+        // Animation finished
+        assertEquals(IntSize(200, 200), sizes[0])
+        assertEquals(IntSize(50, 50), sizes[1])
+        assertEquals(Offset(0f, 0f), positions[0])
+        assertEquals(Offset(0f, 200f), positions[1])
+    }
+
+    @Test
+    fun transitionInterruptionSelfManagedVisibility() {
+        var visible by mutableStateOf(true)
+        val boundsTransform = BoundsTransform { _, _ -> tween(500, easing = LinearEasing) }
+        val positions = mutableListOf(
+            Offset.Zero, Offset.Zero, Offset.Zero, Offset.Zero
+        )
+        val sizes = mutableListOf(IntSize(-1, -1), IntSize(-1, -1), IntSize.Zero, IntSize.Zero)
+        var transitionScope: SharedTransitionScope? = null
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                SharedTransitionLayout {
+                    transitionScope = this
+                    Column {
+                        Box(Modifier
+                            .sharedElementWithCallerManagedVisibility(
+                                rememberSharedContentState(key = "cat"),
+                                visible = visible,
+                                boundsTransform = boundsTransform
+                            )
+                            .onGloballyPositioned {
+                                positions[0] = lookaheadRoot.localPositionOf(it, Offset.Zero)
+                                sizes[0] = it.size
+                            }
+                            .size(200.dp))
+                        Box(Modifier
+                            .sharedBoundsWithCallerManagedVisibility(
+                                rememberSharedContentState(key = "dog"),
+                                visible = visible,
+                                boundsTransform = boundsTransform
+                            )
+                            .onGloballyPositioned {
+                                positions[1] = lookaheadRoot.localPositionOf(it, Offset.Zero)
+                                sizes[1] = it.size
+                            }
+                            .size(50.dp))
+                    }
+                    Row {
+                        Box(Modifier
+                            .sharedElementWithCallerManagedVisibility(
+                                rememberSharedContentState(key = "dog"),
+                                visible = !visible,
+                                boundsTransform = boundsTransform
+                            )
+                            .onGloballyPositioned {
+                                positions[2] = lookaheadRoot.localPositionOf(it, Offset.Zero)
+                                sizes[2] = it.size
+                            }
+                            .size(50.dp))
+                        Box(Modifier
+                            .sharedBoundsWithCallerManagedVisibility(
+                                rememberSharedContentState(key = "cat"),
+                                visible = !visible,
+                                boundsTransform = boundsTransform
+                            )
+                            .onGloballyPositioned {
+                                positions[3] = lookaheadRoot.localPositionOf(it, Offset.Zero)
+                                sizes[3] = it.size
+                            }
+                            .size(200.dp))
+                    }
+                }
+            }
+        }
+        rule.waitForIdle()
+
+        rule.runOnIdle {
+            assertFalse(transitionScope!!.isTransitionActive)
+            assertEquals(IntSize(200, 200), sizes[0])
+            assertEquals(IntSize(50, 50), sizes[1])
+            assertEquals(Offset(0f, 0f), positions[0])
+            assertEquals(Offset(0f, 200f), positions[1])
+        }
+
+        rule.mainClock.autoAdvance = false
+        visible = false
+
+        repeat(20) {
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+
+        val offsetTolerance = Offset(5f, 5f)
+        val tolerance = IntSize(5, 5)
+
+        assertEquals(positions[0], positions[3], offsetTolerance)
+        assertEquals(positions[1], positions[2], offsetTolerance)
+
+        assertEquals(sizes[0], sizes[3], tolerance)
+        assertEquals(sizes[1], sizes[2], tolerance)
+
+        assertTrue(transitionScope!!.isTransitionActive)
+
+        // Interrupt
+        visible = true
+        val lastSizes = mutableListOf<IntSize>().also { it.addAll(sizes) }
+        val lastPositions = mutableListOf<Offset>().also { it.addAll(positions) }
+
+        while (transitionScope?.isTransitionActive != false) {
+
+            // Shared bounds are in sync with each other's bounds
+            assertEquals(positions[0], positions[3], offsetTolerance)
+            assertEquals(positions[1], positions[2], offsetTolerance)
+
+            assertEquals(sizes[0], sizes[3], tolerance)
+            assertEquals(sizes[1], sizes[2], tolerance)
+
+            // Expect size[0] to grow and size[1] to shrink from the point of interruption
+            // And that size always changes continuously
+            assertTrue(sizes[0].width >= lastSizes[0].width)
+            assertTrue(sizes[0].height >= lastSizes[0].height)
+            assertEquals(sizes[0], lastSizes[0], IntSize(10, 10))
+
+            assertTrue(sizes[1].width <= lastSizes[1].width)
+            assertTrue(sizes[1].height <= lastSizes[1].height)
+            assertEquals(sizes[1], lastSizes[1], IntSize(10, 10))
+
+            // Expect positions to change gradually.
+            assertEquals(positions[0], lastPositions[0], Offset(20f, 20f))
+            assertEquals(positions[1], lastPositions[1], Offset(40f, 40f))
+            assertEquals(0f, positions[0].y)
+            assertEquals(0f, positions[1].x)
+
+            lastSizes.clear()
+            lastSizes.addAll(sizes)
+            lastPositions.clear()
+            lastPositions.addAll(positions)
+
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+
+        // Animation finished
+        assertEquals(IntSize(200, 200), sizes[0])
+        assertEquals(IntSize(50, 50), sizes[1])
+        assertEquals(Offset(0f, 0f), positions[0])
+        assertEquals(Offset(0f, 200f), positions[1])
+    }
+
+    @Test
+    fun testKeyMatch() {
+        val key1 = Any()
+        val set = mutableSetOf<SharedTransitionScope.SharedContentState>()
+        var showRow by mutableStateOf(false)
+        var visible by mutableStateOf(true)
+        rule.setContent {
+            SharedTransitionLayout {
+                Column {
+                    Box(
+                        Modifier
+                            .sharedElementWithCallerManagedVisibility(
+                                rememberSharedContentState(key = key1).also {
+                                    set.add(it)
+                                },
+                                visible = visible,
+                            )
+                            .size(200.dp)
+                    )
+                    Box(
+                        Modifier
+                            .sharedElementWithCallerManagedVisibility(
+                                rememberSharedContentState(key = 2).also {
+                                    set.add(it)
+                                },
+                                visible = visible,
+                            )
+                            .size(200.dp)
+                    )
+                    Box(
+                        Modifier
+                            .sharedBoundsWithCallerManagedVisibility(
+                                rememberSharedContentState(key = "cat").also {
+                                    set.add(it)
+                                },
+                                visible = visible,
+                            )
+                            .size(200.dp)
+                    )
+                    Box(
+                        Modifier
+                            .sharedBoundsWithCallerManagedVisibility(
+                                rememberSharedContentState(key = Unit).also {
+                                    set.add(it)
+                                },
+                                visible = visible,
+                            )
+                            .size(200.dp)
+                    )
+                }
+                if (showRow) {
+                    Row {
+                        Box(
+                            Modifier
+                                .sharedElementWithCallerManagedVisibility(
+                                    rememberSharedContentState(key = key1).also {
+                                        set.add(it)
+                                    },
+                                    visible = !visible,
+                                )
+                                .size(50.dp)
+                        )
+                        Box(
+                            Modifier
+                                .sharedElementWithCallerManagedVisibility(
+                                    rememberSharedContentState(key = 2).also {
+                                        set.add(it)
+                                    },
+                                    visible = !visible,
+                                )
+                                .size(50.dp)
+                        )
+                        Box(
+                            Modifier
+                                .sharedBoundsWithCallerManagedVisibility(
+                                    rememberSharedContentState(key = "cat").also {
+                                        set.add(it)
+                                    },
+                                    visible = !visible,
+                                )
+                                .size(50.dp)
+                        )
+                        Box(
+                            Modifier
+                                .sharedBoundsWithCallerManagedVisibility(
+                                    rememberSharedContentState(key = Unit).also {
+                                        set.add(it)
+                                    },
+                                    visible = !visible,
+                                )
+                                .size(50.dp)
+                        )
+                    }
+                }
+            }
+        }
+        rule.waitForIdle()
+        assertEquals(4, set.size)
+        set.forEach {
+            assertFalse(it.isMatchFound)
+        }
+
+        // Show row to add matched shared elements into composition
+        showRow = true
+        rule.runOnIdle {
+            assertEquals(8, set.size)
+            set.forEach {
+                assertTrue(it.isMatchFound)
+            }
+        }
+        visible = false
+        rule.runOnIdle {
+            set.forEach {
+                assertTrue(it.isMatchFound)
+            }
+        }
+        set.clear()
+        showRow = false
+        rule.runOnIdle {
+            assertEquals(4, set.size)
+        }
+        rule.runOnIdle {
+            set.forEach {
+                assertFalse(it.isMatchFound)
+            }
+        }
+    }
+
+    @Test
+    fun testMatchFoundUpdatedPromptly() {
+        val key1 = Any()
+        val set = mutableSetOf<SharedTransitionScope.SharedContentState>()
+        val seekableTransition = SeekableTransitionState(1)
+        var target by mutableStateOf(1)
+        var firstFrame = true
+        rule.setContent {
+            LaunchedEffect(target) {
+                if (seekableTransition.currentState != target) {
+                    seekableTransition.animateTo(target)
+                }
+            }
+            val transition = rememberTransition(transitionState = seekableTransition)
+            SharedTransitionLayout {
+
+                val state1 = rememberSharedContentState(key = key1)
+                val state2 = rememberSharedContentState(key = key1)
+                transition.AnimatedContent {
+                    when (it) {
+                        1 -> Box(
+                            Modifier
+                                .sharedElement(
+                                    state1, this
+                                )
+                                .size(200.dp)
+                        ) {
+                            DisposableEffect(key1 = Unit) {
+                                set.add(state1)
+                                onDispose {
+                                    set.remove(state1)
+                                }
+                            }
+                        }
+
+                        2 -> Box(
+                            Modifier
+                                .sharedElement(
+                                    state2,
+                                    this
+                                )
+                                .size(600.dp)
+                        ) {
+                            DisposableEffect(key1 = Unit) {
+                                set.add(state2)
+                                onDispose {
+                                    set.remove(state2)
+                                }
+                            }
+                        }
+
+                        else -> Box(Modifier.size(200.dp)) {
+                            if (firstFrame) {
+                                firstFrame = false
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        rule.waitForIdle()
+        assertEquals(1, set.size)
+        set.forEach {
+            assertFalse(it.isMatchFound)
+        }
+
+        // Show row to add matched shared elements into composition
+        rule.runOnIdle {
+            target = 2
+        }
+        rule.waitUntil {
+            set.size == 2
+        }
+
+        repeat(5) {
+            rule.mainClock.advanceTimeByFrame()
+        }
+        assertEquals(2, set.size)
+
+        // Now we expect two shared elements to be matched
+        set.forEach {
+            assertTrue(it.isMatchFound)
+        }
+        target = 3
+
+        rule.waitUntil { !firstFrame }
+        set.forEach {
+            assertEquals(2, set.size)
+            assertFalse(it.isMatchFound)
+        }
+        rule.mainClock.advanceTimeBy(50000L)
+    }
+
+    @SdkSuppress(minSdkVersion = 26)
+    @OptIn(ExperimentalAnimationApi::class)
+    @Test
+    fun testBothContentShowing() {
+        var visible by mutableStateOf(false)
+        val tween = tween<Float>(100, easing = LinearEasing)
+        var transitionScope: SharedTransitionScope? = null
+        var exitTransition: Transition<*>? = null
+        var enterTransition: Transition<*>? = null
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                SharedTransitionLayout(
+                    Modifier
+                        .requiredSize(100.dp)
+                        .testTag("scope")
+                        .background(Color.White)
+                ) {
+                    transitionScope = this
+                    AnimatedVisibility(
+                        visible = visible,
+                        enter = EnterTransition.None,
+                        exit = ExitTransition.None
+                    ) {
+                        enterTransition = transition
+                        Box(
+                            Modifier
+                                .sharedBounds(
+                                    rememberSharedContentState(key = "test"),
+                                    this@AnimatedVisibility,
+                                    fadeIn(tween),
+                                    fadeOut(tween)
+                                )
+                                .fillMaxSize()
+                        ) {
+                            Box(
+                                Modifier
+                                    .fillMaxHeight()
+                                    .fillMaxWidth(0.5f)
+                                    .background(Color.Red)
+                                    .align(Alignment.CenterStart)
+                            )
+                        }
+                    }
+                    AnimatedVisibility(
+                        visible = !visible,
+                        enter = EnterTransition.None,
+                        exit = ExitTransition.None
+                    ) {
+                        exitTransition = transition
+                        Box(
+                            Modifier
+                                .sharedBounds(
+                                    rememberSharedContentState(key = "test"),
+                                    this@AnimatedVisibility,
+                                    fadeIn(tween),
+                                    fadeOut(tween)
+                                )
+                                .fillMaxSize()
+                        ) {
+                            Box(
+                                Modifier
+                                    .fillMaxHeight()
+                                    .fillMaxWidth(0.5f)
+                                    .background(Color.Blue)
+                                    .align(Alignment.CenterEnd)
+                            )
+                        }
+                    }
+                }
+            }
+        }
+        rule.waitForIdle()
+        assertFalse(transitionScope!!.isTransitionActive)
+        rule.mainClock.autoAdvance = false
+        visible = true
+
+        while (transitionScope?.isTransitionActive != true) {
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+
+        // Now shared bounds transition started
+        while (transitionScope?.isTransitionActive != false) {
+            val playTime = exitTransition!!.playTimeNanos / 1000_000L
+            val fraction = (playTime.toFloat() / 100f).coerceIn(0f, 1f)
+
+            val enterPlayTime = enterTransition!!.playTimeNanos / 1000_000L
+            val enterFraction = (enterPlayTime.toFloat() / 100f).coerceIn(0f, 1f)
+
+            rule.onNodeWithTag("scope").run {
+                assertExists("Error: Node doesn't exist")
+                captureToImage().run {
+                    assertPixels {
+                        if (it.x < width / 2) {
+                            Color.Red.copy(alpha = enterFraction).compositeOver(Color.White)
+                        } else if (it.x > width / 2) {
+                            Color.Blue.copy(alpha = 1f - fraction).compositeOver(Color.White)
+                        } else null
+                    }
+                }
+            }
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 26)
+    @OptIn(ExperimentalAnimationApi::class)
+    @Test
+    fun testOnlyVisibleContentShowingInSharedElement() {
+        var visible by mutableStateOf(false)
+        val tween = tween<Float>(100, easing = LinearEasing)
+        var transitionScope: SharedTransitionScope? = null
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                SharedTransitionLayout(
+                    Modifier
+                        .requiredSize(100.dp)
+                        .testTag("scope")
+                        .background(Color.White)
+                ) {
+                    transitionScope = this
+                    AnimatedVisibility(
+                        visible = visible, enter = fadeIn(tween), exit = fadeOut(tween)
+                    ) {
+                        Box(
+                            Modifier
+                                .sharedElement(
+                                    rememberSharedContentState(key = "test"),
+                                    this@AnimatedVisibility,
+                                )
+                                .fillMaxSize()
+                        ) {
+                            Box(
+                                Modifier
+                                    .fillMaxHeight()
+                                    .fillMaxWidth(0.5f)
+                                    .background(Color.Red)
+                                    .align(Alignment.CenterStart)
+                            )
+                        }
+                    }
+                    AnimatedVisibility(
+                        visible = !visible,
+                        enter = EnterTransition.None,
+                        exit = ExitTransition.None
+                    ) {
+                        Box(
+                            Modifier
+                                .sharedElement(
+                                    rememberSharedContentState(key = "test"),
+                                    this@AnimatedVisibility,
+                                )
+                                .fillMaxSize()
+                        ) {
+                            Box(
+                                Modifier
+                                    .fillMaxHeight()
+                                    .fillMaxWidth(0.5f)
+                                    .background(Color.Blue)
+                                    .align(Alignment.CenterEnd)
+                            )
+                        }
+                    }
+                }
+            }
+        }
+        rule.waitForIdle()
+        assertFalse(transitionScope!!.isTransitionActive)
+        rule.onNodeWithTag("scope").run {
+            assertExists("Error: Node doesn't exist")
+            captureToImage().run {
+                assertPixels {
+                    if (it.x < width / 2 - 2) {
+                        Color.White
+                    } else if (it.x > width / 2 + 2) {
+                        Color.Blue
+                    } else null
+                }
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        visible = true
+
+        while (transitionScope?.isTransitionActive != true) {
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+
+        // Now shared bounds transition started
+        while (transitionScope?.isTransitionActive != false) {
+            rule.onNodeWithTag("scope").run {
+                assertExists("Error: Node doesn't exist")
+                captureToImage().run {
+                    assertPixels {
+                        if (it.x < width / 2 - 2) {
+                            Color.Red
+                        } else if (it.x > width / 2 + 2) {
+                            Color.White
+                        } else null
+                    }
+                }
+            }
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test
+    fun testOverlayClip() {
+        // Set a clip shape on the shared element that is change both size and position, and check
+        // that the shape is being updated per frame.
+        var transitionScope: SharedTransitionScope? = null
+        var visible by mutableStateOf(true)
+        var size: IntSize? = null
+
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                SharedTransitionLayout(
+                    Modifier
+                        .requiredSize(100.dp)
+                        .testTag("scope")
+                        .background(Color.White)
+                ) {
+                    transitionScope = this
+                    AnimatedVisibility(
+                        visible = visible,
+                        enter = EnterTransition.None,
+                        exit = ExitTransition.None
+                    ) {
+                        Box(
+                            Modifier
+                                .sharedElement(rememberSharedContentState(key = "child"),
+                                    this@AnimatedVisibility,
+                                    boundsTransform = BoundsTransform { _, _ ->
+                                        tween(100, easing = LinearEasing)
+                                    })
+                                .fillMaxSize()
+                        )
+                    }
+                    AnimatedVisibility(
+                        modifier = Modifier.fillMaxSize(),
+                        visible = !visible,
+                        enter = fadeIn(tween(100)),
+                        exit = ExitTransition.None
+                    ) {
+                        Box(Modifier
+                            .fillMaxSize(0.5f)
+                            .sharedElement(rememberSharedContentState(key = "child"),
+                                this@AnimatedVisibility,
+                                clipInOverlayDuringTransition = OverlayClip(CircleShape),
+                                boundsTransform = BoundsTransform { _, _ ->
+                                    tween(100, easing = LinearEasing)
+                                })
+                            .onGloballyPositioned {
+                                size = it.size
+                            }
+                            .background(Color.Blue))
+                    }
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        assertFalse(transitionScope!!.isTransitionActive)
+        rule.onNodeWithTag("scope").run {
+            assertExists("Error: Node doesn't exist")
+            captureToImage().run {
+                assertPixels {
+                    Color.White
+                }
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        visible = false
+
+        while (transitionScope?.isTransitionActive != true) {
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+
+        // Set aside 5 pixel width for anti-aliasing
+        val widthTolerance = 4
+        // Now shared bounds transition started
+        while (transitionScope?.isTransitionActive != false) {
+            rule.onNodeWithTag("scope").run {
+                assertExists("Error: Node doesn't exist")
+                captureToImage().run {
+                    // Check clipping
+                    assertPixels {
+                        val distanceToCenter = sqrt(
+                            (it.x - width / 2) * (it.x - width / 2).toFloat() +
+                                (it.y - height / 2) * (it.y - height / 2)
+                        )
+                        if (it.x < width / 2 &&
+                            distanceToCenter < size!!.width / 2 - widthTolerance
+                        ) {
+                            Color.Blue
+                        } else if (distanceToCenter > size!!.width / 2 + widthTolerance) {
+                            Color.White
+                        } else null
+                    }
+                }
+            }
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test
+    fun testOverlayClipInheritedByChildren() {
+        var transitionScope: SharedTransitionScope? = null
+        var visible by mutableStateOf(true)
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                SharedTransitionLayout(
+                    Modifier
+                        .requiredSize(100.dp)
+                        .testTag("scope")
+                        .background(Color.White)
+                ) {
+                    transitionScope = this
+                    AnimatedVisibility(
+                        visible = visible,
+                        enter = EnterTransition.None,
+                        exit = ExitTransition.None
+                    ) {
+                        Box(
+                            Modifier
+                                .sharedBounds(
+                                    rememberSharedContentState(key = "test"),
+                                    this@AnimatedVisibility,
+                                )
+                                .fillMaxSize()
+                        ) {
+                            Box(
+                                Modifier
+                                    .sharedElement(
+                                        rememberSharedContentState(key = "child"),
+                                        this@AnimatedVisibility
+                                    )
+                                    .fillMaxHeight()
+                                    .fillMaxWidth(0.5f)
+                                    .align(Alignment.CenterStart)
+                            )
+                        }
+                    }
+                    AnimatedVisibility(
+                        visible = !visible,
+                        enter = fadeIn(tween(100)),
+                        exit = ExitTransition.None
+                    ) {
+                        Box(
+                            Modifier
+                                .sharedBounds(
+                                    rememberSharedContentState(key = "test"),
+                                    this@AnimatedVisibility,
+                                    clipInOverlayDuringTransition = OverlayClip(
+                                        clipShape = CircleShape
+                                    )
+                                )
+                                .fillMaxSize()
+                        ) {
+                            Box(
+                                Modifier
+                                    .sharedElement(
+                                        rememberSharedContentState(key = "child"),
+                                        this@AnimatedVisibility
+                                    )
+                                    .fillMaxHeight()
+                                    .fillMaxWidth(0.5f)
+                                    .align(Alignment.CenterStart)
+                                    .background(Color.Blue)
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        assertFalse(transitionScope!!.isTransitionActive)
+        rule.onNodeWithTag("scope").run {
+            assertExists("Error: Node doesn't exist")
+            captureToImage().run {
+                assertPixels {
+                    Color.White
+                }
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        visible = false
+
+        while (transitionScope?.isTransitionActive != true) {
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+
+        // Set aside 5 pixel width for anti-aliasing
+        val widthTolerance = 5
+        // Now shared bounds transition started
+        while (transitionScope?.isTransitionActive != false) {
+            rule.onNodeWithTag("scope").run {
+                assertExists("Error: Node doesn't exist")
+                captureToImage().run {
+                    // Check clipping
+                    assertPixels {
+                        val distanceToCenter = sqrt(
+                            (it.x - width / 2) * (it.x - width / 2).toFloat() +
+                                (it.y - height / 2) * (it.y - height / 2)
+                        )
+                        if (it.x < width / 2 && distanceToCenter < width / 2 - widthTolerance) {
+                            Color.Blue
+                        } else if (
+                            it.x > width / 2 + 5 || distanceToCenter > width / 2 + widthTolerance
+                        ) {
+                            Color.White
+                        } else null
+                    }
+                }
+            }
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+    }
+
+    @OptIn(ExperimentalAnimationApi::class)
+    @Test
+    fun testBoundsTransform() {
+        var transitionScope: SharedTransitionScope? = null
+        var visible by mutableStateOf(true)
+        var parentSize: IntSize? = null
+        var parentPosition: Offset? = null
+        var childSize: IntSize? = null
+        var childPosition: Offset? = null
+        var exitTransition: Transition<*>? = null
+
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                SharedTransitionLayout(
+                    Modifier
+                        .requiredSize(100.dp)
+                        .background(Color.White)
+                ) {
+                    transitionScope = this
+                    AnimatedVisibility(
+                        visible = visible,
+                        enter = EnterTransition.None,
+                        exit = ExitTransition.None
+                    ) {
+                        Box(
+                            Modifier
+                                .sharedBounds(
+                                    rememberSharedContentState(key = "test"),
+                                    this@AnimatedVisibility,
+                                )
+                                .fillMaxSize(),
+                            contentAlignment = Alignment.Center
+                        ) {
+                            Box(
+                                Modifier
+                                    .fillMaxSize(0.5f)
+                                    .sharedElement(
+                                        rememberSharedContentState(key = "child"),
+                                        this@AnimatedVisibility
+                                    )
+                            )
+                        }
+                    }
+                    AnimatedVisibility(
+                        modifier = Modifier
+                            .fillMaxSize(0.5f)
+                            .offset(x = 25.dp, y = 25.dp),
+                        visible = !visible,
+                        enter = fadeIn(tween(100)),
+                        exit = ExitTransition.None
+                    ) {
+                        exitTransition = this.transition
+                        Box(
+                            Modifier
+                                .offset(20.dp)
+                                .sharedBounds(rememberSharedContentState(key = "test"),
+                                    this@AnimatedVisibility,
+                                    boundsTransform = BoundsTransform { _, _ ->
+                                        tween(100, easing = LinearEasing)
+                                    })
+                                .onGloballyPositioned {
+                                    parentPosition =
+                                        lookaheadRoot.localPositionOf(it, Offset.Zero)
+                                    parentSize = it.size
+                                }) {
+                            Box(Modifier
+                                .offset(-20.dp)
+                                .sharedElement(
+                                    rememberSharedContentState(key = "child"),
+                                    this@AnimatedVisibility,
+                                    boundsTransform = { initialBounds, targetBounds ->
+                                        assertEquals(initialBounds, targetBounds)
+                                        spring()
+                                    }
+                                )
+                                .onGloballyPositioned {
+                                    childPosition =
+                                        lookaheadRoot.localPositionOf(it, Offset.Zero)
+                                    childSize = it.size
+                                }
+                                .fillMaxSize()
+                                .background(Color.Blue))
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        assertFalse(transitionScope!!.isTransitionActive)
+
+        rule.mainClock.autoAdvance = false
+        visible = false
+
+        while (transitionScope?.isTransitionActive != true) {
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+
+        // Now shared bounds transition started
+        while (transitionScope?.isTransitionActive != false) {
+            // Expect size (100, 100) -> (50, 50), position -> (0, 0) -> (45, 45)
+            val fraction =
+                ((exitTransition!!.playTimeNanos / 1000_000L) / 100f).coerceIn(0f, 1f)
+            val expectedSize = (50 * fraction + 100 * (1 - fraction)).roundToInt().let {
+                IntSize(it, it)
+            }
+            val expectedPosition = Offset(45f * fraction, 25f * fraction)
+            assertEquals(expectedSize, parentSize!!, IntSize(3, 3))
+            assertEquals(expectedPosition, parentPosition!!, Offset(3f, 3f))
+
+            // Child is expected to hold in place throughout the transition
+            assertEquals(IntSize(50, 50), childSize!!)
+            assertEquals(Offset(25f, 25f), childPosition!!)
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+    }
+
+    @Test
+    fun testPlaceHolderSize() {
+        var transitionScope: SharedTransitionScope? = null
+        var visible by mutableStateOf(true)
+        var parent1Size: IntSize? = null
+        var parent2Size: IntSize? = null
+        var expectedSize by mutableStateOf(IntSize.Zero)
+
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                SharedTransitionLayout(
+                    Modifier
+                        .requiredSize(100.dp)
+                        .background(Color.White)
+                ) {
+                    transitionScope = this
+                    AnimatedVisibility(
+                        visible = visible,
+                        enter = EnterTransition.None,
+                        exit = ExitTransition.None
+                    ) {
+                        Box(
+                            Modifier
+                                .fillMaxSize()
+                                .wrapContentSize()
+                                .onGloballyPositioned {
+                                    parent1Size = it.size
+                                },
+                            contentAlignment = Alignment.Center
+                        ) {
+                            Box(
+                                Modifier
+                                    .sharedElement(
+                                        rememberSharedContentState(key = "child"),
+                                        this@AnimatedVisibility
+                                    )
+                                    .fillMaxSize(0.5f)
+                            )
+                        }
+                    }
+                    AnimatedVisibility(
+                        visible = !visible,
+                        enter = fadeIn(tween(100)),
+                        exit = ExitTransition.None
+                    ) {
+                        Box(Modifier
+                            .onGloballyPositioned {
+                                parent2Size = it.size
+                            }
+                            .offset(-20.dp)
+                            .sharedElement(
+                                rememberSharedContentState(key = "child"),
+                                this@AnimatedVisibility,
+                                placeHolderSize = SharedTransitionScope
+                                    .PlaceHolderSize { _, _ ->
+                                        expectedSize
+                                    }
+                            )
+                            .fillMaxSize()
+                            .background(Color.Blue))
+                    }
+                }
+            }
+        }
+        rule.waitForIdle()
+        assertFalse(transitionScope!!.isTransitionActive)
+
+        rule.mainClock.autoAdvance = false
+        visible = false
+
+        while (transitionScope?.isTransitionActive != true) {
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+
+        // Now shared bounds transition started
+        while (transitionScope?.isTransitionActive != false) {
+            // Expect parent1 to stay at contentSize and parent2 to change size
+            assertEquals(IntSize(50, 50), parent1Size!!)
+            assertEquals(expectedSize, parent2Size!!)
+
+            expectedSize = IntSize(Random.nextInt(0, 100), Random.nextInt(0, 100))
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+    }
+
+    @OptIn(ExperimentalAnimationApi::class)
+    @SdkSuppress(minSdkVersion = 26)
+    @Test
+    fun testRenderInOverlayEqualsFalse() {
+        var transitionScope: SharedTransitionScope? = null
+        var visible by mutableStateOf(true)
+        var exit: Transition<*>? = null
+
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                SharedTransitionLayout(
+                    Modifier
+                        .testTag("scope")
+                        .requiredSize(100.dp)
+                        .background(Color.White)
+                ) {
+                    transitionScope = this
+
+                    AnimatedVisibility(
+                        visible = visible,
+                        enter = EnterTransition.None,
+                        exit = ExitTransition.None
+                    ) {
+                        Box(
+                            Modifier
+                                .sharedElement(
+                                    rememberSharedContentState(key = "child"),
+                                    this@AnimatedVisibility
+                                )
+                                .background(Color.Red)
+                                .fillMaxSize()
+                        )
+                    }
+                    AnimatedVisibility(
+                        visible = !visible,
+                        enter = fadeIn(tween(100, easing = LinearEasing)),
+                        exit = ExitTransition.None
+                    ) {
+                        exit = transition
+                        Box(
+                            Modifier
+                                .sharedElement(
+                                    rememberSharedContentState(key = "child"),
+                                    this@AnimatedVisibility,
+                                    renderInOverlayDuringTransition = false
+                                )
+                                .background(Color.Blue)
+                                .fillMaxSize()
+                        )
+                    }
+                }
+            }
+        }
+        rule.waitForIdle()
+        assertFalse(transitionScope!!.isTransitionActive)
+        rule.onNodeWithTag("scope").run {
+            assertExists("Error: Node doesn't exist")
+            captureToImage().run {
+                assertPixels {
+                    Color.Red
+                }
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        visible = false
+
+        while (transitionScope?.isTransitionActive != true) {
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+
+        // Now shared bounds transition started
+        while (transitionScope?.isTransitionActive != false) {
+            val fraction = ((exit!!.playTimeNanos / 1000_000L) / 100f).coerceIn(0f, 1f)
+            rule.onNodeWithTag("scope").run {
+                assertExists("Error: Node doesn't exist")
+                captureToImage().run {
+                    assertPixels {
+                        Color.Blue.copy(alpha = fraction).compositeOver(Color.White)
+                    }
+                }
+            }
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test
+    fun testZIndexInOverlay() {
+        var transitionScope: SharedTransitionScope? = null
+        var visible by mutableStateOf(true)
+        var greenZIndex by mutableStateOf(0f)
+        var redZIndex by mutableStateOf(0f)
+        var blueZIndex by mutableStateOf(0f)
+
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                SharedTransitionLayout(
+                    Modifier
+                        .testTag("scope")
+                        .requiredSize(100.dp)
+                        .background(Color.White)
+                ) {
+                    transitionScope = this
+
+                    Box(
+                        Modifier
+                            .renderInSharedTransitionScopeOverlay(
+                                zIndexInOverlay = greenZIndex
+                            )
+                            .background(Color.Green)
+                            .fillMaxSize()
+                    )
+
+                    AnimatedVisibility(
+                        visible = visible,
+                        enter = EnterTransition.None,
+                        exit = fadeOut(tween(100, easing = LinearEasing)),
+                    ) {
+                        Box(
+                            Modifier
+                                .sharedBounds(
+                                    rememberSharedContentState(key = "child"),
+                                    this@AnimatedVisibility,
+                                    enter = EnterTransition.None,
+                                    exit = ExitTransition.None,
+                                    zIndexInOverlay = redZIndex
+                                )
+                                .background(Color.Red)
+                                .fillMaxSize()
+                        )
+                    }
+                    AnimatedVisibility(
+                        visible = !visible,
+                        enter = fadeIn(tween(100, easing = LinearEasing)),
+                        exit = ExitTransition.None
+                    ) {
+                        Box(
+                            Modifier
+                                .sharedBounds(
+                                    rememberSharedContentState(key = "child"),
+                                    this@AnimatedVisibility,
+                                    enter = EnterTransition.None,
+                                    exit = ExitTransition.None,
+                                    zIndexInOverlay = blueZIndex
+                                )
+                                .background(Color.Blue)
+                                .fillMaxSize()
+                        )
+                    }
+                }
+            }
+        }
+        rule.waitForIdle()
+        assertFalse(transitionScope!!.isTransitionActive)
+        rule.onNodeWithTag("scope").run {
+            assertExists("Error: Node doesn't exist")
+            captureToImage().run {
+                assertPixels {
+                    Color.Red
+                }
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        visible = false
+        greenZIndex = 0f
+        redZIndex = 0f
+        blueZIndex = 1f
+        var expectedTopColor = Color.Blue
+        var i = 0
+
+        while (transitionScope?.isTransitionActive != true) {
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+
+        // Now shared bounds transition started
+        while (transitionScope?.isTransitionActive != false) {
+            rule.onNodeWithTag("scope").run {
+                assertExists("Error: Node doesn't exist")
+                captureToImage().run {
+                    assertPixels {
+                        expectedTopColor
+                    }
+                }
+            }
+
+            greenZIndex = i.toFloat()
+            redZIndex = i.toFloat()
+            blueZIndex = i.toFloat()
+            when (i) {
+                0 -> {
+                    redZIndex++
+                    expectedTopColor = Color.Red
+                }
+
+                1 -> {
+                    greenZIndex++
+                    expectedTopColor = Color.Green
+                }
+
+                2 -> {
+                    blueZIndex++
+                    expectedTopColor = Color.Blue
+                }
+            }
+            i = (i + 1) % 3
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+    }
+
+    @Test
+    fun testSkipToLookahead() {
+        // Set a clip shape on the shared element that is change both size and position, and check
+        // that the shape is being updated per frame.
+        var transitionScope: SharedTransitionScope? = null
+        var visible by mutableStateOf(true)
+        var size: IntSize? = null
+
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                SharedTransitionLayout(
+                    Modifier
+                        .requiredSize(100.dp)
+                        .testTag("scope")
+                        .background(Color.White)
+                ) {
+                    transitionScope = this
+                    AnimatedVisibility(
+                        visible = visible,
+                        enter = EnterTransition.None,
+                        exit = ExitTransition.None
+                    ) {
+                        Box(
+                            Modifier
+                                .sharedElement(rememberSharedContentState(key = "child"),
+                                    this@AnimatedVisibility,
+                                    boundsTransform = BoundsTransform { _, _ ->
+                                        tween(100, easing = LinearEasing)
+                                    })
+                                .fillMaxSize()
+                        )
+                    }
+                    AnimatedVisibility(
+                        modifier = Modifier.fillMaxSize(),
+                        visible = !visible,
+                        enter = fadeIn(tween(100)),
+                        exit = ExitTransition.None
+                    ) {
+                        Box(Modifier
+                            .padding(25.dp)
+                            .sharedElement(rememberSharedContentState(key = "child"),
+                                this@AnimatedVisibility,
+                                clipInOverlayDuringTransition = OverlayClip(CircleShape),
+                                boundsTransform = BoundsTransform { _, _ ->
+                                    tween(100, easing = LinearEasing)
+                                })
+                            .skipToLookaheadSize()
+                            .onGloballyPositioned {
+                                size = it.size
+                            }
+                            .background(Color.Blue))
+                    }
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        assertFalse(transitionScope!!.isTransitionActive)
+        assertNull(size)
+
+        rule.mainClock.autoAdvance = false
+        visible = false
+
+        while (transitionScope?.isTransitionActive != true) {
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+
+        // Now shared bounds transition started
+        while (transitionScope?.isTransitionActive != false) {
+            // Check that child size has skipped to lookahead size
+            assertEquals(IntSize(50, 50), size)
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test
+    fun testRenderInOverlay() {
+        var transitionScope: SharedTransitionScope? = null
+        var visible by mutableStateOf(true)
+
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                SharedTransitionLayout(
+                    Modifier
+                        .testTag("scope")
+                        .requiredSize(100.dp)
+                        .background(Color.White)
+                ) {
+                    transitionScope = this
+                    Box(
+                        Modifier
+                            .renderInSharedTransitionScopeOverlay(
+                                zIndexInOverlay = 1f
+                            )
+                            .background(Color.Green)
+                            .fillMaxSize()
+                    )
+
+                    AnimatedVisibility(
+                        visible = visible,
+                        enter = EnterTransition.None,
+                        exit = ExitTransition.None
+                    ) {
+                        Box(
+                            Modifier
+                                .fillMaxSize()
+                                .wrapContentSize(align = Alignment.BottomEnd),
+                        ) {
+                            Box(
+                                Modifier
+                                    .sharedBounds(
+                                        rememberSharedContentState(key = "child"),
+                                        this@AnimatedVisibility
+                                    )
+                                    .fillMaxSize(0.5f)
+                                    .background(Color.Red)
+                            )
+                        }
+                    }
+                    AnimatedVisibility(
+                        visible = !visible,
+                        enter = fadeIn(tween(100)),
+                        exit = ExitTransition.None
+                    ) {
+                        Box(
+                            Modifier
+                                .sharedBounds(
+                                    rememberSharedContentState(key = "child"),
+                                    this@AnimatedVisibility,
+                                )
+                                .fillMaxSize(0.5f)
+                                .background(Color.Blue)
+                        )
+                    }
+                }
+            }
+        }
+        rule.waitForIdle()
+        assertFalse(transitionScope!!.isTransitionActive)
+        // tolerance due to AA
+        val tolerance = 2
+        rule.onNodeWithTag("scope").run {
+            assertExists("Error: Node doesn't exist")
+            captureToImage().run {
+                assertPixels {
+                    if (it.x > width / 2 + tolerance && it.y > height / 2 + tolerance) {
+                        Color.Red
+                    } else if (it.x < width / 2 - tolerance || it.y < height / 2 - tolerance) {
+                        Color.Green
+                    } else null
+                }
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        visible = false
+
+        while (transitionScope?.isTransitionActive != true) {
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+
+        // Now shared bounds transition started
+        while (transitionScope?.isTransitionActive != false) {
+            rule.onNodeWithTag("scope").run {
+                assertExists("Error: Node doesn't exist")
+                captureToImage().run {
+                    // Check clipping
+                    assertPixels {
+                        Color.Green
+                    }
+                }
+            }
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+    }
+
+    @Test
+    fun testSharedContentStateClipPathInOverlay() {
+        var transitionScope: SharedTransitionScope? = null
+        var visible by mutableStateOf(true)
+        var parentSharedContentState: SharedTransitionScope.SharedContentState? = null
+        var childSharedContentState: SharedTransitionScope.SharedContentState? = null
+
+        var clippedParentSharedContentState: SharedTransitionScope.SharedContentState? = null
+        var clippedChildSharedContentState: SharedTransitionScope.SharedContentState? = null
+        val predefinedPath = Path().apply {
+            addRect(Rect(5f, 5f, 6f, 6f))
+        }
+
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                SharedTransitionLayout(
+                    Modifier
+                        .requiredSize(100.dp)
+                        .background(Color.White)
+                ) {
+                    transitionScope = this
+                    AnimatedVisibility(
+                        visible = visible,
+                        enter = EnterTransition.None,
+                        exit = ExitTransition.None
+                    ) {
+                        Box(
+                            Modifier
+                                .sharedBounds(
+                                    rememberSharedContentState(key = "parent").also {
+                                        parentSharedContentState = it
+                                    },
+                                    this@AnimatedVisibility,
+                                )
+                                .fillMaxSize(),
+                            contentAlignment = Alignment.Center
+                        ) {
+                            Box(
+                                Modifier
+                                    .fillMaxSize(0.5f)
+                                    .sharedElement(
+                                        rememberSharedContentState(key = "child").also {
+                                            childSharedContentState = it
+                                        },
+                                        this@AnimatedVisibility
+                                    )
+                            )
+                        }
+                    }
+                    AnimatedVisibility(
+                        modifier = Modifier
+                            .fillMaxSize(0.5f)
+                            .offset(x = 25.dp, y = 25.dp),
+                        visible = !visible,
+                        enter = fadeIn(tween(100)),
+                        exit = ExitTransition.None
+                    ) {
+                        Box(
+                            Modifier
+                                .offset(20.dp)
+                                .sharedBounds(
+                                    rememberSharedContentState(key = "parent").also {
+                                        clippedParentSharedContentState = it
+                                    },
+                                    this@AnimatedVisibility,
+                                    clipInOverlayDuringTransition = object :
+                                        SharedTransitionScope.OverlayClip {
+                                        override fun getClipPath(
+                                            state: SharedTransitionScope.SharedContentState,
+                                            bounds: Rect,
+                                            layoutDirection: LayoutDirection,
+                                            density: Density
+                                        ): Path? {
+                                            return predefinedPath
+                                        }
+                                    }
+                                )) {
+                            Box(
+                                Modifier
+                                    .offset(-20.dp)
+                                    .sharedElement(
+                                        rememberSharedContentState(key = "child").also {
+                                            clippedChildSharedContentState = it
+                                        },
+                                        this@AnimatedVisibility,
+                                    )
+                                    .fillMaxSize(),
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        assertFalse(transitionScope!!.isTransitionActive)
+
+        rule.mainClock.autoAdvance = false
+        visible = false
+
+        while (transitionScope?.isTransitionActive != true) {
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+        // Pulse another frame to ensure the rendering has happened
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeByFrame()
+
+        // Now shared bounds transition started
+        while (transitionScope?.isTransitionActive != false) {
+            // Check that the custom clip is picked up by both parent and child shared states
+            assertEquals(predefinedPath, clippedParentSharedContentState!!.clipPathInOverlay)
+            assertEquals(predefinedPath, clippedChildSharedContentState!!.clipPathInOverlay)
+            assertEquals(null, parentSharedContentState!!.clipPathInOverlay)
+            assertEquals(null, childSharedContentState!!.clipPathInOverlay)
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+    }
+
+    @Test
+    fun testParentSharedContentState() {
+        var transitionScope: SharedTransitionScope? = null
+        var visible by mutableStateOf(true)
+        var parentSharedContentState: SharedTransitionScope.SharedContentState? = null
+        var childSharedContentState: SharedTransitionScope.SharedContentState? = null
+
+        var clippedParentSharedContentState: SharedTransitionScope.SharedContentState? = null
+        var clippedChildSharedContentState: SharedTransitionScope.SharedContentState? = null
+
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                SharedTransitionLayout(
+                    Modifier
+                        .requiredSize(100.dp)
+                        .background(Color.White)
+                ) {
+                    transitionScope = this
+                    AnimatedVisibility(
+                        visible = visible,
+                        enter = EnterTransition.None,
+                        exit = ExitTransition.None
+                    ) {
+                        Box(
+                            Modifier
+                                .sharedBounds(
+                                    rememberSharedContentState(key = "parent").also {
+                                        parentSharedContentState = it
+                                    },
+                                    this@AnimatedVisibility,
+                                )
+                                .fillMaxSize(),
+                            contentAlignment = Alignment.Center
+                        ) {
+                            Box(
+                                Modifier
+                                    .fillMaxSize(0.5f)
+                                    .sharedElement(
+                                        rememberSharedContentState(key = "child").also {
+                                            childSharedContentState = it
+                                        },
+                                        this@AnimatedVisibility
+                                    )
+                            )
+                        }
+                    }
+                    AnimatedVisibility(
+                        modifier = Modifier
+                            .fillMaxSize(0.5f)
+                            .offset(x = 25.dp, y = 25.dp),
+                        visible = !visible,
+                        enter = fadeIn(tween(100)),
+                        exit = ExitTransition.None
+                    ) {
+                        Box(
+                            Modifier
+                                .offset(20.dp)
+                                .sharedBounds(
+                                    rememberSharedContentState(key = "parent").also {
+                                        clippedParentSharedContentState = it
+                                    },
+                                    this@AnimatedVisibility,
+                                )
+                        ) {
+                            Box(
+                                Modifier
+                                    .offset(-20.dp)
+                                    .sharedElement(
+                                        rememberSharedContentState(key = "child").also {
+                                            clippedChildSharedContentState = it
+                                        },
+                                        this@AnimatedVisibility,
+                                    )
+                                    .fillMaxSize(),
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        assertFalse(transitionScope!!.isTransitionActive)
+
+        rule.mainClock.autoAdvance = false
+        visible = false
+
+        while (transitionScope?.isTransitionActive != true) {
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+        // Pulse another frame to ensure the rendering has happened
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeByFrame()
+
+        // Now shared bounds transition started
+        while (transitionScope?.isTransitionActive != false) {
+            // Check that the custom clip is picked up by both parent and child shared states
+            assertNotNull(parentSharedContentState)
+            assertEquals(
+                parentSharedContentState,
+                childSharedContentState!!.parentSharedContentState
+            )
+            assertNotNull(clippedParentSharedContentState)
+            assertEquals(
+                clippedParentSharedContentState,
+                clippedChildSharedContentState!!.parentSharedContentState
+            )
+
+            assertTrue(parentSharedContentState!!.isMatchFound)
+            assertTrue(childSharedContentState!!.isMatchFound)
+            assertTrue(clippedParentSharedContentState!!.isMatchFound)
+            assertTrue(clippedChildSharedContentState!!.isMatchFound)
+            rule.waitForIdle()
+            rule.mainClock.advanceTimeByFrame()
+        }
+    }
+}
+
+private fun assertEquals(a: IntSize, b: IntSize, delta: IntSize) {
+    assertEquals(a.width.toFloat(), b.width.toFloat(), delta.width.toFloat())
+    assertEquals(a.height.toFloat(), b.height.toFloat(), delta.height.toFloat())
+}
+
+private fun assertEquals(a: Offset, b: Offset, delta: Offset) {
+    assertEquals(a.x, b.x, delta.x)
+    assertEquals(a.y, b.y, delta.y)
+}
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedVisibility.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedVisibility.kt
index 970d9ca..71d31fb 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedVisibility.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedVisibility.kt
@@ -670,7 +670,7 @@
             properties["label"] = label
         }
     ) {
-        this.then(transition.createModifier(enter, exit, label))
+        this.then(transition.createModifier(enter, exit, label = label))
     }
 }
 
@@ -812,7 +812,7 @@
                 content = { scope.content() },
                 modifier = modifier
                     .then(childTransition
-                        .createModifier(enter, exit, "Built-in")
+                        .createModifier(enter, exit, label = "Built-in")
                         .then(if (onLookaheadMeasured != null) {
                             Modifier.layout { measurable, constraints ->
                                 measurable
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/BoundsAnimation.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/BoundsAnimation.kt
new file mode 100644
index 0000000..42017bc
--- /dev/null
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/BoundsAnimation.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation
+
+import androidx.compose.animation.core.AnimationVector4D
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.VisibilityThreshold
+import androidx.compose.animation.core.spring
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.geometry.Rect
+
+@ExperimentalSharedTransitionApi
+internal class BoundsAnimation(
+    val transitionScope: SharedTransitionScope,
+    val transition: Transition<Boolean>,
+    animation: Transition<Boolean>.DeferredAnimation<Rect, AnimationVector4D>,
+    boundsTransform: BoundsTransform
+) {
+    var animation: Transition<Boolean>.DeferredAnimation<Rect, AnimationVector4D>
+        by mutableStateOf(animation)
+        private set
+
+    fun updateAnimation(
+        animation: Transition<Boolean>.DeferredAnimation<Rect, AnimationVector4D>,
+        boundsTransform: BoundsTransform,
+    ) {
+        if (this.animation != animation) {
+            this.animation = animation
+            animationState = null
+            animationSpec = DefaultBoundsAnimation
+        }
+        this.boundsTransform = boundsTransform
+    }
+
+    private var boundsTransform: BoundsTransform by mutableStateOf(boundsTransform)
+
+    val isRunning: Boolean
+        get() {
+            var parent: Transition<*> = transition
+            while (parent.parentTransition != null) {
+                parent = parent.parentTransition!!
+            }
+            return parent.currentState != parent.targetState
+        }
+
+    var animationSpec: FiniteAnimationSpec<Rect> = DefaultBoundsAnimation
+
+    // It's important to back this state up by a mutable state, so that whoever read it when
+    // it was null will get an invalidation when it's set.
+    var animationState: State<Rect>? by mutableStateOf(null)
+    val value: Rect?
+        get() = if (transitionScope.isTransitionActive) {
+            animationState?.value
+        } else {
+            null
+        }
+
+    fun animate(currentBounds: Rect, targetBounds: Rect) {
+        if (transitionScope.isTransitionActive) {
+            if (animationState == null) {
+                // Only invoke bounds transform when animation is initialized. This means
+                // boundsTransform will not participate in interruption-handling animations.
+                animationSpec = boundsTransform.transform(currentBounds, targetBounds)
+            }
+            animationState = animation.animate(transitionSpec = { animationSpec }) {
+                if (it == transition.targetState) {
+                    // its own bounds
+                    targetBounds
+                } else {
+                    currentBounds
+                }
+            }
+        }
+    }
+
+    val target: Boolean get() = transition.targetState
+}
+
+private val DefaultBoundsAnimation = spring(
+    stiffness = Spring.StiffnessMediumLow,
+    visibilityThreshold = Rect.VisibilityThreshold
+)
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt
index c2a1136..f4f0b56 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt
@@ -854,6 +854,7 @@
 internal fun Transition<EnterExitState>.createModifier(
     enter: EnterTransition,
     exit: ExitTransition,
+    isEnabled: () -> Boolean = { true },
     label: String
 ): Modifier {
     val activeEnter = trackActiveEnter(enter = enter)
@@ -884,11 +885,13 @@
 
     val graphicsLayerBlock = createGraphicsLayerBlock(activeEnter, activeExit, label)
     return Modifier
-        .graphicsLayer(clip = !disableClip)
+        .graphicsLayer {
+            clip = !disableClip && isEnabled()
+        }
         .then(
             EnterExitTransitionElement(
                 this, sizeAnimation, offsetAnimation, slideAnimation,
-                activeEnter, activeExit, graphicsLayerBlock
+                activeEnter, activeExit, isEnabled, graphicsLayerBlock
             )
         )
 }
@@ -1058,6 +1061,7 @@
     var slideAnimation: Transition<EnterExitState>.DeferredAnimation<IntOffset, AnimationVector2D>?,
     var enter: EnterTransition,
     var exit: ExitTransition,
+    var isEnabled: () -> Boolean,
     var graphicsLayerBlock: GraphicsLayerBlockForEnterExit
 ) : LayoutModifierNodeWithPassThroughIntrinsics() {
 
@@ -1149,7 +1153,7 @@
             return layout(measuredSize.width, measuredSize.height) {
                 placeable.place(0, 0)
             }
-        } else {
+        } else if (isEnabled()) {
             val layerBlock = graphicsLayerBlock.init()
             // Measure the content based on the current constraints passed down from parent.
             // AnimatedContent will measure outgoing children with a cached constraints to avoid
@@ -1176,6 +1180,13 @@
                     offset.x + offsetDelta.x, offset.y + offsetDelta.y, 0f, layerBlock
                 )
             }
+        } else {
+            // If not enabled, skip all animations
+            return measurable.measure(constraints).run {
+                layout(width, height) {
+                    place(0, 0)
+                }
+            }
         }
     }
 
@@ -1216,12 +1227,13 @@
     var slideAnimation: Transition<EnterExitState>.DeferredAnimation<IntOffset, AnimationVector2D>?,
     var enter: EnterTransition,
     var exit: ExitTransition,
+    var isEnabled: () -> Boolean,
     var graphicsLayerBlock: GraphicsLayerBlockForEnterExit
 ) : ModifierNodeElement<EnterExitTransitionModifierNode>() {
     override fun create(): EnterExitTransitionModifierNode =
         EnterExitTransitionModifierNode(
             transition, sizeAnimation, offsetAnimation, slideAnimation, enter, exit,
-            graphicsLayerBlock
+            isEnabled, graphicsLayerBlock
         )
 
     override fun update(node: EnterExitTransitionModifierNode) {
@@ -1231,6 +1243,7 @@
         node.slideAnimation = slideAnimation
         node.enter = enter
         node.exit = exit
+        node.isEnabled = isEnabled
         node.graphicsLayerBlock = graphicsLayerBlock
     }
 
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/ExperimentalSharedTransitionApi.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/ExperimentalSharedTransitionApi.kt
new file mode 100644
index 0000000..47e60b6
--- /dev/null
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/ExperimentalSharedTransitionApi.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation
+
+@RequiresOptIn(message = "This is an experimental shared transition API.")
+@Target(
+    AnnotationTarget.CLASS,
+    AnnotationTarget.FUNCTION,
+    AnnotationTarget.PROPERTY,
+    AnnotationTarget.FIELD,
+    AnnotationTarget.PROPERTY_GETTER,
+)
+@Retention(AnnotationRetention.BINARY)
+annotation class ExperimentalSharedTransitionApi
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/RenderInTransitionOverlayNodeElement.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/RenderInTransitionOverlayNodeElement.kt
new file mode 100644
index 0000000..74e4d20
--- /dev/null
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/RenderInTransitionOverlayNodeElement.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalSharedTransitionApi::class)
+
+package androidx.compose.animation
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.drawscope.clipPath
+import androidx.compose.ui.graphics.drawscope.translate
+import androidx.compose.ui.graphics.layer.GraphicsLayer
+import androidx.compose.ui.graphics.layer.drawLayer
+import androidx.compose.ui.modifier.ModifierLocalModifierNode
+import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.requireDensity
+import androidx.compose.ui.node.requireGraphicsContext
+import androidx.compose.ui.node.requireLayoutCoordinates
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+
+internal data class RenderInTransitionOverlayNodeElement(
+    var sharedTransitionScope: SharedTransitionScope,
+    var renderInOverlay: () -> Boolean,
+    val zIndexInOverlay: Float,
+    val clipInOverlay: (LayoutDirection, Density) -> Path?
+) : ModifierNodeElement<RenderInTransitionOverlayNode>() {
+    override fun create(): RenderInTransitionOverlayNode {
+        return RenderInTransitionOverlayNode(
+            sharedTransitionScope, renderInOverlay, zIndexInOverlay, clipInOverlay
+        )
+    }
+
+    override fun update(node: RenderInTransitionOverlayNode) {
+        node.sharedScope = sharedTransitionScope
+        node.renderInOverlay = renderInOverlay
+        node.zIndexInOverlay = zIndexInOverlay
+        node.clipInOverlay = clipInOverlay
+    }
+
+    override fun hashCode(): Int =
+        ((sharedTransitionScope.hashCode() * 31 + renderInOverlay.hashCode()) * 31 +
+            zIndexInOverlay.hashCode()) * 31 + clipInOverlay.hashCode()
+
+    override fun equals(other: Any?): Boolean {
+        if (other is RenderInTransitionOverlayNodeElement) {
+            return sharedTransitionScope == other.sharedTransitionScope &&
+                renderInOverlay == other.renderInOverlay &&
+                zIndexInOverlay == other.zIndexInOverlay &&
+                clipInOverlay == other.clipInOverlay
+        }
+        return false
+    }
+
+    override fun InspectorInfo.inspectableProperties() {
+        name = "renderInSharedTransitionOverlay"
+        properties["sharedTransitionScope"] = sharedTransitionScope
+        properties["renderInOverlay"] = renderInOverlay
+        properties["zIndexInOverlay"] = zIndexInOverlay
+        properties["clipInOverlayDuringTransition"] = clipInOverlay
+    }
+}
+
+internal class RenderInTransitionOverlayNode(
+    var sharedScope: SharedTransitionScope,
+    var renderInOverlay: () -> Boolean,
+    zIndexInOverlay: Float,
+    var clipInOverlay: (LayoutDirection, Density) -> Path?,
+) : Modifier.Node(), DrawModifierNode, ModifierLocalModifierNode {
+    var zIndexInOverlay by mutableFloatStateOf(zIndexInOverlay)
+
+    val parentState: SharedElementInternalState?
+        get() = ModifierLocalSharedElementInternalState.current
+
+    private inner class LayerWithRenderer(val layer: GraphicsLayer) : LayerRenderer {
+        override val parentState: SharedElementInternalState?
+            get() = this@RenderInTransitionOverlayNode.parentState
+
+        override val zIndex: Float
+            get() = this@RenderInTransitionOverlayNode.zIndexInOverlay
+
+        override fun drawInOverlay(drawScope: DrawScope) {
+            if (renderInOverlay()) {
+                with(drawScope) {
+                    val (x, y) = sharedScope.root.localPositionOf(
+                        this@RenderInTransitionOverlayNode.requireLayoutCoordinates(), Offset.Zero
+                    )
+                    val clipPath = clipInOverlay(layoutDirection, requireDensity())
+                    if (clipPath != null) {
+                        clipPath(clipPath) {
+                            translate(x, y) {
+                                drawLayer(layer)
+                            }
+                        }
+                    } else {
+                        translate(x, y) {
+                            drawLayer(layer)
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    // Render in-place logic. Depending on the result of `renderInOverlay()`, the content will
+    // either render in-place or in the overlay, but never in both places.
+    override fun ContentDrawScope.draw() {
+        val layer = requireNotNull(layer) {
+            "Error: layer never initialized"
+        }
+        layer.record {
+            this@draw.drawContent()
+        }
+        if (!renderInOverlay()) {
+            drawLayer(layer)
+        }
+    }
+
+    val layer: GraphicsLayer?
+        get() = layerWithRenderer?.layer
+    private var layerWithRenderer: LayerWithRenderer? = null
+    override fun onAttach() {
+        LayerWithRenderer(requireGraphicsContext().createGraphicsLayer()).let {
+            sharedScope.onLayerRendererCreated(it)
+            layerWithRenderer = it
+        }
+    }
+
+    override fun onDetach() {
+        layerWithRenderer?.let {
+            sharedScope.onLayerRendererRemoved(it)
+            requireGraphicsContext().releaseGraphicsLayer(it.layer)
+        }
+    }
+}
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SharedContentNode.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SharedContentNode.kt
new file mode 100644
index 0000000..f4ca8a5
--- /dev/null
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SharedContentNode.kt
@@ -0,0 +1,271 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalSharedTransitionApi::class)
+
+package androidx.compose.animation
+
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.layer.GraphicsLayer
+import androidx.compose.ui.graphics.layer.drawLayer
+import androidx.compose.ui.layout.ApproachLayoutModifierNode
+import androidx.compose.ui.layout.ApproachMeasureScope
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.modifier.ModifierLocalModifierNode
+import androidx.compose.ui.modifier.modifierLocalMapOf
+import androidx.compose.ui.modifier.modifierLocalOf
+import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.requireDensity
+import androidx.compose.ui.node.requireGraphicsContext
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.roundToIntSize
+import androidx.compose.ui.util.fastRoundToInt
+
+internal data class SharedBoundsNodeElement(
+    val sharedElementState: SharedElementInternalState
+) : ModifierNodeElement<SharedBoundsNode>() {
+    override fun create(): SharedBoundsNode =
+        SharedBoundsNode(sharedElementState)
+
+    override fun update(node: SharedBoundsNode) {
+        node.state = sharedElementState
+    }
+
+    override fun InspectorInfo.inspectableProperties() {
+        name = "sharedBounds"
+        properties["sharedElementState"] = sharedElementState
+    }
+}
+
+/**
+ * SharedContentNode is a Modifier.Node that dynamically acquire target bounds and animating the
+ * layout bounds for Modifier.sharedElement and Modifier.sharedBounds.
+ *
+ * The target bounds are calculated during the lookahead pass based for the node that is becoming
+ * visible. Once the target bounds are calculated, the bounds animation will happen during the
+ * approach pass.
+ */
+internal class SharedBoundsNode(
+    state: SharedElementInternalState,
+) : ApproachLayoutModifierNode, Modifier.Node(), DrawModifierNode, ModifierLocalModifierNode {
+    private val rootCoords: LayoutCoordinates get() = sharedElement.scope.root
+    private val rootLookaheadCoords: LayoutCoordinates get() = sharedElement.scope.lookaheadRoot
+
+    var state: SharedElementInternalState = state
+        internal set(value) {
+            if (value != field) {
+                // State changed!
+                field = value
+                if (isAttached) {
+                    provide(ModifierLocalSharedElementInternalState, value)
+                    state.parentState = ModifierLocalSharedElementInternalState.current
+                    state.layer = layer
+                    state.lookaheadCoords = lookaheadCoords
+                    state.lookaheadSize = lookaheadSize
+                }
+            }
+        }
+
+    private var lookaheadCoords: LayoutCoordinates? = state.lookaheadCoords
+        set(value) {
+            state.lookaheadCoords = value
+            field = value
+        }
+    private val boundsAnimation: BoundsAnimation get() = state.boundsAnimation
+    private var lookaheadSize: Size? = state.lookaheadSize
+        set(value) {
+            state.lookaheadSize = value
+            field = value
+        }
+
+    private var layer: GraphicsLayer? = state.layer
+        set(value) {
+            if (value == null) {
+                field?.let {
+                    requireGraphicsContext().releaseGraphicsLayer(it)
+                }
+            } else {
+                state.layer = value
+            }
+            field = value
+        }
+
+    private val sharedElement: SharedElement get() = state.sharedElement
+    override val providedValues =
+        modifierLocalMapOf(ModifierLocalSharedElementInternalState to state)
+
+    override fun onAttach() {
+        super.onAttach()
+        provide(ModifierLocalSharedElementInternalState, state)
+        state.parentState = ModifierLocalSharedElementInternalState.current
+        layer = requireGraphicsContext().createGraphicsLayer()
+    }
+
+    override fun onDetach() {
+        super.onDetach()
+        layer = null
+        state.parentState = null
+        lookaheadCoords = null
+    }
+
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        // Lookahead pass: Record lookahead size and lookahead coordinates
+        val placeable = measurable.measure(constraints)
+        lookaheadSize = Size(placeable.width.toFloat(), placeable.height.toFloat())
+        return layout(placeable.width, placeable.height) {
+            val topLeft = coordinates?.let {
+                lookaheadCoords = it
+                rootLookaheadCoords.localPositionOf(it, Offset.Zero).also { topLeft ->
+                    if (sharedElement.currentBounds == null) {
+                        sharedElement.currentBounds = Rect(
+                            topLeft,
+                            requireNotNull(lookaheadSize) {
+                                "Error: Lookahead measure has not happened."
+                            }
+                        )
+                    }
+                }
+            }
+            placeable.place(0, 0)
+            // Update the lookahead result after child placement, so that child has an
+            // opportunity to use its placement to influence the bounds animation.
+            topLeft?.let {
+                sharedElement.onLookaheadResult(state, lookaheadSize!!, it)
+            }
+        }
+    }
+
+    private fun MeasureScope.place(placeable: Placeable): MeasureResult {
+        val (w, h) = state.placeHolderSize.calculateSize(
+            lookaheadSize!!.roundToIntSize(),
+            IntSize(placeable.width, placeable.height)
+        )
+        return layout(w, h) {
+            // No match
+            if (!sharedElement.foundMatch) {
+                // Update currentBounds
+                coordinates?.updateCurrentBounds()
+                placeable.place(0, 0)
+            } else {
+                // Start animation if needed
+                if (sharedElement.targetBounds != null) {
+                    boundsAnimation.animate(
+                        sharedElement.currentBounds!!,
+                        sharedElement.targetBounds!!
+                    )
+                }
+                val animatedBounds = boundsAnimation.value
+                val positionInScope =
+                    coordinates?.let { rootCoords.localPositionOf(it, Offset.Zero) }
+                val topLeft: Offset
+
+                // animation finished at visible
+                if (animatedBounds != null) {
+                    // Update CurrentBounds as needed
+                    if (boundsAnimation.target) {
+                        sharedElement.currentBounds = animatedBounds
+                    }
+                    topLeft = animatedBounds.topLeft
+                } else {
+                    if (boundsAnimation.target) {
+                        coordinates?.updateCurrentBounds()
+                    }
+                    topLeft = sharedElement.currentBounds!!.topLeft
+                }
+                val (x, y) = positionInScope?.let { topLeft - it } ?: Offset.Zero
+                placeable.place(x.fastRoundToInt(), y.fastRoundToInt())
+            }
+        }
+    }
+
+    override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
+        return sharedElement.foundMatch && state.sharedElement.scope.isTransitionActive
+    }
+
+    @ExperimentalComposeUiApi
+    override fun ApproachMeasureScope.approachMeasure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        // Approach pass. Animation may not have started, or if the animation isn't
+        // running, we'll measure with current bounds.
+        val resolvedConstraints = if (!sharedElement.foundMatch) {
+            constraints
+        } else {
+            (boundsAnimation.value ?: sharedElement.currentBounds)?.let {
+                val (width, height) = it.size.roundToIntSize()
+                require(
+                    width != Constraints.Infinity &&
+                        height != Constraints.Infinity
+                ) {
+                    "Error: Infinite width/height is invalid. " +
+                        "animated bounds: ${boundsAnimation.value}," +
+                        " current bounds: ${sharedElement.currentBounds}"
+                }
+                Constraints.fixed(width.coerceAtLeast(0), height.coerceAtLeast(0))
+            } ?: constraints
+        }
+        val placeable = measurable.measure(resolvedConstraints)
+        return place(placeable)
+    }
+
+    private fun LayoutCoordinates.updateCurrentBounds() {
+        sharedElement.currentBounds =
+            Rect(
+                rootCoords.localPositionOf(this, Offset.Zero),
+                Size(this.size.width.toFloat(), this.size.height.toFloat())
+            )
+    }
+
+    override fun ContentDrawScope.draw() {
+        // Update clipPath
+        state.clipPathInOverlay = state.overlayClip.getClipPath(
+            state.userState,
+            sharedElement.currentBounds!!,
+            layoutDirection,
+            requireDensity()
+        )
+        val layer = requireNotNull(state.layer) {
+            "Error: Layer is null when accessed for shared bounds/element : ${sharedElement.key}," +
+                "target: ${state.boundsAnimation.target}, is attached: $isAttached"
+        }
+
+        layer.record {
+            this@draw.drawContent()
+        }
+        if (state.shouldRenderInPlace) {
+            drawLayer(layer)
+        }
+    }
+}
+
+internal val ModifierLocalSharedElementInternalState =
+    modifierLocalOf<SharedElementInternalState?> { null }
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SharedElement.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SharedElement.kt
new file mode 100644
index 0000000..13841b8
--- /dev/null
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SharedElement.kt
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalSharedTransitionApi::class)
+
+package androidx.compose.animation
+
+import androidx.compose.runtime.RememberObserver
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.SnapshotStateObserver
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.drawscope.clipPath
+import androidx.compose.ui.graphics.drawscope.translate
+import androidx.compose.ui.graphics.layer.GraphicsLayer
+import androidx.compose.ui.graphics.layer.drawLayer
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.util.fastAny
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastForEachReversed
+
+internal class SharedElement(val key: Any, val scope: SharedTransitionScope) {
+    fun isAnimating(): Boolean = states.fastAny { it.boundsAnimation.isRunning } && foundMatch
+
+    // observation is stopped when no states are in the list, started when new state is added
+    private val observer = SnapshotStateObserver { it() }
+
+    private var _targetBounds: Rect? by mutableStateOf(null)
+
+    /**
+     * This should be only read only in the post-lookahead placement pass. It returns null when
+     * there's no shared element/bounds becoming visible (i.e. when only exiting shared elements
+     * are defined, which is an incorrect state).
+     */
+    val targetBounds: Rect?
+        get() {
+            _targetBounds = targetBoundsProvider?.run {
+                Rect(calculateLookaheadOffset(), requireNotNull(lookaheadSize) {
+                    "Error: target has not been lookahead measured."
+                })
+            }
+            return _targetBounds
+        }
+
+    fun updateMatch() {
+        val hasVisibleContent = hasVisibleContent()
+        if (states.size > 1 && hasVisibleContent) {
+            foundMatch = true
+        } else if (scope.isTransitionActive) {
+            // Unrecoverable state when the shared element/bound that is becoming visible
+            // is removed.
+            if (!hasVisibleContent) {
+                foundMatch = false
+            }
+        } else {
+            // Transition not active
+            foundMatch = false
+        }
+        if (states.isNotEmpty()) {
+            observer.observeReads(this, updateMatch, observingVisibilityChange)
+        }
+    }
+
+    var foundMatch: Boolean by mutableStateOf(false)
+        private set
+
+    // Tracks current size, should be continuous
+    var currentBounds: Rect? by mutableStateOf(null)
+
+    internal var targetBoundsProvider: SharedElementInternalState? = null
+        private set
+
+    fun onLookaheadResult(state: SharedElementInternalState, lookaheadSize: Size, topLeft: Offset) {
+        if (state.boundsAnimation.target) {
+            targetBoundsProvider = state
+
+            // Only update bounds when offset is updated so as to not accidentally fire
+            // up animations, only to interrupt them in the same frame later on.
+            if (_targetBounds?.topLeft != topLeft || _targetBounds?.size != lookaheadSize) {
+                val target = Rect(topLeft, lookaheadSize)
+                _targetBounds = target
+                states.fastForEach {
+                    it.boundsAnimation.animate(currentBounds!!, target)
+                }
+            }
+        }
+    }
+
+    /**
+     * Each state comes from a call site of sharedElement/sharedBounds of the same key. In most
+     * cases there will be 1 (i.e. no match) or 2 (i.e. match found) states. In the interrupted
+     * cases, there may be multiple scenes showing simultaneously, resulting in more than 2
+     * shared element states for the same key to be present. In those cases, we expect there to be
+     * only 1 state that is becoming visible, which we will use to derive target bounds. If none
+     * is becoming visible, then we consider this an error case for the lack of target, and
+     * consequently animate none of them.
+     */
+    val states = mutableStateListOf<SharedElementInternalState>()
+
+    private fun hasVisibleContent(): Boolean = states.fastAny { it.boundsAnimation.target }
+
+    /**
+     * This gets called to update the target bounds. The 3 scenarios where
+     * [updateTargetBoundsProvider] is needed
+     * are: when a shared element is 1) added,  2) removed, or 3) getting a target state change.
+     *
+     * This is always called from an effect. Assume all compositional changes have been made in this
+     * call.
+     */
+    fun updateTargetBoundsProvider() {
+        var targetProvider: SharedElementInternalState? = null
+        states.fastForEachReversed {
+            if (it.boundsAnimation.target) {
+                targetProvider = it
+                return@fastForEachReversed
+            }
+        }
+
+        if (targetProvider == this.targetBoundsProvider) return
+        // Update provider
+        this.targetBoundsProvider = targetProvider
+        _targetBounds = null
+    }
+
+    fun onSharedTransitionFinished() {
+        foundMatch = states.size > 1 && hasVisibleContent()
+        _targetBounds = null
+    }
+
+    private val updateMatch: (SharedElement) -> Unit = {
+        updateMatch()
+    }
+
+    private val observingVisibilityChange: () -> Unit = {
+        hasVisibleContent()
+    }
+
+    fun addState(sharedElementState: SharedElementInternalState) {
+        val wasEmpty = states.isEmpty()
+        states.add(sharedElementState)
+        if (wasEmpty) {
+            observer.start()
+        }
+        observer.observeReads(this, updateMatch, observingVisibilityChange)
+    }
+
+    fun removeState(sharedElementState: SharedElementInternalState) {
+        states.remove(sharedElementState)
+        if (states.isEmpty()) {
+            updateMatch()
+            observer.stop()
+        } else {
+            observer.observeReads(this, updateMatch, observingVisibilityChange)
+        }
+    }
+}
+
+internal class SharedElementInternalState(
+    sharedElement: SharedElement,
+    boundsAnimation: BoundsAnimation,
+    placeHolderSize: SharedTransitionScope.PlaceHolderSize,
+    renderOnlyWhenVisible: Boolean,
+    overlayClip: SharedTransitionScope.OverlayClip,
+    renderInOverlayDuringTransition: Boolean,
+    userState: SharedTransitionScope.SharedContentState,
+    zIndex: Float
+) : LayerRenderer, RememberObserver {
+
+    override var zIndex: Float by mutableFloatStateOf(zIndex)
+
+    var renderInOverlayDuringTransition: Boolean by mutableStateOf(renderInOverlayDuringTransition)
+    var sharedElement: SharedElement by mutableStateOf(sharedElement)
+    var boundsAnimation: BoundsAnimation by mutableStateOf(boundsAnimation)
+    var placeHolderSize: SharedTransitionScope.PlaceHolderSize by mutableStateOf(placeHolderSize)
+    var renderOnlyWhenVisible: Boolean by mutableStateOf(renderOnlyWhenVisible)
+    var overlayClip: SharedTransitionScope.OverlayClip by mutableStateOf(overlayClip)
+    var userState: SharedTransitionScope.SharedContentState by mutableStateOf(userState)
+
+    init {
+        sharedElement.scope.onStateAdded(this)
+        sharedElement.updateTargetBoundsProvider()
+    }
+
+    internal var clipPathInOverlay: Path? = null
+
+    override fun drawInOverlay(drawScope: DrawScope) {
+        val layer = layer ?: return
+        if (shouldRenderInOverlay) {
+            with(drawScope) {
+                val (x, y) = sharedElement.currentBounds?.topLeft!!
+                clipPathInOverlay?.let {
+                    clipPath(it) {
+                        translate(x, y) {
+                            drawLayer(layer)
+                        }
+                    }
+                } ?: translate(x, y) { drawLayer(layer) }
+            }
+        }
+    }
+
+    var lookaheadSize: Size? = null
+    var lookaheadCoords: LayoutCoordinates? = null
+    override var parentState: SharedElementInternalState? = null
+
+    // This can only be accessed during placement
+    fun calculateLookaheadOffset(): Offset {
+        val c = requireNotNull(lookaheadCoords) {
+            "Error: target has not been placed in lookahead pass yet."
+        }
+        return sharedElement.scope.lookaheadRoot.localPositionOf(c, Offset.Zero)
+    }
+
+    val target: Boolean get() = boundsAnimation.target
+
+    var layer: GraphicsLayer? = null
+
+    private val shouldRenderBasedOnTarget: Boolean
+        get() = sharedElement.targetBoundsProvider == this || !renderOnlyWhenVisible
+
+    internal val shouldRenderInOverlay: Boolean
+        get() = shouldRenderBasedOnTarget && sharedElement.foundMatch &&
+            renderInOverlayDuringTransition
+
+    val shouldRenderInPlace: Boolean
+        get() = !sharedElement.foundMatch || (!shouldRenderInOverlay && shouldRenderBasedOnTarget)
+
+    override fun onRemembered() {
+    }
+
+    override fun onForgotten() {
+        sharedElement.scope.onStateRemoved(this)
+        sharedElement.updateTargetBoundsProvider()
+    }
+
+    override fun onAbandoned() {}
+}
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SharedTransitionScope.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SharedTransitionScope.kt
new file mode 100644
index 0000000..c00017a
--- /dev/null
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SharedTransitionScope.kt
@@ -0,0 +1,988 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation
+
+import androidx.collection.MutableScatterMap
+import androidx.compose.animation.SharedTransitionScope.PlaceHolderSize
+import androidx.compose.animation.SharedTransitionScope.PlaceHolderSize.Companion.animatedSize
+import androidx.compose.animation.SharedTransitionScope.PlaceHolderSize.Companion.contentSize
+import androidx.compose.animation.SharedTransitionScope.SharedContentState
+import androidx.compose.animation.core.ExperimentalTransitionApi
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.core.Spring.StiffnessMediumLow
+import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.VisibilityThreshold
+import androidx.compose.animation.core.createChildTransition
+import androidx.compose.animation.core.createDeferredAnimation
+import androidx.compose.animation.core.rememberTransition
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.SnapshotStateObserver
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.addOutline
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.constrain
+import androidx.compose.ui.util.fastForEach
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * [SharedTransitionLayout] creates a layout and a [SharedTransitionScope] for the child layouts
+ * in [content]. Any child (direct or indirect) of the [SharedTransitionLayout] can use the
+ * receiver scope [SharedTransitionScope] to create shared element or shared bounds transitions.
+ *
+ * **Note**: [SharedTransitionLayout] creates a new Layout. For use cases where it's preferable
+ * to not introduce a new layout between [content] and the parent layout, consider using
+ * [SharedTransitionScope] instead.
+ *
+ * @param modifier Modifiers to be applied to the layout.
+ * @param content The children composable to be laid out.
+ */
+@ExperimentalSharedTransitionApi
+@Composable
+fun SharedTransitionLayout(
+    modifier: Modifier = Modifier,
+    content: @Composable SharedTransitionScope.() -> Unit
+) {
+    SharedTransitionScope {
+        Box(it.then(modifier)) {
+            content()
+        }
+    }
+}
+
+/**
+ * [SharedTransitionScope] creates a [SharedTransitionScope] for the child layouts
+ * in [content]. Any child (direct or indirect) of the [SharedTransitionLayout] can use the
+ * receiver scope [SharedTransitionScope] to create shared element or shared bounds transitions.
+ * [SharedTransitionScope] will not creates a new Layout.
+ *
+ * **IMPORTANT**: It is important to set the [Modifier] provided to the [content] on the first and
+ * top-most child, as the [Modifier] both obtains the root coordinates and creates an overlay.
+ * If the first child layout in [content] isn't the child with the highest zIndex, consider using
+ * [SharedTransitionLayout] instead.
+ *
+ * @param content The children composable to be laid out.
+ */
+@ExperimentalSharedTransitionApi
+@Composable
+fun SharedTransitionScope(
+    content: @Composable SharedTransitionScope.(Modifier) -> Unit
+) {
+    LookaheadScope {
+        val coroutineScope = rememberCoroutineScope()
+        val sharedScope = remember { SharedTransitionScope(this, coroutineScope) }
+        sharedScope.content(
+            Modifier
+                .layout { measurable, constraints ->
+                    val p = measurable.measure(constraints)
+                    layout(p.width, p.height) {
+                        val coords = coordinates
+                        if (coords != null) {
+                            if (!isLookingAhead) {
+                                sharedScope.root = coords
+                            } else {
+                                sharedScope.lookaheadRoot = coords
+                            }
+                        }
+                        p.place(0, 0)
+                    }
+                }
+                .drawWithContent {
+                    drawContent()
+                    sharedScope.drawInOverlay(this)
+                }
+        )
+    }
+}
+
+/**
+ * [BoundsTransform] defines the animation spec used to animate from initial bounds to the
+ * target bounds.
+ */
+@ExperimentalSharedTransitionApi
+fun interface BoundsTransform {
+    /**
+     * Returns a [FiniteAnimationSpec] for animating the bounds from [initialBounds] to
+     * [targetBounds].
+     */
+    fun transform(initialBounds: Rect, targetBounds: Rect): FiniteAnimationSpec<Rect>
+}
+
+/**
+ * [SharedTransitionScope] provides a coordinator space in which shared elements/ shared bounds
+ * (when matched) will transform their bounds from one to another. Their position animation is
+ * always relative to the origin defined by where [SharedTransitionScope] is in the tree.
+ *
+ * [SharedTransitionScope] also creates an overlay, in which all shared elements and shared bounds
+ * are rendered by default, so that they are not subject to their parent's fading or clipping, and
+ * can therefore transform the bounds without alpha jumps or being unintentionally clipped.
+ *
+ * It is also [SharedTransitionScope]'s responsibility to do the [SharedContentState] key match
+ * for all the [sharedElement] or [sharedBounds] defined in this scope. Note: key match will not
+ * work for [SharedContentState] created in different [SharedTransitionScope]s.
+ *
+ * [SharedTransitionScope] oversees all the animations in its scope. When any of the animations is
+ * active, [isTransitionActive] will be true. Once a bounds transform starts, by default the
+ * shared element or shared bounds will render the content in the overlay. The rendering will remain
+ * in the overlay until all other animations in the [SharedTransitionScope] are finished (i.e.
+ * when [isTransitionActive] == false).
+ */
+@ExperimentalSharedTransitionApi
+@Stable
+class SharedTransitionScope internal constructor(
+    lookaheadScope: LookaheadScope,
+    val coroutineScope: CoroutineScope
+) : LookaheadScope by lookaheadScope {
+
+    /**
+     * PlaceHolderSize defines the size of the space that was or will be occupied by the exiting
+     * or entering [sharedElement]/[sharedBounds].
+     */
+    fun interface PlaceHolderSize {
+        companion object {
+            /**
+             * [animatedSize] is a pre-defined [SharedTransitionScope.PlaceHolderSize] that lets the
+             * parent layout of shared elements or shared bounds observe the animated size during an
+             * active shared transition. Therefore the layout parent will most likely resize itself
+             * and re-layout its children to adjust to the new animated size.
+             *
+             * @see [contentSize]
+             * @see [SharedTransitionScope.PlaceHolderSize]
+             */
+            val animatedSize = PlaceHolderSize { _, animatedSize -> animatedSize }
+
+            /**
+             * [contentSize] is a pre-defined [SharedTransitionScope.PlaceHolderSize] that allows
+             * the parent layout of shared elements or shared bounds to see the content size of the
+             * shared content during an active shared transition. For outgoing content, this
+             * [contentSize] is the initial size before the animation, whereas for incoming content
+             * [contentSize] will return the lookahead/target size of the content. This is the
+             * default value for shared elements and shared bounds. The effect is
+             * that the parent layout does not resize during the shared element transition, hence
+             * giving a sense of stability, rather than dynamic motion. If it's preferred to have
+             * parent layout dynamically adjust its layout based on the shared element's animated
+             * size, consider using [animatedSize].
+             *
+             * @see [contentSize]
+             * @see [SharedTransitionScope.PlaceHolderSize]
+             */
+            val contentSize = PlaceHolderSize { contentSize, _ -> contentSize }
+        }
+
+        /**
+         * Returns the size of the place holder based on [contentSize] and [animatedSize].
+         * Note: [contentSize] for exiting content is the size before it starts exiting.
+         * For entering content, [contentSize] is the lookahead size of the content (i.e. target
+         * size of the shared transition).
+         */
+        fun calculateSize(contentSize: IntSize, animatedSize: IntSize): IntSize
+    }
+
+    /**
+     * Indicates whether there is any ongoing transition between matched [sharedElement] or
+     * [sharedBounds].
+     */
+    var isTransitionActive: Boolean by mutableStateOf(false)
+        private set
+
+    /**
+     * [skipToLookaheadSize] enables a layout to measure its child with the lookahead constraints,
+     * therefore laying out the child as if the transition has finished. This is particularly
+     * helpful for layouts where re-flowing content based on animated constraints is undesirable,
+     * such as texts.
+     *
+     * In the sample below, try remove the [skipToLookaheadSize] modifier and observe the
+     * difference:
+     * @sample androidx.compose.animation.samples.NestedSharedBoundsSample
+     */
+    fun Modifier.skipToLookaheadSize(): Modifier = this.then(SkipToLookaheadElement)
+
+    /**
+     * Renders the content in the [SharedTransitionScope]'s overlay, where shared content (i.e.
+     * shared elements and shared bounds) is rendered by default. This is useful for rendering
+     * content that is not shared on top of shared content to preserve a specific spatial
+     * relationship.
+     *
+     * [renderInOverlay] dynamically controls whether the content should be rendered in the
+     * [SharedTransitionScope]'s overlay. By default, it returns the same value as
+     * [SharedTransitionScope.isTransitionActive]. This means the default behavior is to render
+     * the child layout of this modifier in the overlay only when the transition is active.
+     *
+     * **IMPORTANT:** When elevating layouts into the overlay, the layout is no longer subjected
+     * to 1) its parent's clipping, and 2) parent's layer transform (e.g. alpha, scale, etc).
+     * Therefore, it is recommended to create an enter/exit animation (e.g. using
+     * [AnimatedVisibilityScope.animateEnterExit]) for the child layout to avoid any abrupt visual
+     * changes.
+     *
+     * [clipInOverlayDuringTransition] supports a custom clip path if clipping is desired. By default,
+     * no clipping is applied. Manual management of clipping can often be avoided by putting layouts
+     * with clipping as children of this modifier (i.e. to the right side of this modifier).
+     *
+     * @sample androidx.compose.animation.samples.SharedElementWithFABInOverlaySample
+     */
+    fun Modifier.renderInSharedTransitionScopeOverlay(
+        renderInOverlay: () -> Boolean = defaultRenderInOverlay,
+        zIndexInOverlay: Float = 0f,
+        clipInOverlayDuringTransition: (LayoutDirection, Density) -> Path? =
+            DefaultClipInOverlayDuringTransition
+    ): Modifier =
+        this.then(
+            RenderInTransitionOverlayNodeElement(
+                this@SharedTransitionScope,
+                renderInOverlay,
+                zIndexInOverlay,
+                clipInOverlayDuringTransition
+            )
+        )
+
+    /**
+     * [OverlayClip] defines a specific clipping that should be applied to a [sharedBounds]
+     * or [sharedElement] in the overlay.
+     */
+    interface OverlayClip {
+        /**
+         * Creates a clip path based using current animated [bounds] of the [sharedBounds] or
+         * [sharedElement], their [state] (to query parent state's bounds if needed), and
+         * [layoutDirection] and [density]. The topLeft of the [bounds] is the local position
+         * of the sharedElement/sharedBounds in the [SharedTransitionScope].
+         *
+         * **Important**: The returned [Path] needs to be offset-ed as needed such that it is in
+         * [SharedTransitionScope.lookaheadScopeCoordinates]'s coordinate space. For example,
+         * if the path is created using [bounds], it needs to be offset-ed by [bounds].topLeft.
+         *
+         * It is recommended to modify the same [Path] object and return it here, instead of
+         * creating new [Path]s.
+         */
+        fun getClipPath(
+            state: SharedContentState,
+            bounds: Rect,
+            layoutDirection: LayoutDirection,
+            density: Density
+        ): Path?
+    }
+
+    /**
+     * [sharedElement] is a modifier that tags a layout with a [SharedContentState.key], such that
+     * entering and exiting shared elements of the same key share the animated and continuously
+     * changing bounds during the layout change. The bounds will be animated from the initial
+     * bounds defined by the exiting shared element to the target bounds calculated based on the
+     * incoming shared element. The animation for the bounds can be customized using
+     * [boundsTransform].
+     *
+     * In contrast to [sharedBounds], [sharedElement] is designed for shared content that has the
+     * exact match in terms of visual content and layout when the measure constraints are the same.
+     * Such examples include image assets, icons,
+     * [MovableContent][androidx.compose.runtime.MovableContent] etc.
+     * Only the shared element that is becoming visible will be rendered during the
+     * transition. The bounds for shared element are determined by the bounds of the shared element
+     * becoming visible based on the target state of [animatedVisibilityScope].
+     *
+     * **Important**:
+     * When a shared element finds its match and starts a transition, it will be rendered into
+     * the overlay of the [SharedTransitionScope] in order to avoid being faded in/out along with
+     * its parents or clipped by its parent as it transforms to the target size and position. This
+     * also means that any clipping or fading for the shared elements will need to be applied
+     * _explicitly_ as the child of [sharedElement] (i.e. after [sharedElement] modifier in the
+     * modifier chain). For example:
+     * `Modifier.sharedElement(...).clip(shape = RoundedCornerShape(20.dp)).animateEnterExit(...)`
+     *
+     * By default, the [sharedElement] is clipped by the [clipInOverlayDuringTransition] of its
+     * parent [sharedBounds]. If the [sharedElement] has no parent [sharedBounds] or if the parent
+     * [sharedBounds] has no clipping defined, it'll not be clipped. If additional clipping is
+     * desired to ensure [sharedElement] doesn't move outside of a visual bounds,
+     * [clipInOverlayDuringTransition] can be used to specify the clipping for when the shared element
+     * is going through an active transition towards a new target bounds.
+     *
+     * While the shared elements are rendered in overlay during the transition, its
+     * [zIndexInOverlay] can be specified to allow shared elements to render in a different order
+     * than their placement/zOrder when not in the overlay. For example, the tile of a page is
+     * typically placed and rendered before the content below. During the transition, it may be
+     * desired to animate the title over on top of the other shared elements on that page to
+     * indicate significance or a point of interest. [zIndexInOverlay] can be used to facilitate
+     * such use cases. [zIndexInOverlay] is 0f by default.
+     *
+     * [renderInOverlayDuringTransition] is true by default. In some rare use cases, there may
+     * be no clipping or layer transform (fade, scale, etc) in the application that prevents
+     * shared elements from transitioning from one bounds to another without any clipping or
+     * sudden alpha change. In such cases, [renderInOverlayDuringTransition] could be specified
+     * to false.
+     *
+     * During a shared element transition, the space that was occupied by the exiting shared
+     * element and the space that the entering shared element will take up are considered place
+     * holders. Their sizes during the shared element transition can be configured through
+     * [placeHolderSize]. By default, it will be the same as the content size of the respective
+     * shared element. It can also be set to [animatedSize] or any other [PlaceHolderSize] to
+     * report to their parent layout an animated size to create a visual effect where the parent
+     * layout dynamically adjusts the layout to accommodate the animated size of the shared
+     * elements.
+     *
+     * @sample androidx.compose.animation.samples.SharedElementInAnimatedContentSample
+     *
+     *  @see [sharedBounds]
+     */
+    @OptIn(ExperimentalAnimationApi::class)
+    fun Modifier.sharedElement(
+        state: SharedContentState,
+        animatedVisibilityScope: AnimatedVisibilityScope,
+        boundsTransform: BoundsTransform = DefaultBoundsTransform,
+        placeHolderSize: PlaceHolderSize = contentSize,
+        renderInOverlayDuringTransition: Boolean = true,
+        zIndexInOverlay: Float = 0f,
+        clipInOverlayDuringTransition: OverlayClip = ParentClip
+    ) = this.sharedBoundsImpl(
+        state,
+        parentTransition = animatedVisibilityScope.transition,
+        visible = { it == EnterExitState.Visible },
+        boundsTransform = boundsTransform,
+        placeHolderSize = placeHolderSize,
+        renderOnlyWhenVisible = true,
+        renderInOverlayDuringTransition = renderInOverlayDuringTransition,
+        zIndexInOverlay = zIndexInOverlay,
+        clipInOverlayDuringTransition = clipInOverlayDuringTransition
+    )
+
+    /**
+     * [sharedBounds] is a modifier that tags a layout with a [SharedContentState.key], such that
+     * entering and exiting shared bounds of the same key share the animated and continuously
+     * changing bounds during the layout change. The bounds will be animated from the initial
+     * bounds defined by the exiting shared bounds to the target bounds calculated based on the
+     * incoming shared shared bounds. The animation for the bounds can be customized using
+     * [boundsTransform].
+     *
+     * In contrast to [sharedElement], [sharedBounds] is designed for shared content that has the
+     * visually different content. While the [sharedBounds] keeps the continuity of the bounds,
+     * the incoming and outgoing content within the [sharedBounds] will enter and exit in an
+     * enter/exit transition using [enter]/[exit]. They fade in/out by default.
+     * The target bounds for [sharedBounds] are determined by the bounds of the [sharedBounds]
+     * becoming visible based on the target state of [animatedVisibilityScope].
+     *
+     * **Important**:
+     * When a shared bounds finds its match and starts a transition, it will be rendered into
+     * the overlay of the [SharedTransitionScope] in order to avoid being faded in/out along with
+     * its parents or clipped by its parent as it transforms to the target size and position. This
+     * also means that any clipping or fading for the shared elements will need to be applied
+     * _explicitly_ as the child of [sharedBounds] (i.e. after [sharedBounds] modifier in the
+     * modifier chain). For example:
+     * `Modifier.sharedBounds(...).clip(shape = RoundedCornerShape(20.dp))`
+     *
+     * By default, the [sharedBounds] is clipped by the [clipInOverlayDuringTransition] of its
+     * parent [sharedBounds] in the layout tree. If the [sharedBounds] has no parent [sharedBounds]
+     * or if the parent [sharedBounds] has no clipping defined, it'll not be clipped. If additional
+     * clipping is desired to ensure child [sharedBounds] or child [sharedElement] don't move
+     * outside of the this [sharedBounds]'s visual bounds in the overlay,
+     * [clipInOverlayDuringTransition] can be used to specify the clipping.
+     *
+     * While the shared bounds are rendered in overlay during the transition, its
+     * [zIndexInOverlay] can be specified to allow them to render in a different order
+     * than their placement/zOrder when not in the overlay. For example, the tile of a page is
+     * typically placed and rendered before the content below. During the transition, it may be
+     * desired to animate the title over on top of the other shared elements on that page to
+     * indicate significance or a point of interest. [zIndexInOverlay] can be used to facilitate
+     * such use cases. [zIndexInOverlay] is 0f by default.
+     *
+     * [renderInOverlayDuringTransition] is true by default. In some rare use cases, there may
+     * be no clipping or layer transform (fade, scale, etc) in the application that prevents
+     * shared elements from transitioning from one bounds to another without any clipping or
+     * sudden alpha change. In such cases, [renderInOverlayDuringTransition] could be specified
+     * to false.
+     *
+     * During a shared bounds transition, the space that was occupied by the exiting shared
+     * bounds and the space that the entering shared bounds will take up are considered place
+     * holders. Their sizes during the shared element transition can be configured through
+     * [placeHolderSize]. By default, it will be the same as the content size of the respective
+     * shared bounds. It can also be set to [animatedSize] or any other [PlaceHolderSize] to
+     * report to their parent layout an animated size to create a visual effect where the parent
+     * layout dynamically adjusts the layout to accommodate the animated size of the shared
+     * elements.
+     *
+     * @sample androidx.compose.animation.samples.NestedSharedBoundsSample
+     * @see [sharedBounds]
+     */
+    @OptIn(ExperimentalAnimationApi::class)
+    fun Modifier.sharedBounds(
+        sharedContentState: SharedContentState,
+        animatedVisibilityScope: AnimatedVisibilityScope,
+        enter: EnterTransition = fadeIn(),
+        exit: ExitTransition = fadeOut(),
+        boundsTransform: BoundsTransform = DefaultBoundsTransform,
+        placeHolderSize: PlaceHolderSize = contentSize,
+        renderInOverlayDuringTransition: Boolean = true,
+        zIndexInOverlay: Float = 0f,
+        clipInOverlayDuringTransition: OverlayClip = ParentClip
+    ) =
+        this
+            .sharedBoundsImpl(
+                sharedContentState,
+                animatedVisibilityScope.transition,
+                visible = { it == EnterExitState.Visible },
+                boundsTransform,
+                placeHolderSize = placeHolderSize,
+                renderInOverlayDuringTransition = renderInOverlayDuringTransition,
+                zIndexInOverlay = zIndexInOverlay,
+                clipInOverlayDuringTransition = clipInOverlayDuringTransition,
+                renderOnlyWhenVisible = false
+            )
+            .composed {
+                animatedVisibilityScope.transition.createModifier(
+                    enter = enter,
+                    exit = exit,
+                    isEnabled = { sharedContentState.isMatchFound },
+                    label = "enter/exit for ${sharedContentState.key}"
+                )
+            }
+
+    /**
+     * [sharedElementWithCallerManagedVisibility] is a modifier that tags a layout with a
+     * [SharedContentState.key], such that
+     * entering and exiting shared elements of the same key share the animated and continuously
+     * changing bounds during the layout change. The bounds will be animated from the initial
+     * bounds defined by the exiting shared element to the target bounds calculated based on the
+     * incoming shared element. The animation for the bounds can be customized using
+     * [boundsTransform].
+     *
+     * Compared to [sharedElement], [sharedElementWithCallerManagedVisibility] is designed
+     * for shared element transitions where the shared element is not a part of the content that
+     * is being animated out by [AnimatedVisibility]. Therefore, it is the caller's responsibility
+     * to explicitly remove the exiting shared element (i.e. shared elements where
+     * [visible] == false) from the tree as appropriate. Typically this is when the transition is
+     * finished (i.e. [SharedTransitionScope.isTransitionActive] == false). The target bounds is
+     * derived from the [sharedElementWithCallerManagedVisibility] with [visible] being true.
+     *
+     * In contrast to [sharedBounds], this modifier is intended for shared content that has the
+     * exact match in terms of visual content and layout when the measure constraints are the same.
+     * Such examples include image assets, icons,
+     * [MovableContent][androidx.compose.runtime.MovableContent] etc.
+     * Only the shared element that is becoming visible will be rendered during the
+     * transition.
+     *
+     * **Important**:
+     * When a shared element finds its match and starts a transition, it will be rendered into
+     * the overlay of the [SharedTransitionScope] in order to avoid being faded in/out along with
+     * its parents or clipped by its parent as it transforms to the target size and position. This
+     * also means that any clipping or fading for the shared elements will need to be applied
+     * _explicitly_ as the child of [sharedElementWithCallerManagedVisibility]
+     * (i.e. after [sharedElementWithCallerManagedVisibility] modifier in the
+     * modifier chain). For example:
+     * ```
+     * Modifier.sharedElementWithCallerManagedVisibility(...)
+     *         .clip(shape = RoundedCornerShape(20.dp))
+     * ```
+     *
+     * By default, the [sharedElementWithCallerManagedVisibility] is clipped by the
+     * [clipInOverlayDuringTransition] of its
+     * parent [sharedBounds]. If the [sharedElementWithCallerManagedVisibility] has no parent
+     * [sharedBounds] or if the parent [sharedBounds] has no clipping defined, it'll not be
+     * clipped. If additional clipping is desired to ensure
+     * [sharedElementWithCallerManagedVisibility] doesn't move outside of a visual bounds,
+     * [clipInOverlayDuringTransition] can be used to specify the clipping for when the shared
+     * element is going through an active transition towards a new target bounds.
+     *
+     * While the shared elements are rendered in overlay during the transition, its
+     * [zIndexInOverlay] can be specified to allow shared elements to render in a different order
+     * than their placement/zOrder when not in the overlay. For example, the tile of a page is
+     * typically placed and rendered before the content below. During the transition, it may be
+     * desired to animate the title over on top of the other shared elements on that page to
+     * indicate significance or a point of interest. [zIndexInOverlay] can be used to facilitate
+     * such use cases. [zIndexInOverlay] is 0f by default.
+     *
+     * [renderInOverlayDuringTransition] is true by default. In some rare use cases, there may
+     * be no clipping or layer transform (fade, scale, etc) in the application that prevents
+     * shared elements from transitioning from one bounds to another without any clipping or
+     * sudden alpha change. In such cases, [renderInOverlayDuringTransition] could be specified
+     * to false.
+     *
+     * During a shared element transition, the space that was occupied by the exiting shared
+     * element and the space that the entering shared element will take up are considered place
+     * holders. Their sizes during the shared element transition can be configured through
+     * [placeHolderSize]. By default, it will be the same as the content size of the respective
+     * shared element. It can also be set to [animatedSize] or any other [PlaceHolderSize] to
+     * report to their parent layout an animated size to create a visual effect where the parent
+     * layout dynamically adjusts the layout to accommodate the animated size of the shared
+     * elements.
+     *
+     * @sample androidx.compose.animation.samples.SharedElementWithMovableContentSample
+     */
+    fun Modifier.sharedElementWithCallerManagedVisibility(
+        sharedContentState: SharedContentState,
+        visible: Boolean,
+        boundsTransform: BoundsTransform = DefaultBoundsTransform,
+        placeHolderSize: PlaceHolderSize = contentSize,
+        renderInOverlayDuringTransition: Boolean = true,
+        zIndexInOverlay: Float = 0f,
+        clipInOverlayDuringTransition: OverlayClip = ParentClip
+    ) = this.sharedBoundsImpl<Unit>(
+        sharedContentState,
+        null,
+        { visible },
+        boundsTransform,
+        placeHolderSize,
+        renderOnlyWhenVisible = true,
+        renderInOverlayDuringTransition = renderInOverlayDuringTransition,
+        zIndexInOverlay = zIndexInOverlay,
+        clipInOverlayDuringTransition = clipInOverlayDuringTransition
+    )
+
+    /**
+     * [sharedBoundsWithCallerManagedVisibility] is a modifier that tags a layout with a
+     * [SharedContentState.key], such that
+     * entering and exiting shared bounds of the same key share the animated and continuously
+     * changing bounds during the layout change. The bounds will be animated from the initial
+     * bounds defined by the exiting shared bounds to the target bounds calculated based on the
+     * incoming shared bounds. The animation for the bounds can be customized using
+     * [boundsTransform].
+     *
+     * Compared to [sharedBounds], [sharedBoundsWithCallerManagedVisibility] is designed
+     * for shared bounds transitions where the shared bounds is not a part of the content that
+     * is being animated out by [AnimatedVisibility]. Therefore, it is the caller's responsibility
+     * to explicitly remove the exiting shared bounds (i.e. shared bounds where
+     * [visible] == false) from the tree as appropriate. Typically this is when the transition is
+     * finished (i.e. [SharedTransitionScope.isTransitionActive] == false). The target bounds is
+     * derived from the [sharedBoundsWithCallerManagedVisibility] with [visible] being true.
+     *
+     * Similar to [sharedBounds], [sharedBoundsWithCallerManagedVisibility] is designed for
+     * shared content that has the visually different content. It keeps the
+     * continuity of the bounds. Unlike [sharedBounds], [sharedBoundsWithCallerManagedVisibility]
+     * will not apply any enter transition or exit transition for the incoming and outgoing content
+     * within the bounds. Such enter and exit animation will need to be added by the caller
+     * of this API.
+     *
+     * **Important**:
+     * When a shared bounds finds its match and starts a transition, it will be rendered into
+     * the overlay of the [SharedTransitionScope] in order to avoid being faded in/out along with
+     * its parents or clipped by its parent as it transforms to the target size and position. This
+     * also means that any clipping or fading for the shared elements will need to be applied
+     * _explicitly_ as the child of [sharedBoundsWithCallerManagedVisibility]
+     * (i.e. after [sharedBoundsWithCallerManagedVisibility] modifier in the
+     * modifier chain). For example:
+     * ```
+     * Modifier.sharedBoundsWithCallerManagedVisibility(...)
+     *         .clip(shape = RoundedCornerShape(20.dp))
+     * ```
+     *
+     * By default, the [sharedBoundsWithCallerManagedVisibility] is clipped by the
+     * [clipInOverlayDuringTransition] of its
+     * parent [sharedBounds]. If the [sharedBoundsWithCallerManagedVisibility] has no parent
+     * [sharedBounds] or if the parent [sharedBounds] has no clipping defined, it'll not be
+     * clipped. If additional clipping is desired to ensure
+     * [sharedBoundsWithCallerManagedVisibility] doesn't move outside of a visual bounds,
+     * [clipInOverlayDuringTransition] can be used to specify the clipping for when the shared
+     * bounds is going through an active transition towards a new target bounds.
+     *
+     * While the shared bounds are rendered in overlay during the transition, its
+     * [zIndexInOverlay] can be specified to allow shared bounds to render in a different order
+     * than their placement/zOrder when not in the overlay. For example, the tile of a page is
+     * typically placed and rendered before other layouts. During the transition, it may be
+     * desired to animate the title over on top of the other shared elements on that page to
+     * indicate significance or a point of interest. [zIndexInOverlay] can be used to facilitate
+     * such use cases. [zIndexInOverlay] is 0f by default.
+     *
+     * [renderInOverlayDuringTransition] is true by default. In some rare use cases, there may
+     * be no clipping or layer transform (fade, scale, etc) in the application that prevents
+     * shared bounds from transitioning from one bounds to another without any clipping or
+     * sudden alpha change. In such cases, [renderInOverlayDuringTransition] could be specified
+     * to false.
+     *
+     * During a shared bounds transition, the space that was occupied by the exiting shared
+     * bounds and the space that the entering shared bounds will take up are considered place
+     * holders. Their sizes during the shared bounds transition can be configured through
+     * [placeHolderSize]. By default, it will be the same as the content size of the respective
+     * shared bounds. It can also be set to [animatedSize] or any other [PlaceHolderSize] to
+     * report to their parent layout an animated size to create a visual effect where the parent
+     * layout dynamically adjusts the layout to accommodate the animated size of the shared
+     * bounds.
+     *
+     * // TODO: Evaluate whether this could become a public API
+     */
+    internal fun Modifier.sharedBoundsWithCallerManagedVisibility(
+        sharedContentState: SharedContentState,
+        visible: Boolean,
+        boundsTransform: BoundsTransform = DefaultBoundsTransform,
+        placeHolderSize: PlaceHolderSize = contentSize,
+        renderInOverlayDuringTransition: Boolean = true,
+        zIndexInOverlay: Float = 0f,
+        clipInOverlayDuringTransition: OverlayClip = ParentClip
+    ) = this.sharedBoundsImpl<Unit>(
+        sharedContentState,
+        null,
+        { visible },
+        boundsTransform,
+        placeHolderSize,
+        renderOnlyWhenVisible = false,
+        renderInOverlayDuringTransition = renderInOverlayDuringTransition,
+        zIndexInOverlay = zIndexInOverlay,
+        clipInOverlayDuringTransition = clipInOverlayDuringTransition
+    )
+
+    /**
+     * Creates an [OverlayClip] based on a specific [clipShape].
+     */
+    fun OverlayClip(clipShape: Shape): OverlayClip = ShapeBasedClip(clipShape)
+
+    /**
+     * Creates and remembers a [SharedContentState] with a given [key].
+     */
+    @Composable
+    fun rememberSharedContentState(key: Any): SharedContentState = remember(key) {
+        SharedContentState(key)
+    }
+
+    /**
+     * [SharedContentState] is designed to allow access of the properties of
+     * [sharedBounds]/[sharedElement], such as whether a match of the same [key] has been found in
+     * the [SharedTransitionScope], its [clipPathInOverlay] and [parentSharedContentState] if there
+     * is a parent [sharedBounds] in the layout tree.
+     */
+    class SharedContentState internal constructor(val key: Any) {
+        /**
+         * Indicates whether a match of the same [key] has been found. [sharedElement]
+         * or [sharedBounds] will not have any animation unless a match has been found.
+         *
+         * _Caveat_: [isMatchFound] is only set to true _after_ a new [sharedElement]/[sharedBounds]
+         * of the same [key] has been composed. If the new [sharedBounds]/[sharedElement] is
+         * declared in subcomposition (e.g. a LazyList) where the composition happens as a part of
+         * the measure/layout pass, that's when [isMatchFound] will become true.
+         */
+        val isMatchFound: Boolean
+            get() = nonNullInternalState.sharedElement.foundMatch
+
+        /**
+         * The resolved clip path in overlay based on the [OverlayClip] defined for the shared
+         * content. [clipPathInOverlay] is set during Draw phase, before children are drawn. This
+         * means it is safe to query [parentSharedContentState]'s [clipPathInOverlay] when
+         * the shared content is drawn.
+         */
+        val clipPathInOverlay: Path?
+            get() = nonNullInternalState.clipPathInOverlay
+
+        /**
+         * Returns the [SharedContentState] of a parent [sharedBounds], if any.
+         */
+        val parentSharedContentState: SharedContentState?
+            get() = nonNullInternalState.parentState?.userState
+        internal var internalState: SharedElementInternalState? by mutableStateOf(null)
+        private val nonNullInternalState: SharedElementInternalState
+            get() = requireNotNull(internalState) {
+                "Error: SharedContentState has not been added to a sharedElement/sharedBounds" +
+                    "modifier yet. Therefore the internal state has not bee initialized."
+            }
+    }
+
+    /**********  Impl details below *****************/
+    private val observeAnimatingBlock: () -> Unit = {
+        sharedElements.any { _, element ->
+            element.isAnimating()
+        }
+    }
+
+    private val updateTransitionActiveness: (SharedTransitionScope) -> Unit = {
+        updateTransitionActiveness()
+    }
+
+    private fun updateTransitionActiveness() {
+        val isActive = sharedElements.any { _, element ->
+            element.isAnimating()
+        }
+        if (isActive != isTransitionActive) {
+            isTransitionActive = isActive
+            if (!isActive) {
+                sharedElements.forEach { _, element ->
+                    element.onSharedTransitionFinished()
+                }
+            }
+        }
+        sharedElements.forEach { _, element ->
+            element.updateMatch()
+        }
+        observer.observeReads(this, updateTransitionActiveness, observeAnimatingBlock)
+    }
+
+    private val observer = SnapshotStateObserver { it() }.also { it.start() }
+
+    /**
+     * sharedBoundsImpl is the implementation for creating animations for shared element
+     * or shared bounds transition. [parentTransition] defines the parent Transition that
+     * the shared element will add its animations to. When [parentTransition] is null,
+     * [visible] will be cast to (Unit) -> Boolean, since we have no parent state to use
+     * for the query.
+     */
+    @OptIn(ExperimentalTransitionApi::class)
+    private fun <T> Modifier.sharedBoundsImpl(
+        sharedContentState: SharedContentState,
+        parentTransition: Transition<T>?,
+        visible: (T) -> Boolean,
+        boundsTransform: BoundsTransform,
+        placeHolderSize: PlaceHolderSize = contentSize,
+        renderOnlyWhenVisible: Boolean,
+        renderInOverlayDuringTransition: Boolean,
+        zIndexInOverlay: Float,
+        clipInOverlayDuringTransition: OverlayClip,
+    ) = composed {
+        val key = sharedContentState.key
+        val sharedElement = remember(key) { sharedElementsFor(key) }
+
+        @Suppress("UNCHECKED_CAST")
+        val boundsTransition = key(key, parentTransition) {
+            if (parentTransition != null) {
+                parentTransition.createChildTransition(key.toString()) { visible(it) }
+            } else {
+                val targetState =
+                    (visible as (Unit) -> Boolean).invoke(Unit)
+                val transitionState = remember {
+                    MutableTransitionState(
+                        initialState = if (sharedElement.currentBounds != null) {
+                            // In the transition that we completely own, we could make the
+                            // assumption that if a new shared element is added, it'll
+                            // always animate from current bounds to target bounds. This ensures
+                            // continuity of shared element bounds.
+                            !targetState
+                        } else {
+                            targetState
+                        }
+                    )
+                }.also { it.targetState = targetState }
+                rememberTransition(transitionState)
+            }
+        }
+        val animation = key(isTransitionActive) {
+            boundsTransition.createDeferredAnimation(Rect.VectorConverter)
+        }
+
+        val boundsAnimation = remember(boundsTransition) {
+            BoundsAnimation(
+                this@SharedTransitionScope, boundsTransition, animation, boundsTransform
+            )
+        }.also {
+            it.updateAnimation(animation, boundsTransform)
+        }
+
+        val sharedElementState = remember(key) {
+            SharedElementInternalState(
+                sharedElement,
+                boundsAnimation,
+                placeHolderSize,
+                renderOnlyWhenVisible = renderOnlyWhenVisible,
+                userState = sharedContentState,
+                overlayClip = clipInOverlayDuringTransition,
+                zIndex = zIndexInOverlay,
+                renderInOverlayDuringTransition = renderInOverlayDuringTransition
+            )
+        }.also {
+            sharedContentState.internalState = it
+            // Update the properties if any of them changes
+            it.sharedElement = sharedElement
+            it.renderOnlyWhenVisible = renderOnlyWhenVisible
+            it.boundsAnimation = boundsAnimation
+            it.placeHolderSize = placeHolderSize
+            it.overlayClip = clipInOverlayDuringTransition
+            it.zIndex = zIndexInOverlay
+            it.renderInOverlayDuringTransition = renderInOverlayDuringTransition
+            it.userState = sharedContentState
+        }
+
+        this then SharedBoundsNodeElement(sharedElementState)
+    }
+
+    internal lateinit var root: LayoutCoordinates
+    internal lateinit var lookaheadRoot: LayoutCoordinates
+
+    // TODO: Use MutableObjectList and impl sort
+    private val renderers = mutableListOf<LayerRenderer>()
+
+    private val sharedElements = MutableScatterMap<Any, SharedElement>()
+    private val defaultRenderInOverlay: () -> Boolean = { isTransitionActive }
+
+    private fun sharedElementsFor(key: Any): SharedElement {
+        return sharedElements[key] ?: SharedElement(key, this).also {
+            sharedElements[key] = it
+        }
+    }
+
+    internal fun drawInOverlay(scope: ContentDrawScope) {
+        // TODO: Sort while preserving the parent child order
+        renderers.sortBy {
+            if (it.zIndex == 0f && it is SharedElementInternalState && it.parentState == null) {
+                -1f
+            } else it.zIndex
+        }
+        renderers.fastForEach {
+            it.drawInOverlay(drawScope = scope)
+        }
+    }
+
+    internal fun onStateRemoved(sharedElementState: SharedElementInternalState) {
+        with(sharedElementState.sharedElement) {
+            removeState(sharedElementState)
+            updateTransitionActiveness.invoke(this@SharedTransitionScope)
+            observer.observeReads(scope, updateTransitionActiveness, observeAnimatingBlock)
+            renderers.remove(sharedElementState)
+            if (states.isEmpty()) {
+                scope.coroutineScope.launch {
+                    if (states.isEmpty()) {
+                        scope.sharedElements.remove(key)
+                    }
+                }
+            }
+        }
+    }
+
+    internal fun onStateAdded(sharedElementState: SharedElementInternalState) {
+        with(sharedElementState.sharedElement) {
+            addState(sharedElementState)
+            updateTransitionActiveness.invoke(this@SharedTransitionScope)
+            observer.observeReads(scope, updateTransitionActiveness, observeAnimatingBlock)
+            val id = renderers.indexOfFirst {
+                (it as? SharedElementInternalState)?.sharedElement ==
+                    sharedElementState.sharedElement
+            }
+            if (id == renderers.size - 1 || id == -1) {
+                renderers.add(sharedElementState)
+            } else {
+                renderers.add(id + 1, sharedElementState)
+            }
+        }
+    }
+
+    internal fun onLayerRendererCreated(renderer: LayerRenderer) {
+        renderers.add(renderer)
+    }
+
+    internal fun onLayerRendererRemoved(renderer: LayerRenderer) {
+        renderers.remove(renderer)
+    }
+
+    private class ShapeBasedClip(
+        val clipShape: Shape
+    ) : OverlayClip {
+        private val path = Path()
+
+        override fun getClipPath(
+            state: SharedContentState,
+            bounds: Rect,
+            layoutDirection: LayoutDirection,
+            density: Density
+        ): Path {
+            path.reset()
+            path.addOutline(
+                clipShape.createOutline(
+                    bounds.size,
+                    layoutDirection,
+                    density
+                )
+            )
+            path.translate(bounds.topLeft)
+            return path
+        }
+    }
+}
+
+private object SkipToLookaheadElement : ModifierNodeElement<SkipToLookaheadNode>() {
+    override fun create(): SkipToLookaheadNode {
+        return SkipToLookaheadNode()
+    }
+
+    override fun hashCode(): Int {
+        return SkipToLookaheadElement::class.hashCode()
+    }
+
+    override fun equals(other: Any?): Boolean {
+        return other is SkipToLookaheadElement
+    }
+
+    override fun update(node: SkipToLookaheadNode) {}
+
+    override fun InspectorInfo.inspectableProperties() {
+        name = "skipToLookahead"
+    }
+}
+
+private class SkipToLookaheadNode : LayoutModifierNode,
+    Modifier.Node() {
+    var lookaheadConstraints: Constraints? = null
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        if (isLookingAhead) {
+            lookaheadConstraints = constraints
+        }
+        val p = measurable.measure(lookaheadConstraints!!)
+        val (w, h) = constraints.constrain(IntSize(p.width, p.height))
+        return layout(w, h) {
+            p.place(0, 0)
+        }
+    }
+}
+
+internal interface LayerRenderer {
+    val parentState: SharedElementInternalState?
+    fun drawInOverlay(drawScope: DrawScope)
+    val zIndex: Float
+}
+
+private val DefaultSpring = spring(
+    stiffness = StiffnessMediumLow,
+    visibilityThreshold = Rect.VisibilityThreshold
+)
+
+@ExperimentalSharedTransitionApi
+private val ParentClip: SharedTransitionScope.OverlayClip =
+    object : SharedTransitionScope.OverlayClip {
+        override fun getClipPath(
+            state: SharedTransitionScope.SharedContentState,
+            bounds: Rect,
+            layoutDirection: LayoutDirection,
+            density: Density
+        ): Path? {
+            return state.parentSharedContentState?.clipPathInOverlay
+        }
+    }
+
+private val DefaultClipInOverlayDuringTransition: (LayoutDirection, Density) -> Path? =
+    { _, _ -> null }
+
+@ExperimentalSharedTransitionApi
+private val DefaultBoundsTransform = BoundsTransform { _, _ -> DefaultSpring }