Merge "Add `AnimatedSpatialVisibility` API and use it in XR `ThreePaneScaffold`" into androidx-main
diff --git a/xr/compose/compose/api/current.txt b/xr/compose/compose/api/current.txt
index 7dde2ab..248feae 100644
--- a/xr/compose/compose/api/current.txt
+++ b/xr/compose/compose/api/current.txt
@@ -369,6 +369,67 @@
 
 }
 
+package androidx.xr.compose.subspace.animation {
+
+  public final class AnimatedSpatialVisibilityKt {
+    method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void AnimatedSpatialVisibility(androidx.compose.animation.core.MutableTransitionState<java.lang.Boolean> visibleState, optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional androidx.xr.compose.subspace.animation.SpatialEnterTransition enter, optional androidx.xr.compose.subspace.animation.SpatialExitTransition exit, optional String label, kotlin.jvm.functions.Function1<androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope,kotlin.Unit> content);
+    method @BytecodeOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void AnimatedSpatialVisibility(androidx.compose.animation.core.MutableTransitionState<java.lang.Boolean!>, androidx.xr.compose.subspace.layout.SubspaceModifier?, androidx.xr.compose.subspace.animation.SpatialEnterTransition?, androidx.xr.compose.subspace.animation.SpatialExitTransition?, String?, kotlin.jvm.functions.Function3<? super androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope!,? super androidx.compose.runtime.Composer!,? super java.lang.Integer!,kotlin.Unit!>, androidx.compose.runtime.Composer?, int, int);
+    method @BytecodeOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static <T> void AnimatedSpatialVisibility(androidx.compose.animation.core.Transition<T!>, kotlin.jvm.functions.Function1<? super T!,java.lang.Boolean!>, androidx.xr.compose.subspace.layout.SubspaceModifier?, androidx.xr.compose.subspace.animation.SpatialEnterTransition?, androidx.xr.compose.subspace.animation.SpatialExitTransition?, kotlin.jvm.functions.Function3<? super androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope!,? super androidx.compose.runtime.Composer!,? super java.lang.Integer!,kotlin.Unit!>, androidx.compose.runtime.Composer?, int, int);
+    method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static <T> void AnimatedSpatialVisibility(androidx.compose.animation.core.Transition<T>, kotlin.jvm.functions.Function1<T,java.lang.Boolean> visible, optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional androidx.xr.compose.subspace.animation.SpatialEnterTransition enter, optional androidx.xr.compose.subspace.animation.SpatialExitTransition exit, kotlin.jvm.functions.Function1<androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope,kotlin.Unit> content);
+    method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void AnimatedSpatialVisibility(boolean visible, optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional androidx.xr.compose.subspace.animation.SpatialEnterTransition enter, optional androidx.xr.compose.subspace.animation.SpatialExitTransition exit, optional String label, kotlin.jvm.functions.Function1<androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope,kotlin.Unit> content);
+    method @BytecodeOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void AnimatedSpatialVisibility(boolean, androidx.xr.compose.subspace.layout.SubspaceModifier?, androidx.xr.compose.subspace.animation.SpatialEnterTransition?, androidx.xr.compose.subspace.animation.SpatialExitTransition?, String?, kotlin.jvm.functions.Function3<? super androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope!,? super androidx.compose.runtime.Composer!,? super java.lang.Integer!,kotlin.Unit!>, androidx.compose.runtime.Composer?, int, int);
+  }
+
+  public sealed interface AnimatedSpatialVisibilityScope extends androidx.compose.animation.AnimatedVisibilityScope {
+    method @KotlinOnly @androidx.compose.runtime.Composable public default androidx.xr.compose.subspace.layout.SubspaceModifier animateEnterExit(androidx.xr.compose.subspace.layout.SubspaceModifier, optional androidx.xr.compose.subspace.animation.SpatialEnterTransition enter, optional androidx.xr.compose.subspace.animation.SpatialExitTransition exit);
+    method @BytecodeOnly @androidx.compose.runtime.Composable public default androidx.xr.compose.subspace.layout.SubspaceModifier animateEnterExit(androidx.xr.compose.subspace.layout.SubspaceModifier, androidx.xr.compose.subspace.animation.SpatialEnterTransition, androidx.xr.compose.subspace.animation.SpatialExitTransition, androidx.compose.runtime.Composer?, int);
+    method @BytecodeOnly @Deprecated @androidx.compose.runtime.Composable public default androidx.xr.compose.subspace.layout.SubspaceModifier! animateEnterExit(androidx.xr.compose.subspace.layout.SubspaceModifier!, androidx.xr.compose.subspace.animation.SpatialEnterTransition!, androidx.xr.compose.subspace.animation.SpatialExitTransition!, androidx.compose.runtime.Composer!, int, int);
+  }
+
+  public abstract sealed class SpatialEnterTransition {
+    method @androidx.compose.runtime.Stable public final operator androidx.xr.compose.subspace.animation.SpatialEnterTransition plus(androidx.xr.compose.subspace.animation.SpatialEnterTransition enter);
+    field public static final androidx.xr.compose.subspace.animation.SpatialEnterTransition.Companion Companion;
+  }
+
+  public static final class SpatialEnterTransition.Companion {
+    method @InaccessibleFromKotlin public androidx.xr.compose.subspace.animation.SpatialEnterTransition getNone();
+    property public androidx.xr.compose.subspace.animation.SpatialEnterTransition None;
+  }
+
+  public abstract sealed class SpatialExitTransition {
+    method @androidx.compose.runtime.Stable public final operator androidx.xr.compose.subspace.animation.SpatialExitTransition plus(androidx.xr.compose.subspace.animation.SpatialExitTransition exit);
+    field public static final androidx.xr.compose.subspace.animation.SpatialExitTransition.Companion Companion;
+  }
+
+  public static final class SpatialExitTransition.Companion {
+    method @InaccessibleFromKotlin public androidx.xr.compose.subspace.animation.SpatialExitTransition getNone();
+    property public androidx.xr.compose.subspace.animation.SpatialExitTransition None;
+  }
+
+  public final class SpatialTransitionDefaults {
+    method @InaccessibleFromKotlin public androidx.xr.compose.subspace.animation.SpatialEnterTransition getDefaultEnter();
+    method @InaccessibleFromKotlin public androidx.xr.compose.subspace.animation.SpatialExitTransition getDefaultExit();
+    property public androidx.xr.compose.subspace.animation.SpatialEnterTransition DefaultEnter;
+    property public androidx.xr.compose.subspace.animation.SpatialExitTransition DefaultExit;
+    field public static final androidx.xr.compose.subspace.animation.SpatialTransitionDefaults INSTANCE;
+  }
+
+  public final class SpatialTransitions {
+    method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialEnterTransition fadeIn(optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, optional float initialAlpha);
+    method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialExitTransition fadeOut(optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, optional float targetAlpha);
+    method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialEnterTransition slideIn(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super androidx.xr.compose.unit.IntVolumeSize,androidx.xr.compose.unit.IntVolumeOffset> initialOffset);
+    method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialEnterTransition slideInDepth(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> initialOffsetZ);
+    method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialEnterTransition slideInHorizontally(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> initialOffsetX);
+    method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialEnterTransition slideInVertically(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> initialOffsetY);
+    method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialExitTransition slideOut(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super androidx.xr.compose.unit.IntVolumeSize,androidx.xr.compose.unit.IntVolumeOffset> targetOffset);
+    method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialExitTransition slideOutDepth(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> targetOffsetZ);
+    method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialExitTransition slideOutHorizontally(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> targetOffsetX);
+    method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialExitTransition slideOutVertically(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> targetOffsetY);
+    field public static final androidx.xr.compose.subspace.animation.SpatialTransitions INSTANCE;
+  }
+
+}
+
 package androidx.xr.compose.subspace.layout {
 
   public final class AlphaKt {
@@ -384,6 +445,10 @@
     property public abstract androidx.xr.compose.subspace.layout.SubspaceModifier.Node node;
   }
 
+  public final class LayoutModifierKt {
+    method public static androidx.xr.compose.subspace.layout.SubspaceModifier layout(androidx.xr.compose.subspace.layout.SubspaceModifier, kotlin.jvm.functions.Function3<? super androidx.xr.compose.subspace.layout.SubspaceMeasureScope,? super androidx.xr.compose.subspace.layout.SubspaceMeasurable,? super androidx.xr.compose.unit.VolumeConstraints,? extends androidx.xr.compose.subspace.layout.SubspaceMeasureResult> measure);
+  }
+
   public final class OffsetKt {
     method @KotlinOnly public static androidx.xr.compose.subspace.layout.SubspaceModifier offset(androidx.xr.compose.subspace.layout.SubspaceModifier, optional androidx.compose.ui.unit.Dp x, optional androidx.compose.ui.unit.Dp y, optional androidx.compose.ui.unit.Dp z);
     method @BytecodeOnly public static androidx.xr.compose.subspace.layout.SubspaceModifier offset-qQh39rQ(androidx.xr.compose.subspace.layout.SubspaceModifier, float, float, float);
@@ -977,6 +1042,22 @@
     property public androidx.xr.compose.unit.DpVolumeSize Zero;
   }
 
+  @androidx.compose.runtime.Immutable public final class IntVolumeOffset {
+    ctor public IntVolumeOffset(int x, int y, int z);
+    method @InaccessibleFromKotlin public int getX();
+    method @InaccessibleFromKotlin public int getY();
+    method @InaccessibleFromKotlin public int getZ();
+    property public int x;
+    property public int y;
+    property public int z;
+    field public static final androidx.xr.compose.unit.IntVolumeOffset.Companion Companion;
+  }
+
+  public static final class IntVolumeOffset.Companion {
+    method @InaccessibleFromKotlin public androidx.xr.compose.unit.IntVolumeOffset getZero();
+    property public androidx.xr.compose.unit.IntVolumeOffset Zero;
+  }
+
   public final class IntVolumeSize {
     ctor public IntVolumeSize(int width, int height, int depth);
     method @InaccessibleFromKotlin public int getDepth();
diff --git a/xr/compose/compose/api/restricted_current.txt b/xr/compose/compose/api/restricted_current.txt
index bf97194..096dd52 100644
--- a/xr/compose/compose/api/restricted_current.txt
+++ b/xr/compose/compose/api/restricted_current.txt
@@ -397,6 +397,67 @@
 
 }
 
+package androidx.xr.compose.subspace.animation {
+
+  public final class AnimatedSpatialVisibilityKt {
+    method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void AnimatedSpatialVisibility(androidx.compose.animation.core.MutableTransitionState<java.lang.Boolean> visibleState, optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional androidx.xr.compose.subspace.animation.SpatialEnterTransition enter, optional androidx.xr.compose.subspace.animation.SpatialExitTransition exit, optional String label, kotlin.jvm.functions.Function1<androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope,kotlin.Unit> content);
+    method @BytecodeOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void AnimatedSpatialVisibility(androidx.compose.animation.core.MutableTransitionState<java.lang.Boolean!>, androidx.xr.compose.subspace.layout.SubspaceModifier?, androidx.xr.compose.subspace.animation.SpatialEnterTransition?, androidx.xr.compose.subspace.animation.SpatialExitTransition?, String?, kotlin.jvm.functions.Function3<? super androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope!,? super androidx.compose.runtime.Composer!,? super java.lang.Integer!,kotlin.Unit!>, androidx.compose.runtime.Composer?, int, int);
+    method @BytecodeOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static <T> void AnimatedSpatialVisibility(androidx.compose.animation.core.Transition<T!>, kotlin.jvm.functions.Function1<? super T!,java.lang.Boolean!>, androidx.xr.compose.subspace.layout.SubspaceModifier?, androidx.xr.compose.subspace.animation.SpatialEnterTransition?, androidx.xr.compose.subspace.animation.SpatialExitTransition?, kotlin.jvm.functions.Function3<? super androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope!,? super androidx.compose.runtime.Composer!,? super java.lang.Integer!,kotlin.Unit!>, androidx.compose.runtime.Composer?, int, int);
+    method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static <T> void AnimatedSpatialVisibility(androidx.compose.animation.core.Transition<T>, kotlin.jvm.functions.Function1<T,java.lang.Boolean> visible, optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional androidx.xr.compose.subspace.animation.SpatialEnterTransition enter, optional androidx.xr.compose.subspace.animation.SpatialExitTransition exit, kotlin.jvm.functions.Function1<androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope,kotlin.Unit> content);
+    method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void AnimatedSpatialVisibility(boolean visible, optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional androidx.xr.compose.subspace.animation.SpatialEnterTransition enter, optional androidx.xr.compose.subspace.animation.SpatialExitTransition exit, optional String label, kotlin.jvm.functions.Function1<androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope,kotlin.Unit> content);
+    method @BytecodeOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void AnimatedSpatialVisibility(boolean, androidx.xr.compose.subspace.layout.SubspaceModifier?, androidx.xr.compose.subspace.animation.SpatialEnterTransition?, androidx.xr.compose.subspace.animation.SpatialExitTransition?, String?, kotlin.jvm.functions.Function3<? super androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope!,? super androidx.compose.runtime.Composer!,? super java.lang.Integer!,kotlin.Unit!>, androidx.compose.runtime.Composer?, int, int);
+  }
+
+  public sealed interface AnimatedSpatialVisibilityScope extends androidx.compose.animation.AnimatedVisibilityScope {
+    method @KotlinOnly @androidx.compose.runtime.Composable public default androidx.xr.compose.subspace.layout.SubspaceModifier animateEnterExit(androidx.xr.compose.subspace.layout.SubspaceModifier, optional androidx.xr.compose.subspace.animation.SpatialEnterTransition enter, optional androidx.xr.compose.subspace.animation.SpatialExitTransition exit);
+    method @BytecodeOnly @androidx.compose.runtime.Composable public default androidx.xr.compose.subspace.layout.SubspaceModifier animateEnterExit(androidx.xr.compose.subspace.layout.SubspaceModifier, androidx.xr.compose.subspace.animation.SpatialEnterTransition, androidx.xr.compose.subspace.animation.SpatialExitTransition, androidx.compose.runtime.Composer?, int);
+    method @BytecodeOnly @Deprecated @androidx.compose.runtime.Composable public default androidx.xr.compose.subspace.layout.SubspaceModifier! animateEnterExit(androidx.xr.compose.subspace.layout.SubspaceModifier!, androidx.xr.compose.subspace.animation.SpatialEnterTransition!, androidx.xr.compose.subspace.animation.SpatialExitTransition!, androidx.compose.runtime.Composer!, int, int);
+  }
+
+  public abstract sealed class SpatialEnterTransition {
+    method @androidx.compose.runtime.Stable public final operator androidx.xr.compose.subspace.animation.SpatialEnterTransition plus(androidx.xr.compose.subspace.animation.SpatialEnterTransition enter);
+    field public static final androidx.xr.compose.subspace.animation.SpatialEnterTransition.Companion Companion;
+  }
+
+  public static final class SpatialEnterTransition.Companion {
+    method @InaccessibleFromKotlin public androidx.xr.compose.subspace.animation.SpatialEnterTransition getNone();
+    property public androidx.xr.compose.subspace.animation.SpatialEnterTransition None;
+  }
+
+  public abstract sealed class SpatialExitTransition {
+    method @androidx.compose.runtime.Stable public final operator androidx.xr.compose.subspace.animation.SpatialExitTransition plus(androidx.xr.compose.subspace.animation.SpatialExitTransition exit);
+    field public static final androidx.xr.compose.subspace.animation.SpatialExitTransition.Companion Companion;
+  }
+
+  public static final class SpatialExitTransition.Companion {
+    method @InaccessibleFromKotlin public androidx.xr.compose.subspace.animation.SpatialExitTransition getNone();
+    property public androidx.xr.compose.subspace.animation.SpatialExitTransition None;
+  }
+
+  public final class SpatialTransitionDefaults {
+    method @InaccessibleFromKotlin public androidx.xr.compose.subspace.animation.SpatialEnterTransition getDefaultEnter();
+    method @InaccessibleFromKotlin public androidx.xr.compose.subspace.animation.SpatialExitTransition getDefaultExit();
+    property public androidx.xr.compose.subspace.animation.SpatialEnterTransition DefaultEnter;
+    property public androidx.xr.compose.subspace.animation.SpatialExitTransition DefaultExit;
+    field public static final androidx.xr.compose.subspace.animation.SpatialTransitionDefaults INSTANCE;
+  }
+
+  public final class SpatialTransitions {
+    method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialEnterTransition fadeIn(optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, optional float initialAlpha);
+    method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialExitTransition fadeOut(optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, optional float targetAlpha);
+    method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialEnterTransition slideIn(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super androidx.xr.compose.unit.IntVolumeSize,androidx.xr.compose.unit.IntVolumeOffset> initialOffset);
+    method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialEnterTransition slideInDepth(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> initialOffsetZ);
+    method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialEnterTransition slideInHorizontally(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> initialOffsetX);
+    method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialEnterTransition slideInVertically(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> initialOffsetY);
+    method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialExitTransition slideOut(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super androidx.xr.compose.unit.IntVolumeSize,androidx.xr.compose.unit.IntVolumeOffset> targetOffset);
+    method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialExitTransition slideOutDepth(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> targetOffsetZ);
+    method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialExitTransition slideOutHorizontally(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> targetOffsetX);
+    method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialExitTransition slideOutVertically(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> targetOffsetY);
+    field public static final androidx.xr.compose.subspace.animation.SpatialTransitions INSTANCE;
+  }
+
+}
+
 package androidx.xr.compose.subspace.layout {
 
   public final class AlphaKt {
@@ -412,6 +473,10 @@
     property public abstract androidx.xr.compose.subspace.layout.SubspaceModifier.Node node;
   }
 
+  public final class LayoutModifierKt {
+    method public static androidx.xr.compose.subspace.layout.SubspaceModifier layout(androidx.xr.compose.subspace.layout.SubspaceModifier, kotlin.jvm.functions.Function3<? super androidx.xr.compose.subspace.layout.SubspaceMeasureScope,? super androidx.xr.compose.subspace.layout.SubspaceMeasurable,? super androidx.xr.compose.unit.VolumeConstraints,? extends androidx.xr.compose.subspace.layout.SubspaceMeasureResult> measure);
+  }
+
   public final class OffsetKt {
     method @KotlinOnly public static androidx.xr.compose.subspace.layout.SubspaceModifier offset(androidx.xr.compose.subspace.layout.SubspaceModifier, optional androidx.compose.ui.unit.Dp x, optional androidx.compose.ui.unit.Dp y, optional androidx.compose.ui.unit.Dp z);
     method @BytecodeOnly public static androidx.xr.compose.subspace.layout.SubspaceModifier offset-qQh39rQ(androidx.xr.compose.subspace.layout.SubspaceModifier, float, float, float);
@@ -1039,6 +1104,22 @@
     property public androidx.xr.compose.unit.DpVolumeSize Zero;
   }
 
+  @androidx.compose.runtime.Immutable public final class IntVolumeOffset {
+    ctor public IntVolumeOffset(int x, int y, int z);
+    method @InaccessibleFromKotlin public int getX();
+    method @InaccessibleFromKotlin public int getY();
+    method @InaccessibleFromKotlin public int getZ();
+    property public int x;
+    property public int y;
+    property public int z;
+    field public static final androidx.xr.compose.unit.IntVolumeOffset.Companion Companion;
+  }
+
+  public static final class IntVolumeOffset.Companion {
+    method @InaccessibleFromKotlin public androidx.xr.compose.unit.IntVolumeOffset getZero();
+    property public androidx.xr.compose.unit.IntVolumeOffset Zero;
+  }
+
   public final class IntVolumeSize {
     ctor public IntVolumeSize(int width, int height, int depth);
     method @InaccessibleFromKotlin public int getDepth();
diff --git a/xr/compose/compose/samples/src/main/java/androidx/xr/compose/samples/animation/SpatialTransitionSamples.kt b/xr/compose/compose/samples/src/main/java/androidx/xr/compose/samples/animation/SpatialTransitionSamples.kt
new file mode 100644
index 0000000..930bdb4
--- /dev/null
+++ b/xr/compose/compose/samples/src/main/java/androidx/xr/compose/samples/animation/SpatialTransitionSamples.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2025 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.xr.compose.samples.animation
+
+import androidx.annotation.Sampled
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.xr.compose.spatial.ApplicationSubspace
+import androidx.xr.compose.subspace.SpatialPanel
+import androidx.xr.compose.subspace.animation.AnimatedSpatialVisibility
+import androidx.xr.compose.subspace.animation.SpatialTransitions
+import androidx.xr.compose.unit.IntVolumeOffset
+
+@Sampled
+@Composable
+fun SpatialFade() {
+    ApplicationSubspace {
+        val visibleState = remember { MutableTransitionState(false) }
+        visibleState.targetState = true
+
+        AnimatedSpatialVisibility(
+            visibleState = visibleState,
+            enter = SpatialTransitions.fadeIn(initialAlpha = 0.5f),
+            exit =
+                SpatialTransitions.fadeOut(
+                    animationSpec = spring(stiffness = Spring.StiffnessVeryLow)
+                ),
+        ) {
+            SpatialPanel { Text("Spatial panel") }
+        }
+    }
+}
+
+@Sampled
+@Composable
+fun SpatialSlide() {
+    ApplicationSubspace {
+        val visibleState = remember { MutableTransitionState(false) }
+        visibleState.targetState = true
+
+        AnimatedSpatialVisibility(
+            visibleState = visibleState,
+            // if sliding on multiple axes, an initial/target point must be given
+            enter =
+                SpatialTransitions.slideIn { size ->
+                    IntVolumeOffset(
+                        // negative x: from left to right
+                        x = -size.width / 2,
+                        // negative y: from bottom to top
+                        y = -size.height / 2,
+                        // negative z: from far to close
+                        z = -size.depth / 2,
+                    )
+                },
+            // defaults are provided when only sliding on one axis
+            exit = SpatialTransitions.slideOutHorizontally(),
+        ) {
+            SpatialPanel { Text("Spatial panel") }
+        }
+    }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/AnimatedSpatialVisibility.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/AnimatedSpatialVisibility.kt
new file mode 100644
index 0000000..789a2a5
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/AnimatedSpatialVisibility.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2025 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.xr.compose.subspace.animation
+
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.rememberTransition
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.runtime.Composable
+import androidx.xr.compose.subspace.SubspaceComposable
+import androidx.xr.compose.subspace.layout.SubspaceLayout
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+
+/**
+ * [AnimatedSpatialVisibility] composable animates the appearance and disappearance of its subspace
+ * content, as [visible] value changes. Different [SpatialEnterTransition]s and
+ * [SpatialExitTransition]s can be defined in [enter] and [exit] for the appearance and
+ * disappearance animation. There are 4 types of [SpatialEnterTransition] and
+ * [SpatialExitTransition]: Fade, Expand/Shrink, Scale and Slide. The enter transitions can be
+ * combined using `+`. Same for exit transitions. The order of the combination does not matter, as
+ * the transition animations will start simultaneously. See [SpatialEnterTransition] and
+ * [SpatialExitTransition] for details on the three types of transition.
+ *
+ * @param visible defines whether the content should be visible
+ * @param modifier modifier for the [SubspaceLayout] created to contain the [content]
+ * @param enter EnterTransition(s) used for the appearing animation
+ * @param exit ExitTransition(s) used for the disappearing animation
+ * @param label A label to differentiate from other animations in Android Studio animation preview.
+ * @param content Content to appear or disappear based on the value of [visible]
+ * @see androidx.compose.animation.AnimatedVisibility
+ * @see SpatialEnterTransition
+ * @see SpatialExitTransition
+ * @see AnimatedSpatialVisibilityScope
+ */
+@Composable
+@SubspaceComposable
+public fun AnimatedSpatialVisibility(
+    visible: Boolean,
+    modifier: SubspaceModifier = SubspaceModifier,
+    enter: SpatialEnterTransition = SpatialTransitionDefaults.DefaultEnter,
+    exit: SpatialExitTransition = SpatialTransitionDefaults.DefaultExit,
+    label: String = "AnimatedSpatialVisibility",
+    content: @Composable @SubspaceComposable AnimatedSpatialVisibilityScope.() -> Unit,
+) {
+    val transition: Transition<Boolean> = updateTransition(targetState = visible, label = label)
+    AnimatedSpatialVisibilityImpl(transition, { it }, modifier, enter, exit, content)
+}
+
+/**
+ * [AnimatedSpatialVisibility] composable animates the appearance and disappearance of its content,
+ * as [visibleState]'s [targetState][MutableTransitionState.targetState] changes. The [visibleState]
+ * can also be used to observe the state of [AnimatedSpatialVisibility]. For example:
+ * `visibleState.isIdle` indicates whether all the animations have finished in
+ * [AnimatedSpatialVisibility], and `visibleState.currentState` returns the initial state of the
+ * current animations.
+ *
+ * @param visibleState defines whether the content should be visible
+ * @param modifier modifier for the [SubspaceLayout] created to contain the [content]
+ * @param enter EnterTransition(s) used for the appearing animation
+ * @param exit ExitTransition(s) used for the disappearing animation
+ * @param label A label to differentiate from other animations in Android Studio animation preview.
+ * @param content Content to appear or disappear based on the value of [visibleState]
+ * @see androidx.compose.animation.AnimatedVisibility
+ * @see SpatialEnterTransition
+ * @see SpatialExitTransition
+ * @see AnimatedSpatialVisibilityScope
+ */
+@Composable
+@SubspaceComposable
+public fun AnimatedSpatialVisibility(
+    visibleState: MutableTransitionState<Boolean>,
+    modifier: SubspaceModifier = SubspaceModifier,
+    enter: SpatialEnterTransition = SpatialTransitionDefaults.DefaultEnter,
+    exit: SpatialExitTransition = SpatialTransitionDefaults.DefaultExit,
+    label: String = "AnimatedSpatialVisibility",
+    content: @Composable @SubspaceComposable AnimatedSpatialVisibilityScope.() -> Unit,
+) {
+    val transition = rememberTransition(visibleState, label)
+    AnimatedSpatialVisibilityImpl(transition, { it }, modifier, enter, exit, content)
+}
+
+/**
+ * This extension function creates an [AnimatedSpatialVisibility] composable as a child Transition
+ * of the given Transition. This means: 1) the enter/exit transition is now triggered by the
+ * provided [Transition]'s [targetState][Transition.targetState] change. When the targetState
+ * changes, the visibility will be derived using the [visible] lambda and
+ * [Transition.targetState]. 2) The enter/exit transitions, as well as any custom enter/exit
+ * animations defined in [AnimatedSpatialVisibility] are now hoisted to the parent Transition. The
+ * parent Transition will wait for all of them to finish before it considers itself finished (i.e.
+ * [Transition.currentState] = [Transition.targetState]), and subsequently removes the content in
+ * the exit case.
+ *
+ * @param visible defines whether the content should be visible based on transition state T
+ * @param modifier modifier for the [SubspaceLayout] created to contain the [content]
+ * @param enter EnterTransition(s) used for the appearing animation
+ * @param exit ExitTransition(s) used for the disappearing animation
+ * @param content Content to appear or disappear based on the visibility derived from the
+ *   [Transition.targetState] and the provided [visible] lambda
+ * @see androidx.compose.animation.AnimatedVisibility
+ * @see SpatialEnterTransition
+ * @see SpatialExitTransition
+ * @see AnimatedSpatialVisibilityScope
+ */
+@Composable
+@SubspaceComposable
+public fun <T> Transition<T>.AnimatedSpatialVisibility(
+    visible: (T) -> Boolean,
+    modifier: SubspaceModifier = SubspaceModifier,
+    enter: SpatialEnterTransition = SpatialTransitionDefaults.DefaultEnter,
+    exit: SpatialExitTransition = SpatialTransitionDefaults.DefaultExit,
+    content: @Composable @SubspaceComposable AnimatedSpatialVisibilityScope.() -> Unit,
+) {
+    AnimatedSpatialVisibilityImpl(this, visible, modifier, enter, exit, content)
+}
+
+/**
+ * This is the scope for the content of [AnimatedSpatialVisibility]. In this scope, direct and
+ * indirect children of [AnimatedSpatialVisibility] will be able to define their own enter/exit
+ * transitions using the built-in options via [SubspaceModifier.animateEnterExit]. They will also be
+ * able define custom enter/exit animations using the [transition] object.
+ * [AnimatedSpatialVisibility] will ensure both custom and built-in enter/exit animations finish
+ * before it considers itself idle, and subsequently removes its content in the case of exit.
+ *
+ * __Note:__ Custom enter/exit animations that are created *independent* of the
+ * [AnimatedSpatialVisibilityScope.transition] will have no guarantee to finish when exiting, as
+ * [AnimatedSpatialVisibility] would have no visibility of such animations.
+ *
+ * @see AnimatedVisibilityScope
+ */
+public sealed interface AnimatedSpatialVisibilityScope : AnimatedVisibilityScope {
+    @Composable
+    public fun SubspaceModifier.animateEnterExit(
+        enter: SpatialEnterTransition = SpatialTransitionDefaults.DefaultEnter,
+        exit: SpatialExitTransition = SpatialTransitionDefaults.DefaultExit,
+    ): SubspaceModifier = transition.createModifier(enter, exit)
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/AnimatedSpatialVisibilityImpl.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/AnimatedSpatialVisibilityImpl.kt
new file mode 100644
index 0000000..b110c07
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/AnimatedSpatialVisibilityImpl.kt
@@ -0,0 +1,320 @@
+/*
+ * Copyright 2025 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.xr.compose.subspace.animation
+
+import androidx.compose.animation.EnterExitState
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector3D
+import androidx.compose.animation.core.ExperimentalTransitionApi
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.TwoWayConverter
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.createChildTransition
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastMap
+import androidx.compose.ui.util.fastRoundToInt
+import androidx.xr.compose.subspace.SubspaceComposable
+import androidx.xr.compose.subspace.animation.SpatialTransitionDefaults.DefaultAlphaAnimationSpec
+import androidx.xr.compose.subspace.animation.SpatialTransitionDefaults.DefaultSlideAnimationSpec
+import androidx.xr.compose.subspace.layout.SubspaceLayout
+import androidx.xr.compose.subspace.layout.SubspaceMeasurable
+import androidx.xr.compose.subspace.layout.SubspaceMeasurePolicy
+import androidx.xr.compose.subspace.layout.SubspaceMeasureResult
+import androidx.xr.compose.subspace.layout.SubspaceMeasureScope
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.subspace.layout.alpha
+import androidx.xr.compose.subspace.layout.layout
+import androidx.xr.compose.unit.IntVolumeOffset
+import androidx.xr.compose.unit.IntVolumeSize
+import androidx.xr.compose.unit.VolumeConstraints
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Vector3
+import kotlin.math.max
+
+@Composable
+@SubspaceComposable
+internal fun <T> AnimatedSpatialVisibilityImpl(
+    transition: Transition<T>,
+    visible: (T) -> Boolean,
+    modifier: SubspaceModifier,
+    enter: SpatialEnterTransition,
+    exit: SpatialExitTransition,
+    content: @Composable @SubspaceComposable AnimatedSpatialVisibilityScope.() -> Unit,
+) {
+    AnimatedSpatialEnterExitImpl(
+        transition = transition,
+        visible = visible,
+        modifier =
+            modifier.layout { measurable, constraints ->
+                val placeable = measurable.measure(constraints)
+                val size =
+                    if (!visible(transition.targetState)) {
+                        IntVolumeSize.Zero
+                    } else {
+                        IntVolumeSize(
+                            placeable.measuredWidth,
+                            placeable.measuredHeight,
+                            placeable.measuredDepth,
+                        )
+                    }
+                layout(size.width, size.height, size.depth) { placeable.place(Pose.Identity) }
+            },
+        enter = enter,
+        exit = exit,
+        shouldDisposeBlock = { current, target ->
+            current == target && target == EnterExitState.PostExit
+        },
+        content = content,
+    )
+}
+
+@OptIn(ExperimentalTransitionApi::class)
+@Composable
+@SubspaceComposable
+private fun <T> AnimatedSpatialEnterExitImpl(
+    transition: Transition<T>,
+    visible: (T) -> Boolean,
+    modifier: SubspaceModifier,
+    enter: SpatialEnterTransition,
+    exit: SpatialExitTransition,
+    shouldDisposeBlock: (EnterExitState, EnterExitState) -> Boolean,
+    content: @Composable @SubspaceComposable AnimatedSpatialVisibilityScope.() -> Unit,
+) {
+    // TODO(kmost): The original implementation of this in AnimatedEnterExitImpl also checks
+    //  `transition.isSeeking` and `transition.hasInitialValueAnimations`, but these are internal
+    //  APIs, so we can't access them.
+    if (visible(transition.targetState) || visible(transition.currentState)) {
+        val childTransition =
+            transition.createChildTransition(label = "SpatialEnterExitTransition") {
+                transition.targetEnterExit(visible, it)
+            }
+
+        val shouldDisposeBlockUpdated by rememberUpdatedState(shouldDisposeBlock)
+
+        val shouldDisposeAfterExit by
+            produceState(
+                initialValue =
+                    shouldDisposeBlock(childTransition.currentState, childTransition.targetState)
+            ) {
+                snapshotFlow { childTransition.exitFinished }
+                    .collect {
+                        value =
+                            if (it) {
+                                shouldDisposeBlockUpdated(
+                                    childTransition.currentState,
+                                    childTransition.targetState,
+                                )
+                            } else {
+                                false
+                            }
+                    }
+            }
+        if (!childTransition.exitFinished || !shouldDisposeAfterExit) {
+            val scope = remember(transition) { AnimatedSpatialVisibilityScopeImpl(childTransition) }
+            SubspaceLayout(
+                modifier = modifier.then(childTransition.createModifier(enter, exit)),
+                content = { scope.content() },
+                measurePolicy = remember { AnimatedSpatialEnterExitMeasurePolicy() },
+            )
+        }
+    }
+}
+
+private class AnimatedSpatialEnterExitMeasurePolicy() : SubspaceMeasurePolicy {
+    override fun SubspaceMeasureScope.measure(
+        measurables: List<SubspaceMeasurable>,
+        constraints: VolumeConstraints,
+    ): SubspaceMeasureResult {
+        var maxWidth = 0
+        var maxHeight = 0
+        var maxDepth = 0
+        val placeables =
+            measurables.fastMap {
+                it.measure(constraints).apply {
+                    maxWidth = max(maxWidth, measuredWidth)
+                    maxHeight = max(maxHeight, measuredHeight)
+                    maxDepth = max(maxDepth, measuredDepth)
+                }
+            }
+        return layout(maxWidth, maxHeight, maxDepth) {
+            placeables.fastForEach { it.place(Pose.Identity) }
+        }
+    }
+}
+
+@Composable
+internal fun Transition<EnterExitState>.createModifier(
+    enter: SpatialEnterTransition,
+    exit: SpatialExitTransition,
+): SubspaceModifier {
+    return fadeAnimationModifier(enter, exit).then(slideAnimationModifier(enter, exit))
+}
+
+@Composable
+private fun Transition<EnterExitState>.fadeAnimationModifier(
+    activeEnter: SpatialEnterTransition,
+    activeExit: SpatialExitTransition,
+): SubspaceModifier {
+    // No fade animations
+    if (activeEnter.data.fade == null && activeExit.data.fade == null) return SubspaceModifier
+
+    val alphaSpec: @Composable Transition.Segment<EnterExitState>.() -> FiniteAnimationSpec<Float> =
+        {
+            when {
+                EnterExitState.PreEnter isTransitioningTo EnterExitState.Visible -> {
+                    activeEnter.data.fade?.animationSpec ?: DefaultAlphaAnimationSpec
+                }
+                EnterExitState.Visible isTransitioningTo EnterExitState.PostExit -> {
+                    activeExit.data.fade?.animationSpec ?: DefaultAlphaAnimationSpec
+                }
+                else -> DefaultAlphaAnimationSpec
+            }
+        }
+    val animationLabel = remember { "$label alpha" }
+    val alpha by
+        animateFloat(alphaSpec, label = animationLabel) { targetState ->
+            when (targetState) {
+                EnterExitState.Visible -> 1f
+                EnterExitState.PreEnter -> activeEnter.data.fade?.alpha ?: 1f
+                EnterExitState.PostExit -> activeExit.data.fade?.alpha ?: 1f
+            }
+        }
+    return SubspaceModifier.alpha(alpha)
+}
+
+@Composable
+private fun Transition<EnterExitState>.slideAnimationModifier(
+    activeEnter: SpatialEnterTransition,
+    activeExit: SpatialExitTransition,
+): SubspaceModifier {
+    // No slide animations
+    if (activeEnter.data.slide == null && activeExit.data.slide == null) return SubspaceModifier
+
+    val offset = remember { Animatable(IntVolumeOffset.Zero, IntVolumeOffsetToVector) }
+
+    val animationSpec =
+        with(segment) {
+            when {
+                EnterExitState.PreEnter isTransitioningTo EnterExitState.Visible ->
+                    activeEnter.data.slide?.animationSpec ?: DefaultSlideAnimationSpec
+                EnterExitState.Visible isTransitioningTo EnterExitState.PostExit ->
+                    activeExit.data.slide?.animationSpec ?: DefaultSlideAnimationSpec
+                else -> DefaultSlideAnimationSpec
+            }
+        }
+
+    val density = LocalDensity.current
+    fun EnterExitState.calculateOffset(fullSize: IntVolumeSize): IntVolumeOffset {
+        return when (this) {
+            EnterExitState.PreEnter ->
+                activeEnter.data.slide?.slideOffset?.invoke(density, fullSize)
+                    ?: IntVolumeOffset.Zero
+            EnterExitState.Visible -> IntVolumeOffset.Zero
+            EnterExitState.PostExit ->
+                activeExit.data.slide?.slideOffset?.invoke(density, fullSize)
+                    ?: IntVolumeOffset.Zero
+        }
+    }
+
+    // This will be updated by the layout modifier below, and is necessary to compute offsets.
+    var fullSize: IntVolumeSize by remember { mutableStateOf(IntVolumeSize.Zero) }
+    var isInitialSnapToFinished by remember { mutableStateOf(false) }
+
+    // When the size, target state, or animationSpec changes, animate towards the new target offset.
+    LaunchedEffect(fullSize, targetState, animationSpec) {
+        // Ideally, the offset would just be initialized via fullSize, but fullSize is not known
+        // until layout time, so we need to defer this snapTo.
+        // We also don't want to snapTo more than once, as it will result in the animation
+        // restarting in odd ways if the developer changes the layout's size.
+        if (!isInitialSnapToFinished) {
+            isInitialSnapToFinished = true
+            offset.snapTo(currentState.calculateOffset(fullSize))
+        }
+        offset.animateTo(targetState.calculateOffset(fullSize), animationSpec)
+    }
+
+    return SubspaceModifier.layout { measurable, constraints ->
+        val placeable = measurable.measure(constraints)
+        // Size can only be determined in a custom layout modifier
+        fullSize =
+            IntVolumeSize(
+                placeable.measuredWidth,
+                placeable.measuredHeight,
+                placeable.measuredDepth,
+            )
+        layout(placeable.measuredWidth, placeable.measuredHeight, placeable.measuredDepth) {
+            placeable.place(
+                Pose(
+                    // Place this layout at the offset calculated by our animation
+                    Vector3(
+                        offset.value.x.toFloat(),
+                        offset.value.y.toFloat(),
+                        offset.value.z.toFloat(),
+                    )
+                )
+            )
+        }
+    }
+}
+
+// This converts Boolean visible to EnterExitState
+@Composable
+private fun <T> Transition<T>.targetEnterExit(
+    visible: (T) -> Boolean,
+    targetState: T,
+): EnterExitState =
+    key(this) {
+        val hasBeenVisible = remember { mutableStateOf(false) }
+        if (visible(currentState)) {
+            hasBeenVisible.value = true
+        }
+        if (visible(targetState)) {
+            EnterExitState.Visible
+        } else {
+            // If never been visible, visible = false means PreEnter, otherwise PostExit
+            if (hasBeenVisible.value) {
+                EnterExitState.PostExit
+            } else {
+                EnterExitState.PreEnter
+            }
+        }
+    }
+
+private val Transition<EnterExitState>.exitFinished
+    get() = currentState == EnterExitState.PostExit && targetState == EnterExitState.PostExit
+
+private val IntVolumeOffsetToVector: TwoWayConverter<IntVolumeOffset, AnimationVector3D> =
+    TwoWayConverter(
+        { AnimationVector3D(it.x.toFloat(), it.y.toFloat(), it.z.toFloat()) },
+        { IntVolumeOffset(it.v1.fastRoundToInt(), it.v2.fastRoundToInt(), it.v3.fastRoundToInt()) },
+    )
+
+private class AnimatedSpatialVisibilityScopeImpl(
+    override val transition: Transition<EnterExitState>
+) : AnimatedSpatialVisibilityScope
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/SpatialTransition.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/SpatialTransition.kt
new file mode 100644
index 0000000..c080fcf
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/SpatialTransition.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2025 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.xr.compose.subspace.animation
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.unit.Density
+import androidx.xr.compose.unit.IntVolumeOffset
+import androidx.xr.compose.unit.IntVolumeSize
+
+/**
+ * [SpatialEnterTransition] defines how an [AnimatedSpatialVisibility] Composable appears on screen
+ * as it becomes visible. The categories of EnterTransitions available are:
+ * 1. fade: [SpatialTransitions.fadeIn]
+ * 2. slide: [SpatialTransitions.slideIn], [SpatialTransitions.slideInHorizontally],
+ *    [SpatialTransitions.slideInVertically], [SpatialTransitions.slideInDepth]
+ *
+ * [SpatialEnterTransition.None] can be used when no enter transition is desired. Different
+ * [SpatialEnterTransition]s can be combined using [SpatialEnterTransition.plus].
+ *
+ * __Note__: [SpatialTransitions.fadeIn] and [SpatialTransitions.slideIn] do not affect the size of
+ * the [AnimatedVisibility] composable.
+ *
+ * @see EnterTransition
+ */
+public sealed class SpatialEnterTransition {
+    internal abstract val data: SpatialTransitionData
+
+    /**
+     * Combines different enter transitions. The order of the [SpatialEnterTransition]s being
+     * combined does not matter, as these [SpatialEnterTransition]s will start simultaneously. The
+     * order of applying transforms from these enter transitions (if defined) is: alpha and scale
+     * first, shrink or expand, then slide.
+     *
+     * @param enter another [SpatialEnterTransition] to be combined
+     */
+    @Stable
+    public operator fun plus(enter: SpatialEnterTransition): SpatialEnterTransition {
+        return Impl(data.merge(enter.data))
+    }
+
+    @Immutable
+    internal class Impl(override val data: SpatialTransitionData) : SpatialEnterTransition()
+
+    public companion object {
+        /**
+         * This can be used when no enter transition is desired.
+         *
+         * @see [SpatialExitTransition.None]
+         */
+        public val None: SpatialEnterTransition = Impl(SpatialTransitionData())
+    }
+}
+
+/**
+ * [SpatialExitTransition] defines how an [AnimatedSpatialVisibility] Composable disappears on
+ * screen as it becomes not visible. The categories of [SpatialExitTransition] available are:
+ * 1. fade: [SpatialTransitions.fadeOut]
+ * 2. slide: [SpatialTransitions.slideOut], [SpatialTransitions.slideOutHorizontally],
+ *    [SpatialTransitions.slideOutVertically], [SpatialTransitions.slideOutDepth]
+ *
+ * [SpatialExitTransition.None] can be used when no exit transition is desired. Different
+ * [SpatialExitTransition]s can be combined using [SpatialExitTransition.plus].
+ *
+ * __Note__: [SpatialTransitions.fadeOut] and [SpatialTransitions.slideOut] do not affect the size
+ * of the [AnimatedSpatialVisibility] composable.
+ *
+ * @see ExitTransition
+ */
+public sealed class SpatialExitTransition {
+    internal abstract val data: SpatialTransitionData
+
+    /**
+     * Combines different exit transitions. The order of the [SpatialExitTransition]s being combined
+     * does not matter, as these [SpatialExitTransition]s will start simultaneously. The order of
+     * applying transforms from these exit transitions (if defined) is: alpha and scale first,
+     * shrink or expand, then slide.
+     *
+     * @param exit another [SpatialExitTransition] to be combined
+     */
+    @Stable
+    public operator fun plus(exit: SpatialExitTransition): SpatialExitTransition {
+        return Impl(data.merge(exit.data))
+    }
+
+    @Immutable
+    internal class Impl(override val data: SpatialTransitionData) : SpatialExitTransition()
+
+    public companion object {
+        /**
+         * This can be used when no enter transition is desired.
+         *
+         * @see [SpatialEnterTransition.None]
+         */
+        public val None: SpatialExitTransition = Impl(SpatialTransitionData())
+    }
+}
+
+/** ********************* Below are internal classes and methods ***************** */
+@Immutable
+internal data class Fade(val alpha: Float, val animationSpec: FiniteAnimationSpec<Float>)
+
+@Immutable
+internal data class Slide(
+    val slideOffset: Density.(fullSize: IntVolumeSize) -> IntVolumeOffset,
+    val animationSpec: FiniteAnimationSpec<IntVolumeOffset>,
+)
+
+@Immutable
+internal data class SpatialTransitionData(val fade: Fade? = null, val slide: Slide? = null)
+
+private fun SpatialTransitionData.merge(other: SpatialTransitionData): SpatialTransitionData {
+    return SpatialTransitionData(fade = other.fade ?: fade, slide = other.slide ?: slide)
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/SpatialTransitions.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/SpatialTransitions.kt
new file mode 100644
index 0000000..87a213a
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/SpatialTransitions.kt
@@ -0,0 +1,328 @@
+/*
+ * Copyright 2025 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.xr.compose.subspace.animation
+
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.dp
+import androidx.xr.compose.unit.IntVolumeOffset
+import androidx.xr.compose.unit.IntVolumeSize
+
+/** Public transition spec APIs for use with [AnimatedSpatialVisibility]. */
+public object SpatialTransitions {
+    /**
+     * This fades in the content of the transition, from the specified starting alpha (i.e.
+     * [initialAlpha]) to 1f, using the supplied [animationSpec]. [initialAlpha] defaults to 0f, and
+     * [spring] is used by default.
+     *
+     * @sample androidx.xr.compose.samples.animation.SpatialFade
+     * @param animationSpec the [FiniteAnimationSpec] for this animation, [spring] by default
+     * @param initialAlpha the starting alpha of the enter transition, 0f by default
+     */
+    @Stable
+    public fun fadeIn(
+        animationSpec: FiniteAnimationSpec<Float> =
+            SpatialTransitionDefaults.DefaultAlphaAnimationSpec,
+        initialAlpha: Float = 0f,
+    ): SpatialEnterTransition {
+        return SpatialEnterTransition.Impl(
+            SpatialTransitionData(fade = Fade(initialAlpha, animationSpec))
+        )
+    }
+
+    /**
+     * This fades out the content of the transition, from full opacity to the specified target alpha
+     * (i.e. [targetAlpha]), using the supplied [animationSpec]. By default, the content will be
+     * faded out to fully transparent (i.e. [targetAlpha] defaults to 0), and [animationSpec] uses
+     * [spring] by default.
+     *
+     * @sample androidx.xr.compose.samples.animation.SpatialFade
+     * @param animationSpec the [FiniteAnimationSpec] for this animation, [spring] by default
+     * @param targetAlpha the target alpha of the exit transition, 0f by default
+     */
+    @Stable
+    public fun fadeOut(
+        animationSpec: FiniteAnimationSpec<Float> =
+            SpatialTransitionDefaults.DefaultAlphaAnimationSpec,
+        targetAlpha: Float = 0f,
+    ): SpatialExitTransition {
+        return SpatialExitTransition.Impl(
+            SpatialTransitionData(fade = Fade(targetAlpha, animationSpec))
+        )
+    }
+
+    /**
+     * This slides in the content of the transition, from a starting offset defined in
+     * [initialOffset] to `IntVolumeOffset(0, 0, 0)`. The direction of the slide can be controlled
+     * by configuring the [initialOffset]. A positive x value means sliding from right to left,
+     * whereas a negative x value will slide the content to the right. Similarly positive and
+     * negative y values correspond to sliding up and down, respectively, and positive and negative
+     * z values correspond to sliding closer and further, respectively.
+     *
+     * If the sliding is only desired along one axis, consider using [slideInHorizontally],
+     * [slideInVertically], or [slideInDepth].
+     *
+     * [initialOffset] is a lambda that takes the full size of the content and returns an offset.
+     * This allows the offset to be defined proportional to the full size, or as an absolute value.
+     *
+     * @sample androidx.xr.compose.samples.animation.SpatialSlide
+     * @param animationSpec the animation used for the slide-in, [spring] by default.
+     * @param initialOffset a lambda that takes the full size of the content and returns the initial
+     *   offset for the slide-in
+     */
+    @Stable
+    public fun slideIn(
+        animationSpec: FiniteAnimationSpec<IntVolumeOffset> =
+            SpatialTransitionDefaults.DefaultSlideAnimationSpec,
+        initialOffset: Density.(fullSize: IntVolumeSize) -> IntVolumeOffset,
+    ): SpatialEnterTransition {
+        return SpatialEnterTransition.Impl(
+            SpatialTransitionData(slide = Slide(initialOffset, animationSpec))
+        )
+    }
+
+    /**
+     * This slides in the content horizontally, from a starting offset defined in [initialOffsetX]
+     * to `0` **pixels**. The direction of the slide can be controlled by configuring the
+     * [initialOffsetX]. A positive value means sliding from right to left, whereas a negative value
+     * would slide the content from left to right.
+     *
+     * [initialOffsetX] is a lambda that takes the full width of the content and returns an offset.
+     * This allows the starting offset to be defined proportional to the full size, or as an
+     * absolute value. It defaults to return half of negative width, which would offset the content
+     * to the left by half of its width, and slide towards the right.
+     *
+     * @sample androidx.xr.compose.samples.animation.SpatialSlide
+     * @param animationSpec the animation used for the slide-in, [spring] by default.
+     * @param initialOffsetX a lambda that takes the full width of the content in pixels and returns
+     *   the initial offset for the slide-in, by default it returns `-fullWidth/2`
+     */
+    @Stable
+    public fun slideInHorizontally(
+        animationSpec: FiniteAnimationSpec<IntVolumeOffset> =
+            SpatialTransitionDefaults.DefaultSlideAnimationSpec,
+        initialOffsetX: Density.(fullWidth: Int) -> Int = { -it / 2 },
+    ): SpatialEnterTransition {
+        return slideIn(
+            initialOffset = { IntVolumeOffset(x = initialOffsetX(it.width), y = 0, z = 0) },
+            animationSpec = animationSpec,
+        )
+    }
+
+    /**
+     * This slides in the content vertically, from a starting offset defined in [initialOffsetY] to
+     * `0` **pixels**. The direction of the slide can be controlled by configuring the
+     * [initialOffsetY]. A positive value means sliding from top to bottom, whereas a negative value
+     * would slide the content from bottom to top.
+     *
+     * [initialOffsetY] is a lambda that takes the full height of the content and returns an offset.
+     * This allows the starting offset to be defined proportional to the full size, or as an
+     * absolute value. It defaults to return half of negative height, which would offset the content
+     * down by half of its height, and slide upwards.
+     *
+     * @sample androidx.xr.compose.samples.animation.SpatialSlide
+     * @param animationSpec the animation used for the slide-in, [spring] by default.
+     * @param initialOffsetY a lambda that takes the full height of the content in pixels and
+     *   returns the initial offset for the slide-in, by default it returns `-fullHeight/2`
+     */
+    @Stable
+    public fun slideInVertically(
+        animationSpec: FiniteAnimationSpec<IntVolumeOffset> =
+            SpatialTransitionDefaults.DefaultSlideAnimationSpec,
+        initialOffsetY: Density.(fullHeight: Int) -> Int = { -it / 2 },
+    ): SpatialEnterTransition {
+        return slideIn(
+            initialOffset = { IntVolumeOffset(x = 0, y = initialOffsetY(it.height), z = 0) },
+            animationSpec = animationSpec,
+        )
+    }
+
+    /**
+     * This slides in the content depthwise, from a starting offset defined in [initialOffsetZ] to
+     * `0` **pixels**. The direction of the slide can be controlled by configuring the
+     * [initialOffsetZ]. A positive value means sliding from close to far, whereas a negative value
+     * would slide the content from far to close.
+     *
+     * [initialOffsetZ] is a lambda that takes the full depth of the content and returns an offset.
+     * This allows the starting offset to be defined proportional to the full size, or as an
+     * absolute value.
+     *
+     * Unlike [slideInVertically] and [slideInHorizontally], this defaults to sliding in from `20dp`
+     * **away** from the neutral depth point. This is because many commonly-animated Spatial
+     * elements, such as `SpatialPanel`, report a depth of 0.
+     *
+     * @sample androidx.xr.compose.samples.animation.SpatialSlide
+     * @param animationSpec the animation used for the slide-in, [spring] by default.
+     * @param initialOffsetZ a lambda that takes the full height of the content in pixels and
+     *   returns the initial offset for the slide-in, by default it returns `-20.dp.toPx()`
+     */
+    @Stable
+    public fun slideInDepth(
+        animationSpec: FiniteAnimationSpec<IntVolumeOffset> =
+            SpatialTransitionDefaults.DefaultSlideAnimationSpec,
+        initialOffsetZ: Density.(fullDepth: Int) -> Int = { -20.dp.roundToPx() },
+    ): SpatialEnterTransition {
+        return slideIn(
+            initialOffset = { IntVolumeOffset(x = 0, y = 0, z = initialOffsetZ(it.depth)) },
+            animationSpec = animationSpec,
+        )
+    }
+
+    /**
+     * This slides out the content of the transition, from an offset of `IntVolumeOffset(0, 0, 0)`
+     * to the target offset defined in [targetOffset]. The direction of the slide can be controlled
+     * by configuring the [targetOffset]. A positive x value means sliding from left to right,
+     * whereas a negative x value would slide the content from right to left. Similarly, positive
+     * and negative y values correspond to sliding down and up, respectively, and positive and
+     * negative z values correspond to sliding closer and further, respectively.
+     *
+     * If the sliding is only desired along one axis, consider using [slideOutHorizontally],
+     * [slideOutVertically], or [slideOutDepth].
+     *
+     * [targetOffset] is a lambda that takes the full size of the content and returns an offset.
+     * This allows the offset to be defined proportional to the full size, or as an absolute value.
+     *
+     * @sample androidx.xr.compose.samples.animation.SpatialSlide
+     * @param animationSpec the animation used for the slide-out, [spring] by default.
+     * @param targetOffset a lambda that takes the full size of the content and returns the target
+     *   offset for the slide-out
+     */
+    @Stable
+    public fun slideOut(
+        animationSpec: FiniteAnimationSpec<IntVolumeOffset> =
+            SpatialTransitionDefaults.DefaultSlideAnimationSpec,
+        targetOffset: Density.(fullSize: IntVolumeSize) -> IntVolumeOffset,
+    ): SpatialExitTransition {
+        return SpatialExitTransition.Impl(
+            SpatialTransitionData(slide = Slide(targetOffset, animationSpec))
+        )
+    }
+
+    /**
+     * This slides out the content horizontally, from 0 to a target offset defined in
+     * [targetOffsetX] in **pixels**. The direction of the slide can be controlled by configuring
+     * the [targetOffsetX]. A positive value means sliding to the right, whereas a negative value
+     * would slide the content towards the left.
+     *
+     * [targetOffsetX] is a lambda that takes the full width of the content and returns an offset.
+     * This allows the target offset to be defined proportional to the full size, or as an absolute
+     * value. It defaults to return half of negative width, which would slide the content to the
+     * left by half of its width.
+     *
+     * @sample androidx.xr.compose.samples.animation.SpatialSlide
+     * @param animationSpec the animation used for the slide-out, [spring] by default.
+     * @param targetOffsetX a lambda that takes the full width of the content and returns the
+     *   initial offset for the slide-in, by default it returns `fullWidth/2`
+     */
+    @Stable
+    public fun slideOutHorizontally(
+        animationSpec: FiniteAnimationSpec<IntVolumeOffset> =
+            SpatialTransitionDefaults.DefaultSlideAnimationSpec,
+        targetOffsetX: Density.(fullWidth: Int) -> Int = { -it / 2 },
+    ): SpatialExitTransition {
+        return slideOut(
+            targetOffset = { IntVolumeOffset(x = targetOffsetX(it.width), y = 0, z = 0) },
+            animationSpec = animationSpec,
+        )
+    }
+
+    /**
+     * This slides out the content vertically, from 0 to a target offset defined in [targetOffsetY]
+     * in **pixels**. The direction of the slide-out can be controlled by configuring the
+     * [targetOffsetY]. A positive target offset means sliding down, whereas a negative value would
+     * slide the content up.
+     *
+     * [targetOffsetY] is a lambda that takes the full Height of the content and returns an offset.
+     * This allows the target offset to be defined proportional to the full height, or as an
+     * absolute value. It defaults to return half of the negative height, which would slide the
+     * content up by half of its Height.
+     *
+     * @sample androidx.xr.compose.samples.animation.SpatialSlide
+     * @param animationSpec the animation used for the slide-out, [spring] by default.
+     * @param targetOffsetY a lambda that takes the full Height of the content and returns the
+     *   target offset for the slide-out, by default it returns `fullHeight/2`
+     */
+    @Stable
+    public fun slideOutVertically(
+        animationSpec: FiniteAnimationSpec<IntVolumeOffset> =
+            SpatialTransitionDefaults.DefaultSlideAnimationSpec,
+        targetOffsetY: Density.(fullHeight: Int) -> Int = { -it / 2 },
+    ): SpatialExitTransition {
+        return slideOut(
+            targetOffset = { IntVolumeOffset(x = 0, y = targetOffsetY(it.height), z = 0) },
+            animationSpec = animationSpec,
+        )
+    }
+
+    /**
+     * This slides out the content depthwise, from 0 to a target offset defined in [targetOffsetZ]
+     * in **pixels**. The direction of the slide-out can be controlled by configuring the
+     * [targetOffsetZ]. A positive value means sliding from close to far, whereas a negative value
+     * would slide the content from far to close.
+     *
+     * [targetOffsetZ] is a lambda that takes the full depth of the content and returns an offset.
+     * This allows the target offset to be defined proportional to the full depth, or as an absolute
+     * value.
+     *
+     * Unlike [slideInVertically] and [slideInHorizontally], this defaults to sliding in from `20dp`
+     * **away** from the neutral depth point. This is because many commonly-animated Spatial
+     * elements, such as `SpatialPanel`, report a depth of 0.
+     *
+     * @sample androidx.xr.compose.samples.animation.SpatialSlide
+     * @param animationSpec the animation used for the slide-out, [spring] by default.
+     * @param targetOffsetZ a lambda that takes the full depth of the content and returns the target
+     *   offset for the slide-out, by default it returns `-20.dp.toPx()`
+     */
+    @Stable
+    public fun slideOutDepth(
+        animationSpec: FiniteAnimationSpec<IntVolumeOffset> =
+            SpatialTransitionDefaults.DefaultSlideAnimationSpec,
+        targetOffsetZ: Density.(fullDepth: Int) -> Int = { -20.dp.roundToPx() },
+    ): SpatialExitTransition {
+        return slideOut(
+            targetOffset = { IntVolumeOffset(x = 0, y = 0, z = targetOffsetZ(it.depth)) },
+            animationSpec = animationSpec,
+        )
+    }
+}
+
+/** Defaults for use with [SpatialTransitions]. */
+public object SpatialTransitionDefaults {
+    /** The default [SpatialEnterTransition] used by [SpatialTransitions]. */
+    public val DefaultEnter: SpatialEnterTransition =
+        SpatialTransitions.fadeIn() + SpatialTransitions.slideInDepth()
+
+    /** The default [SpatialExitTransition] used by [SpatialTransitions]. */
+    public val DefaultExit: SpatialExitTransition =
+        SpatialTransitions.fadeOut() + SpatialTransitions.slideOutDepth()
+
+    internal val DefaultAlphaAnimationSpec: FiniteAnimationSpec<Float> =
+        spring(stiffness = Spring.StiffnessLow, dampingRatio = DEFAULT_SPATIAL_SPRING_DAMPING)
+
+    internal val DefaultSlideAnimationSpec: FiniteAnimationSpec<IntVolumeOffset> =
+        spring(
+            stiffness = Spring.StiffnessLow,
+            dampingRatio = DEFAULT_SPATIAL_SPRING_DAMPING,
+            visibilityThreshold = IntVolumeOffset(1, 1, 1),
+        )
+
+    /** Spatial Animations use a different damping value than 2D animations. */
+    private const val DEFAULT_SPATIAL_SPRING_DAMPING = 0.8f
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/LayoutModifier.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/LayoutModifier.kt
new file mode 100644
index 0000000..4b315de
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/LayoutModifier.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2025 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.xr.compose.subspace.layout
+
+import androidx.xr.compose.subspace.node.SubspaceLayoutModifierNode
+import androidx.xr.compose.subspace.node.SubspaceModifierNodeElement
+import androidx.xr.compose.unit.VolumeConstraints
+
+/** Creates a node that allows changing how the wrapped element is measured and laid out. */
+public fun SubspaceModifier.layout(
+    measure: SubspaceMeasureScope.(SubspaceMeasurable, VolumeConstraints) -> SubspaceMeasureResult
+): SubspaceModifier = this then LayoutElement(measure)
+
+private data class LayoutElement(
+    val measure:
+        SubspaceMeasureScope.(SubspaceMeasurable, VolumeConstraints) -> SubspaceMeasureResult
+) : SubspaceModifierNodeElement<SubspaceLayoutModifierImpl>() {
+    override fun create() = SubspaceLayoutModifierImpl(measure)
+
+    override fun update(node: SubspaceLayoutModifierImpl) {
+        node.measureBlock = measure
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as LayoutElement
+
+        return measure === other.measure
+    }
+
+    override fun hashCode(): Int {
+        return measure.hashCode()
+    }
+}
+
+private class SubspaceLayoutModifierImpl(
+    var measureBlock:
+        SubspaceMeasureScope.(SubspaceMeasurable, VolumeConstraints) -> SubspaceMeasureResult
+) : SubspaceLayoutModifierNode, SubspaceModifier.Node() {
+    override fun SubspaceMeasureScope.measure(
+        measurable: SubspaceMeasurable,
+        constraints: VolumeConstraints,
+    ): SubspaceMeasureResult = measureBlock(measurable, constraints)
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/unit/IntVolumeOffset.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/unit/IntVolumeOffset.kt
new file mode 100644
index 0000000..2fc6de2
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/unit/IntVolumeOffset.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2025 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.xr.compose.unit
+
+import androidx.compose.runtime.Immutable
+
+/** Represents the offset of an object in 3D space. */
+@Immutable
+public class IntVolumeOffset(public val x: Int, public val y: Int, public val z: Int) {
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as IntVolumeOffset
+
+        if (x != other.x) return false
+        if (y != other.y) return false
+        if (z != other.z) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = x
+        result = 31 * result + y
+        result = 31 * result + z
+        return result
+    }
+
+    override fun toString(): String {
+        return "IntVolumeOffset(x=$x, y=$y, z=$z)"
+    }
+
+    /** Contains a common constant */
+    public companion object {
+        /** A [IntVolumeOffset] with all offsets set to 0. */
+        public val Zero: IntVolumeOffset = IntVolumeOffset(0, 0, 0)
+    }
+}
diff --git a/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/DetailPane.kt b/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/DetailPane.kt
index 22a477f..af092dd 100644
--- a/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/DetailPane.kt
+++ b/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/DetailPane.kt
@@ -37,7 +37,14 @@
 @OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3Api::class)
 @Composable
 internal fun DetailPane(navigator: ThreePaneScaffoldNavigator<Destination>) {
-    val destination = navigator.currentDestination?.contentKey ?: return
+    val destination = navigator.currentDestination?.contentKey
+    if (destination == null) {
+        // Populate the pane with blank content so it can show as a "placeholder", as suggested in
+        // the guidelines:
+        // https://developer.android.com/develop/ui/compose/layouts/adaptive/canonical-layouts#list-detail
+        Surface(Modifier.fillMaxSize()) {}
+        return
+    }
     val coroutineScope = rememberCoroutineScope()
     Scaffold(
         topBar = { MediumTopAppBar(title = { Text("XR Compose Adaptive: ${destination.label}") }) }
diff --git a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/Pane.kt b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/Pane.kt
index 8f902be..9d9d7d9 100644
--- a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/Pane.kt
+++ b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/Pane.kt
@@ -16,34 +16,22 @@
 
 package androidx.xr.compose.material3
 
-import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.MutableTransitionState
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi
 import androidx.compose.material3.adaptive.layout.AnimatedPaneOverride
 import androidx.compose.material3.adaptive.layout.AnimatedPaneOverrideScope
 import androidx.compose.material3.adaptive.layout.AnimatedPaneScope
-import androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldPaneScope
+import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
 import androidx.compose.material3.adaptive.layout.PaneScaffoldRole
 import androidx.compose.material3.adaptive.layout.PaneScaffoldValue
+import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-
-@OptIn(
-    ExperimentalMaterial3AdaptiveComponentOverrideApi::class,
-    ExperimentalMaterial3AdaptiveApi::class,
-)
-@Composable
-private fun <S : PaneScaffoldRole, T : PaneScaffoldValue<S>> Pane(
-    scope: ExtendedPaneScaffoldPaneScope<S, T>,
-    modifier: Modifier,
-    content: @Composable (AnimatedPaneScope.() -> Unit),
-) {
-    with(scope) {
-        scaffoldStateTransition.AnimatedVisibility(visible = { true }, modifier = modifier) {
-            AnimatedPaneScope.create(this).content()
-        }
-    }
-}
+import androidx.xr.compose.subspace.SpatialPanel
+import androidx.xr.compose.subspace.animation.AnimatedSpatialVisibility
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.subspace.layout.fillMaxHeight
+import androidx.xr.compose.subspace.layout.width
 
 @OptIn(
     ExperimentalMaterial3AdaptiveComponentOverrideApi::class,
@@ -52,8 +40,29 @@
 @ExperimentalMaterial3XrApi
 internal object XrAnimatedPaneOverride : AnimatedPaneOverride {
     @Composable
-    override fun <S : PaneScaffoldRole, T : PaneScaffoldValue<S>> AnimatedPaneOverrideScope<S, T>
-        .AnimatedPane() {
-        Pane(scope, modifier, content)
+    override fun <
+        Role : PaneScaffoldRole,
+        ScaffoldValue : PaneScaffoldValue<Role>,
+    > AnimatedPaneOverrideScope<Role, ScaffoldValue>.AnimatedPane() {
+        // TODO(kmost): No way to convert between Enter/ExitTransition and
+        //  SpatialEnter/ExitTransition, so for now we cannot respect those scope properties.
+
+        val state = MutableTransitionState(false)
+        state.targetState =
+            scope.scaffoldStateTransition.targetState[scope.paneRole] != PaneAdaptedValue.Hidden
+
+        AnimatedSpatialVisibility(visibleState = state) {
+            val width =
+                when (scope.paneRole as ThreePaneScaffoldRole) {
+                    ThreePaneScaffoldRole.Primary -> XrThreePaneScaffoldTokens.PrimaryPanePanelWidth
+                    ThreePaneScaffoldRole.Secondary ->
+                        XrThreePaneScaffoldTokens.SecondaryPanePanelWidth
+                    ThreePaneScaffoldRole.Tertiary ->
+                        XrThreePaneScaffoldTokens.TertiaryPanePanelWidth
+                }
+            SpatialPanel(SubspaceModifier.width(width).fillMaxHeight()) {
+                AnimatedPaneScope.create(this).content()
+            }
+        }
     }
 }
diff --git a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/ThreePaneScaffold.kt b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/ThreePaneScaffold.kt
index cdb2632..eaf19e5 100644
--- a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/ThreePaneScaffold.kt
+++ b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/ThreePaneScaffold.kt
@@ -19,28 +19,21 @@
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi
 import androidx.compose.material3.adaptive.layout.PaneScaffoldDirective
-import androidx.compose.material3.adaptive.layout.PaneScaffoldParentData
 import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldHorizontalOrder
 import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldOverride
 import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldOverrideScope
 import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import androidx.xr.compose.material3.XrThreePaneScaffoldOverride.ThreePaneScaffold
 import androidx.xr.compose.spatial.Subspace
 import androidx.xr.compose.subspace.SpatialLayoutSpacer
-import androidx.xr.compose.subspace.SpatialPanel
 import androidx.xr.compose.subspace.SpatialRow
+import androidx.xr.compose.subspace.SubspaceComposable
 import androidx.xr.compose.subspace.layout.SubspaceModifier
-import androidx.xr.compose.subspace.layout.fillMaxHeight
 import androidx.xr.compose.subspace.layout.height
 import androidx.xr.compose.subspace.layout.offset
 import androidx.xr.compose.subspace.layout.width
-import kotlin.math.roundToInt
 
 /**
  * A pane scaffold composable that can display up to three panes in the order that
@@ -73,40 +66,28 @@
 ) {
     Subspace {
         SpatialRow(
-            // Offset by 1dp as a workaround to fix b/395685251, where elements in the XR-overrides
-            // ThreePaneScaffold are not clickable when composed from within the XR-overrides
-            // NavigationSuiteScaffold.
-            modifier = modifier.offset(z = 1.dp).height(XrThreePaneScaffoldTokens.PanelHeight)
+            modifier =
+                modifier
+                    // Offset by 1dp as a workaround to fix b/395685251, where elements in the
+                    // XR-overrides ThreePaneScaffold are not clickable when composed from within
+                    // the XR-overrides NavigationSuiteScaffold.
+                    .offset(z = 1.dp)
+                    .height(XrThreePaneScaffoldTokens.PanelHeight)
         ) {
             var drawSpacer = false // Only draws spacers after the first pane is drawn
             paneOrder.each { role ->
                 when (role) {
                     ThreePaneScaffoldRole.Primary -> {
-                        Panel(
-                            scaffoldDirective,
-                            XrThreePaneScaffoldTokens.PrimaryPanePanelWidth,
-                            drawSpacer,
-                            primaryPane,
-                        )
+                        Panel(scaffoldDirective, drawSpacer, primaryPane)
                         drawSpacer = true
                     }
                     ThreePaneScaffoldRole.Secondary -> {
-                        Panel(
-                            scaffoldDirective,
-                            XrThreePaneScaffoldTokens.SecondaryPanePanelWidth,
-                            drawSpacer,
-                            secondaryPane,
-                        )
+                        Panel(scaffoldDirective, drawSpacer, secondaryPane)
                         drawSpacer = true
                     }
                     ThreePaneScaffoldRole.Tertiary ->
                         if (tertiaryPane != null) {
-                            Panel(
-                                scaffoldDirective,
-                                XrThreePaneScaffoldTokens.TertiaryPanePanelWidth,
-                                drawSpacer,
-                                tertiaryPane,
-                            )
+                            Panel(scaffoldDirective, drawSpacer, tertiaryPane)
                             drawSpacer = true
                         }
                 }
@@ -117,34 +98,17 @@
 
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
 @Composable
+@SubspaceComposable
 private fun Panel(
     scaffoldDirective: PaneScaffoldDirective,
-    defaultPreferredWidth: Dp,
     drawSpacer: Boolean,
-    content: @Composable () -> Unit,
+    content: @Composable @SubspaceComposable () -> Unit,
 ) {
     if (drawSpacer) {
         SpatialLayoutSpacer(SubspaceModifier.width(scaffoldDirective.horizontalPartitionSpacerSize))
     }
 
-    SpatialPanel(SubspaceModifier.width(defaultPreferredWidth).fillMaxHeight()) {
-        Layout(content) { measurables, constraints ->
-            val measurable = measurables.getOrNull(0)
-            if (measurable == null) {
-                return@Layout layout(
-                    defaultPreferredWidth.toPx().roundToInt(),
-                    constraints.maxHeight,
-                ) {}
-            }
-            val parentData = measurable.parentData as? PaneScaffoldParentData
-            val widthFloat = parentData?.preferredWidth ?: defaultPreferredWidth
-            val width = widthFloat.toPx().roundToInt()
-            val height = constraints.maxHeight
-            return@Layout layout(width, height) {
-                measurable.measure(Constraints.fixed(width, height)).place(0, 0)
-            }
-        }
-    }
+    content()
 }
 
 /**
@@ -172,7 +136,7 @@
 }
 
 // TODO(conradchen): Confirm the values with design
-private object XrThreePaneScaffoldTokens {
+internal object XrThreePaneScaffoldTokens {
     val PanelHeight = 1024.dp
     val PrimaryPanePanelWidth = 800.dp
     val SecondaryPanePanelWidth = 412.dp