[go: nahoru, domu]

feat: add left/right animation and minor prop renaming in Carousel

* Allow different animations for Carousel and CarouselItem when moving to previous or next slide
* Created a CarouselScope class which provides the CarouselItem composable
* Renamed "timeToDisplayMillis" to "autoScrollDurationMillis"

Test: Updated existing tests

Relnote: "Configure different animation in Carousel and CarouselItem when scrolling to previous or next slide, created CarouselScope &
moved CarouselItem public composable inside it and updated `timeToDisplayMillis` prop to `autoScrollDurationMillis` in Carousel"

Change-Id: Iae656d7782425e179772216a43bab841fceb496c
diff --git a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FeaturedCarousel.kt b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FeaturedCarousel.kt
index 15d409d..1d4abee 100644
--- a/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FeaturedCarousel.kt
+++ b/tv/integration-tests/demos/src/main/java/androidx/tv/integration/demos/FeaturedCarousel.kt
@@ -16,6 +16,7 @@
 
 package androidx.tv.integration.demos
 
+import androidx.compose.animation.ExperimentalAnimationApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.focusable
@@ -42,11 +43,10 @@
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
-import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.Carousel
 import androidx.tv.material3.CarouselDefaults
-import androidx.tv.material3.CarouselItem
 import androidx.tv.material3.CarouselState
+import androidx.tv.material3.ExperimentalTvMaterial3Api
 
 @Composable
 fun FeaturedCarouselContent() {
@@ -95,7 +95,7 @@
         .onFocusChanged { isFocused = it.isFocused }
 }
 
-@OptIn(ExperimentalTvMaterial3Api::class)
+@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
 @Composable
 internal fun FeaturedCarousel(modifier: Modifier = Modifier) {
     val backgrounds = listOf(
@@ -127,7 +127,6 @@
         }
     ) { itemIndex ->
         CarouselItem(
-            overlayEnterTransitionStartDelayMillis = 0,
             background = {
                 Box(
                     modifier = Modifier
@@ -136,18 +135,22 @@
                 )
             }
         ) {
-            OverlayButton()
+            Box(modifier = Modifier) {
+                OverlayButton(
+                    modifier = Modifier
+                )
+            }
         }
     }
 }
 
 @Composable
-private fun OverlayButton() {
+private fun OverlayButton(modifier: Modifier = Modifier) {
     var isFocused by remember { mutableStateOf(false) }
 
     Button(
          },
-        modifier = Modifier
+        modifier = modifier
             .onFocusChanged { isFocused = it.isFocused }
             .padding(40.dp)
             .border(
diff --git a/tv/samples/src/main/java/androidx/tv/samples/CarouselSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/CarouselSamples.kt
index b235403..ba6bdb0 100644
--- a/tv/samples/src/main/java/androidx/tv/samples/CarouselSamples.kt
+++ b/tv/samples/src/main/java/androidx/tv/samples/CarouselSamples.kt
@@ -17,6 +17,7 @@
 package androidx.tv.samples
 
 import androidx.annotation.Sampled
+import androidx.compose.animation.ExperimentalAnimationApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.layout.Box
@@ -39,13 +40,12 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.unit.dp
-import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.Carousel
 import androidx.tv.material3.CarouselDefaults
-import androidx.tv.material3.CarouselItem
 import androidx.tv.material3.CarouselState
+import androidx.tv.material3.ExperimentalTvMaterial3Api
 
-@OptIn(ExperimentalTvMaterial3Api::class)
+@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
 @Sampled
 @Composable
 fun SimpleCarousel() {
@@ -62,7 +62,6 @@
             .fillMaxWidth(),
     ) { itemIndex ->
         CarouselItem(
-            overlayEnterTransitionStartDelayMillis = 0,
             background = {
                 Box(
                     modifier = Modifier
@@ -92,7 +91,7 @@
     }
 }
 
-@OptIn(ExperimentalTvMaterial3Api::class)
+@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
 @Sampled
 @Composable
 fun CarouselIndicatorWithRectangleShape() {
@@ -132,7 +131,6 @@
         }
     ) { itemIndex ->
         CarouselItem(
-            overlayEnterTransitionStartDelayMillis = 0,
             background = {
                 Box(
                     modifier = Modifier
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt
index 3e782bc..1c7d493 100644
--- a/tv/tv-material/api/current.txt
+++ b/tv/tv-material/api/current.txt
@@ -21,6 +21,9 @@
   public final class ImmersiveListKt {
   }
 
+  public final class KeyEventUtilsKt {
+  }
+
   public final class MaterialTheme {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ColorScheme getColorScheme();
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.Shapes getShapes();
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
index 1e8c90f..4afda2a 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -6,29 +6,33 @@
 
   @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CarouselDefaults {
     method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public void IndicatorRow(int slideCount, int activeSlideIndex, optional androidx.compose.ui.Modifier modifier, optional float spacing, optional kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> indicator);
-    method public androidx.compose.animation.EnterTransition getEnterTransition();
-    method public androidx.compose.animation.ExitTransition getExitTransition();
-    property public final androidx.compose.animation.EnterTransition EnterTransition;
-    property public final androidx.compose.animation.ExitTransition ExitTransition;
+    method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransform();
+    property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransform;
     field public static final androidx.tv.material3.CarouselDefaults INSTANCE;
     field public static final long TimeToDisplaySlideMillis = 5000L; // 0x1388L
   }
 
   @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CarouselItemDefaults {
-    method public androidx.compose.animation.EnterTransition getOverlayEnterTransition();
-    method public androidx.compose.animation.ExitTransition getOverlayExitTransition();
-    property public final androidx.compose.animation.EnterTransition OverlayEnterTransition;
-    property public final androidx.compose.animation.ExitTransition OverlayExitTransition;
+    method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransformBackward();
+    method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransformForward();
+    method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransformLeftToRight();
+    method @androidx.compose.runtime.Composable public androidx.compose.animation.ContentTransform getContentTransformRightToLeft();
+    property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransformBackward;
+    property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransformForward;
+    property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransformLeftToRight;
+    property @androidx.compose.runtime.Composable public final androidx.compose.animation.ContentTransform contentTransformRightToLeft;
     field public static final androidx.tv.material3.CarouselItemDefaults INSTANCE;
-    field public static final long OverlayEnterTransitionStartDelayMillis = 200L; // 0xc8L
   }
 
   public final class CarouselItemKt {
-    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void CarouselItem(kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.compose.ui.Modifier modifier, optional long overlayEnterTransitionStartDelayMillis, optional androidx.compose.animation.EnterTransition overlayEnterTransition, optional androidx.compose.animation.ExitTransition overlayExitTransition, kotlin.jvm.functions.Function0<kotlin.Unit> overlay);
   }
 
   public final class CarouselKt {
-    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Carousel(int slideCount, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.CarouselState carouselState, optional long timeToDisplaySlideMillis, optional androidx.compose.animation.EnterTransition enterTransition, optional androidx.compose.animation.ExitTransition exitTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> carouselIndicator, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Carousel(int slideCount, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.CarouselState carouselState, optional long autoScrollDurationMillis, optional androidx.compose.animation.ContentTransform contentTransformForward, optional androidx.compose.animation.ContentTransform contentTransformBackward, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> carouselIndicator, kotlin.jvm.functions.Function2<? super androidx.tv.material3.CarouselScope,? super java.lang.Integer,kotlin.Unit> content);
+  }
+
+  @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CarouselScope {
+    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public void CarouselItem(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.compose.animation.ContentTransform contentTransformForward, optional androidx.compose.animation.ContentTransform contentTransformBackward, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
   @androidx.compose.runtime.Stable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CarouselState {
@@ -138,6 +142,9 @@
     method public androidx.compose.ui.Modifier immersiveListItem(androidx.compose.ui.Modifier, int index);
   }
 
+  public final class KeyEventUtilsKt {
+  }
+
   public final class MaterialTheme {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ColorScheme getColorScheme();
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.Shapes getShapes();
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt
index 3e782bc..1c7d493 100644
--- a/tv/tv-material/api/restricted_current.txt
+++ b/tv/tv-material/api/restricted_current.txt
@@ -21,6 +21,9 @@
   public final class ImmersiveListKt {
   }
 
+  public final class KeyEventUtilsKt {
+  }
+
   public final class MaterialTheme {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ColorScheme getColorScheme();
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.Shapes getShapes();
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselItemTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselItemTest.kt
deleted file mode 100644
index bc41a0f..0000000
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselItemTest.kt
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright 2023 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.tv.material3
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.text.BasicText
-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.focus.onFocusChanged
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.key.NativeKeyEvent
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.SemanticsActions
-import androidx.compose.ui.test.assertIsFocused
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.performSemanticsAction
-import androidx.compose.ui.unit.dp
-import androidx.test.platform.app.InstrumentationRegistry
-import org.junit.Rule
-import org.junit.Test
-
-class CarouselItemTest {
-    @get:Rule
-    val rule = createComposeRule()
-
-    @OptIn(ExperimentalTvMaterial3Api::class)
-    @Test
-    fun carouselItem_overlayVisibleAfterRenderTime() {
-        val overlayEnterTransitionStartDelay: Long = 2000
-        val overlayTag = "overlay"
-        val backgroundTag = "background"
-        rule.setContent {
-            CarouselItem(
-                overlayEnterTransitionStartDelayMillis = overlayEnterTransitionStartDelay,
-                background = {
-                    Box(
-                        Modifier
-                            .testTag(backgroundTag)
-                            .size(200.dp)
-                            .background(Color.Blue)) }) {
-                Box(
-                    Modifier
-                        .testTag(overlayTag)
-                        .size(50.dp)
-                        .background(Color.Red))
-            }
-        }
-
-        // only background is visible
-        rule.onNodeWithTag(backgroundTag).assertExists()
-        rule.onNodeWithTag(overlayTag).assertDoesNotExist()
-
-        // advance clock by `overlayEnterTransitionStartDelay`
-        rule.mainClock.advanceTimeBy(overlayEnterTransitionStartDelay)
-
-        rule.onNodeWithTag(backgroundTag).assertExists()
-        rule.onNodeWithTag(overlayTag).assertExists()
-    }
-
-    @OptIn(ExperimentalTvMaterial3Api::class)
-    @Test
-    fun carouselItem_parentContainerGainsFocused_onBackPress() {
-        rule.setContent {
-            Box(modifier = Modifier
-                .testTag("box-container")
-                .fillMaxSize()
-                .focusable()) {
-                CarouselItem(
-                    overlayEnterTransitionStartDelayMillis = 0,
-                    modifier = Modifier.testTag("carousel-item"),
-                    background = { Box(Modifier.size(300.dp).background(Color.Cyan)) }
-                ) {
-                    SampleButton()
-                }
-            }
-        }
-
-        // Request focus for Carousel Item on start
-        rule.onNodeWithTag("carousel-item")
-            .performSemanticsAction(SemanticsActions.RequestFocus)
-        rule.waitForIdle()
-
-        // Check if overlay button in carousel item is focused
-        rule.onNodeWithTag("sample-button").assertIsFocused()
-
-        // Trigger back press
-        performKeyPress(NativeKeyEvent.KEYCODE_BACK)
-        rule.waitForIdle()
-
-        // Check if carousel item loses focus and parent container gains focus
-        rule.onNodeWithTag("box-container").assertIsFocused()
-    }
-
-    @Composable
-    private fun SampleButton(text: String = "sample-button") {
-        var isFocused by remember { mutableStateOf(false) }
-        BasicText(
-            text = text,
-            modifier = Modifier.testTag(text)
-                .size(100.dp, 20.dp)
-                .background(Color.Yellow)
-                .onFocusChanged { isFocused = it.isFocused }
-                .border(2.dp, if (isFocused) Color.Green else Color.Transparent)
-                .focusable()
-        )
-    }
-
-    private fun performKeyPress(keyCode: Int, count: Int = 1) {
-        repeat(count) {
-            InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode)
-        }
-    }
-}
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselScopeTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselScopeTest.kt
new file mode 100644
index 0000000..10080e0
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselScopeTest.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2023 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.tv.material3
+
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.text.BasicText
+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.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.unit.dp
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Rule
+import org.junit.Test
+
+const val sampleButtonTag = "sample-button"
+
+class CarouselScopeTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
+    @Test
+    fun carouselItem_parentContainerGainsFocused_onBackPress() {
+        val containerBoxTag = "container-box"
+        val carouselItemTag = "carousel-item"
+
+        rule.setContent {
+            val carouselState = remember { CarouselState() }
+            var isContainerBoxFocused by remember { mutableStateOf(false) }
+            Box(
+                modifier = Modifier
+                    .testTag(containerBoxTag)
+                    .fillMaxSize()
+                    .onFocusChanged { isContainerBoxFocused = it.isFocused }
+                    .border(10.dp, if (isContainerBoxFocused) Color.Green else Color.Transparent)
+                    .focusable()
+            ) {
+                CarouselScope(carouselState = carouselState)
+                    .CarouselItem(
+                        modifier = Modifier.testTag(carouselItemTag),
+                        background = {
+                            Box(
+                                modifier = Modifier
+                                    .size(300.dp)
+                                    .background(Color.Cyan))
+                        },
+                        content = { SampleButton() },
+                    )
+            }
+        }
+
+        // Request focus for Carousel Item on start
+        rule.onNodeWithTag(carouselItemTag)
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+        rule.waitForIdle()
+
+        // Check if overlay button in carousel item is focused
+        rule.onNodeWithTag(sampleButtonTag).assertIsFocused()
+
+        // Trigger back press
+        performKeyPress(NativeKeyEvent.KEYCODE_BACK)
+        rule.waitForIdle()
+
+        // Check if carousel item loses focus and parent container gains focus
+        rule.onNodeWithTag(containerBoxTag).assertIsFocused()
+    }
+
+    private fun performKeyPress(keyCode: Int, count: Int = 1) {
+        repeat(count) {
+            InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode)
+        }
+    }
+}
+
+@Composable
+private fun SampleButton(text: String = sampleButtonTag) {
+    var isFocused by remember { mutableStateOf(false) }
+    BasicText(
+        text = text,
+        modifier = Modifier
+            .testTag(text)
+            .size(100.dp, 20.dp)
+            .background(Color.Yellow)
+            .onFocusChanged { isFocused = it.isFocused }
+            .border(2.dp, if (isFocused) Color.Green else Color.Transparent)
+            .focusable()
+    )
+}
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
index 5e2da37..4c152ad 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
@@ -18,6 +18,8 @@
 
 import android.os.SystemClock
 import android.view.KeyEvent
+import androidx.compose.animation.ContentTransform
+import androidx.compose.animation.ExperimentalAnimationApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.focusable
@@ -71,7 +73,6 @@
 
 private const val delayBetweenSlides = 2500L
 private const val animationTime = 900L
-private const val overlayRenderWaitTime = 1500L
 
 @OptIn(ExperimentalTvMaterial3Api::class)
 class CarouselTest {
@@ -274,6 +275,7 @@
         rule.onNodeWithText("Text 2").assertIsDisplayed()
     }
 
+    @OptIn(ExperimentalAnimationApi::class)
     @Test
     fun carousel_pagerIndicatorDisplayed() {
         rule.setContent {
@@ -285,6 +287,7 @@
         rule.onNodeWithTag("indicator").assertIsDisplayed()
     }
 
+    @OptIn(ExperimentalAnimationApi::class)
     @Test
     fun carousel_withAnimatedContent_successfulTransition() {
         rule.setContent {
@@ -298,15 +301,14 @@
             }
         }
 
-        rule.onNodeWithText("Text 1").assertDoesNotExist()
-
-        rule.mainClock.advanceTimeBy(overlayRenderWaitTime + animationTime, true)
+        rule.mainClock.advanceTimeBy(animationTime, true)
         rule.mainClock.advanceTimeByFrame()
 
         rule.onNodeWithText("Text 1").assertIsDisplayed()
         rule.onNodeWithText("PLAY").assertIsDisplayed()
     }
 
+    @OptIn(ExperimentalAnimationApi::class)
     @Test
     fun carousel_withAnimatedContent_successfulFocusIn() {
         rule.setContent {
@@ -320,7 +322,7 @@
             .performSemanticsAction(SemanticsActions.RequestFocus)
 
         // current slide overlay render delay
-        rule.mainClock.advanceTimeBy(animationTime + overlayRenderWaitTime, false)
+        rule.mainClock.advanceTimeBy(animationTime, false)
         rule.mainClock.advanceTimeBy(animationTime, false)
         rule.mainClock.advanceTimeByFrame()
 
@@ -363,6 +365,7 @@
         rule.onNodeWithTag("box-container").assertIsFocused()
     }
 
+    @OptIn(ExperimentalAnimationApi::class)
     @Test
     fun carousel_withCarouselItem_parentContainerGainsFocus_onBackPress() {
         rule.setContent {
@@ -384,7 +387,7 @@
         // Trigger recomposition after requesting focus and advance time to finish animations
         rule.mainClock.advanceTimeByFrame()
         rule.waitForIdle()
-        rule.mainClock.advanceTimeBy(animationTime + overlayRenderWaitTime, false)
+        rule.mainClock.advanceTimeBy(animationTime, false)
         rule.waitForIdle()
 
         // Check if the overlay button is focused
@@ -400,6 +403,7 @@
         rule.onNodeWithTag("box-container").assertIsFocused()
     }
 
+    @OptIn(ExperimentalAnimationApi::class)
     @Test
     fun carousel_scrollToRegainFocus_checkBringIntoView() {
         val focusRequester = FocusRequester()
@@ -429,12 +433,9 @@
                             .border(2.dp, Color.Black),
                         carouselState = remember { CarouselState() },
                         slideCount = 3,
-                        timeToDisplaySlideMillis = delayBetweenSlides
+                        autoScrollDurationMillis = delayBetweenSlides
                     ) {
-                        SampleCarouselSlide(
-                            index = it,
-                            overlayRenderWaitTime = overlayRenderWaitTime,
-                        ) {
+                        SampleCarouselSlide(index = it) {
                             Box {
                                 Column(modifier = Modifier.align(Alignment.BottomStart)) {
                                     BasicText(text = "carousel-frame")
@@ -490,6 +491,7 @@
         assertThat(checkNodeCompletelyVisible(rule, "featured-carousel")).isTrue()
     }
 
+    @OptIn(ExperimentalAnimationApi::class)
     @Test
     fun carousel_zeroSlideCount_shouldNotCrash() {
         val testTag = "emptyCarousel"
@@ -500,6 +502,7 @@
         rule.onNodeWithTag(testTag).assertExists()
     }
 
+    @OptIn(ExperimentalAnimationApi::class)
     @Test
     fun carousel_oneSlideCount_shouldNotCrash() {
         val testTag = "emptyCarousel"
@@ -568,6 +571,7 @@
         rule.onNodeWithText("Button-1").assertIsFocused()
     }
 
+    @OptIn(ExperimentalAnimationApi::class)
     @Test
     fun carousel_manualScrolling_fastMultipleKeyPresses() {
         val carouselState = CarouselState()
@@ -608,14 +612,14 @@
             }
         }
 
-        rule.mainClock.advanceTimeBy(animationTime + overlayRenderWaitTime)
+        rule.mainClock.advanceTimeBy(animationTime)
 
         val finalSlide = slideProgression.sum()
         rule.onNodeWithText("Play $finalSlide").assertIsFocused()
 
         performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 3)
 
-        rule.mainClock.advanceTimeBy((animationTime + overlayRenderWaitTime) * 3)
+        rule.mainClock.advanceTimeBy((animationTime) * 3)
 
         rule.onNodeWithText("Play ${finalSlide + 3}").assertIsFocused()
     }
@@ -678,7 +682,7 @@
             .performSemanticsAction(SemanticsActions.RequestFocus)
 
         // current slide overlay render delay
-        rule.mainClock.advanceTimeBy(animationTime + overlayRenderWaitTime, false)
+        rule.mainClock.advanceTimeBy(animationTime, false)
         rule.mainClock.advanceTimeBy(animationTime, false)
         rule.mainClock.advanceTimeByFrame()
 
@@ -728,7 +732,7 @@
             .performSemanticsAction(SemanticsActions.RequestFocus)
 
         // current slide overlay render delay
-        rule.mainClock.advanceTimeBy(animationTime + overlayRenderWaitTime, false)
+        rule.mainClock.advanceTimeBy(animationTime, false)
         rule.mainClock.advanceTimeBy(animationTime, false)
         rule.mainClock.advanceTimeByFrame()
 
@@ -794,13 +798,13 @@
     }
 }
 
-@OptIn(ExperimentalTvMaterial3Api::class)
+@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
 @Composable
 private fun SampleCarousel(
     carouselState: CarouselState = remember { CarouselState() },
     slideCount: Int = 3,
     timeToDisplaySlideMillis: Long = delayBetweenSlides,
-    content: @Composable (index: Int) -> Unit
+    content: @Composable CarouselScope.(index: Int) -> Unit
 ) {
     Carousel(
         modifier = Modifier
@@ -810,7 +814,7 @@
             .testTag("pager"),
         carouselState = carouselState,
         slideCount = slideCount,
-        timeToDisplaySlideMillis = timeToDisplaySlideMillis,
+        autoScrollDurationMillis = timeToDisplaySlideMillis,
         carouselIndicator = {
             CarouselDefaults.IndicatorRow(
                 modifier = Modifier
@@ -821,22 +825,22 @@
                 slideCount = slideCount
             )
         },
-        content = content,
+        content = { content(it) },
     )
 }
 
-@OptIn(ExperimentalTvMaterial3Api::class)
+@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
 @Composable
-private fun SampleCarouselSlide(
+private fun CarouselScope.SampleCarouselSlide(
     index: Int,
     modifier: Modifier = Modifier,
-    overlayRenderWaitTime: Long = CarouselItemDefaults.OverlayEnterTransitionStartDelayMillis,
+    contentTransformForward: ContentTransform =
+        CarouselItemDefaults.contentTransformForward,
     content: (@Composable () -> Unit) = { SampleButton("Play $index") },
 ) {
-
     CarouselItem(
         modifier = modifier,
-        overlayEnterTransitionStartDelayMillis = overlayRenderWaitTime,
+        contentTransformForward = contentTransformForward,
         background = {
             Box(
                 modifier = Modifier
@@ -844,9 +848,10 @@
                     .background(Color.Red)
                     .border(2.dp, Color.Blue)
             )
-        },
-        overlay = content
-    )
+        }
+    ) {
+        content()
+    }
 }
 
 @Composable
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
index 2d04b43..442c810 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
@@ -21,8 +21,7 @@
 import android.view.KeyEvent.KEYCODE_DPAD_RIGHT
 import androidx.compose.animation.AnimatedContent
 import androidx.compose.animation.AnimatedVisibilityScope
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.ContentTransform
 import androidx.compose.animation.ExperimentalAnimationApi
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.fadeIn
@@ -81,10 +80,13 @@
  * @param modifier Modifier applied to the Carousel.
  * @param slideCount total number of slides present in the carousel.
  * @param carouselState state associated with this carousel.
- * @param timeToDisplaySlideMillis duration for which slide should be visible before moving to
+ * @param autoScrollDurationMillis duration for which slide should be visible before moving to
  * the next slide.
- * @param enterTransition transition used to bring a slide into view.
- * @param exitTransition transition used to remove a slide from view.
+ * @param contentTransformForward animation transform applied when we are moving forward in the
+ * carousel while scrolling
+ * @param contentTransformBackward animation transform applied when we are moving backward in the
+ * carousel while scrolling
+ * in the next slide
  * @param carouselIndicator indicator showing the position of the current slide among all slides.
  * @param content defines the slides for a given index.
  */
@@ -96,9 +98,9 @@
     slideCount: Int,
     modifier: Modifier = Modifier,
     carouselState: CarouselState = remember { CarouselState() },
-    timeToDisplaySlideMillis: Long = CarouselDefaults.TimeToDisplaySlideMillis,
-    enterTransition: EnterTransition = CarouselDefaults.EnterTransition,
-    exitTransition: ExitTransition = CarouselDefaults.ExitTransition,
+    autoScrollDurationMillis: Long = CarouselDefaults.TimeToDisplaySlideMillis,
+    contentTransformForward: ContentTransform = CarouselDefaults.contentTransform,
+    contentTransformBackward: ContentTransform = CarouselDefaults.contentTransform,
     carouselIndicator:
     @Composable BoxScope.() -> Unit = {
         CarouselDefaults.IndicatorRow(
@@ -109,7 +111,7 @@
                 .padding(16.dp),
         )
     },
-    content: @Composable (index: Int) -> Unit
+    content: @Composable CarouselScope.(index: Int) -> Unit
 ) {
     CarouselStateUpdater(carouselState, slideCount)
     var focusState: FocusState? by remember { mutableStateOf(null) }
@@ -119,7 +121,7 @@
     var isAutoScrollActive by remember { mutableStateOf(false) }
 
     AutoScrollSideEffect(
-        timeToDisplaySlideMillis = timeToDisplaySlideMillis,
+        autoScrollDurationMillis = autoScrollDurationMillis,
         slideCount = slideCount,
         carouselState = carouselState,
         doAutoScroll = shouldPerformAutoScroll(focusState),
@@ -147,8 +149,14 @@
     ) {
         AnimatedContent(
             targetState = carouselState.activeSlideIndex,
-            transitionSpec = { enterTransition.with(exitTransition) }
-        ) { slideIndex ->
+            transitionSpec = {
+                if (carouselState.isMovingBackward) {
+                    contentTransformBackward
+                } else {
+                    contentTransformForward
+                }
+            }
+        ) { activeSlideIndex ->
             LaunchedEffect(Unit) {
                 this@AnimatedContent.onAnimationCompletion {
                     // Outer box is focused
@@ -163,7 +171,8 @@
             // IndexOutOfBoundsException. Guarding against this by checking against slideCount
             // before invoking.
             if (slideCount > 0) {
-                content.invoke(if (slideIndex < slideCount) slideIndex else 0)
+                CarouselScope(carouselState = carouselState)
+                    .content(if (activeSlideIndex < slideCount) activeSlideIndex else 0)
             }
         }
         this.carouselIndicator()
@@ -187,7 +196,7 @@
 @OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
 private fun AutoScrollSideEffect(
-    timeToDisplaySlideMillis: Long,
+    autoScrollDurationMillis: Long,
     slideCount: Int,
     carouselState: CarouselState,
     doAutoScroll: Boolean,
@@ -199,7 +208,7 @@
         LaunchedEffect(carouselState) {
             while (true) {
                 yield()
-                delay(timeToDisplaySlideMillis)
+                delay(autoScrollDurationMillis)
                 if (carouselState.activePauseHandlesCount > 0) {
                     snapshotFlow { carouselState.activePauseHandlesCount }
                         .first { pauseHandleCount -> pauseHandleCount == 0 }
@@ -310,6 +319,13 @@
         internal set
 
     /**
+     * Tracks whether we are scrolling backward in the Carousel. By default, we are moving forward
+     * because of auto-scroll
+     */
+    internal var isMovingBackward = false
+        private set
+
+    /**
      * Pauses the auto-scrolling behaviour of Carousel.
      * The pause request is ignored if [slideIndex] is not the current slide that is visible.
      * Returns a [ScrollPauseHandle] that can be used to resume
@@ -329,6 +345,8 @@
         // No slides available for carousel
         if (slideCount == 0) return
 
+        isMovingBackward = true
+
         // Go to previous slide
         activeSlideIndex = floorMod(activeSlideIndex - 1, slideCount)
     }
@@ -337,6 +355,8 @@
         // No slides available for carousel
         if (slideCount == 0) return
 
+        isMovingBackward = false
+
         // Go to next slide
         activeSlideIndex = floorMod(activeSlideIndex + 1, slideCount)
     }
@@ -388,14 +408,13 @@
     const val TimeToDisplaySlideMillis: Long = 5000
 
     /**
-     * Default transition used to bring the slide into view
+     * Transition applied when bringing it into view and removing it from the view
      */
-    val EnterTransition: EnterTransition = fadeIn(animationSpec = tween(100))
-
-    /**
-     * Default transition used to remove the slide from view
-     */
-    val ExitTransition: ExitTransition = fadeOut(animationSpec = tween(100))
+    @OptIn(ExperimentalAnimationApi::class)
+    val contentTransform: ContentTransform
+    @Composable get() =
+        fadeIn(animationSpec = tween(100))
+            .with(fadeOut(animationSpec = tween(100)))
 
     /**
      * An indicator showing the position of the current active slide among the slides of the
@@ -416,7 +435,7 @@
         spacing: Dp = 8.dp,
         indicator: @Composable (isActive: Boolean) -> Unit = { isActive ->
             val activeColor = Color.White
-            val inactiveColor = activeColor.copy(alpha = 0.5f)
+            val inactiveColor = activeColor.copy(alpha = 0.3f)
             Box(
                 modifier = Modifier
                     .size(8.dp)
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/CarouselItem.kt b/tv/tv-material/src/main/java/androidx/tv/material3/CarouselItem.kt
index 96114ca..fff09aa 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/CarouselItem.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/CarouselItem.kt
@@ -16,137 +16,149 @@
 
 package androidx.tv.material3
 
-import android.view.KeyEvent
 import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.ContentTransform
+import androidx.compose.animation.ExperimentalAnimationApi
 import androidx.compose.animation.slideInHorizontally
 import androidx.compose.animation.slideOutHorizontally
+import androidx.compose.animation.with
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
 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.runtime.snapshotFlow
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusDirection
 import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown
-import androidx.compose.ui.input.key.key
-import androidx.compose.ui.input.key.nativeKeyCode
 import androidx.compose.ui.input.key.onKeyEvent
-import androidx.compose.ui.input.key.type
 import androidx.compose.ui.platform.LocalFocusManager
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.first
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.tv.material3.KeyEventPropagation.ContinuePropagation
 
 /**
  * This composable is intended for use in Carousel.
  * A composable that has
  * - a [background] layer that is rendered as soon as the composable is visible.
- * - an [overlay] layer that is rendered after a delay of
- *   [overlayEnterTransitionStartDelayMillis].
+ * - a [content] layer that is rendered on top of the [background]
  *
- * @param modifier modifier applied to the CarouselItem.
- * @param overlayEnterTransitionStartDelayMillis time between the rendering of the
- * background and the overlay.
- * @param overlayEnterTransition animation used to bring the overlay into view.
- * @param overlayExitTransition animation used to remove the overlay from view.
- * @param background composable defining the background of the slide.
- * @param overlay composable defining the content overlaid on the background.
+ * @param background composable defining the background of the slide
+ * @param slideIndex current active slide index of the carousel
+ * @param modifier modifier applied to the CarouselItem
+ * @param contentTransform content transform to be applied to the content of the slide when
+ * scrolling
+ * @param content composable defining the content displayed on top of the background
  */
 @Suppress("IllegalExperimentalApiUsage")
-@OptIn(ExperimentalComposeUiApi::class)
+@OptIn(ExperimentalAnimationApi::class, ExperimentalComposeUiApi::class)
 @ExperimentalTvMaterial3Api
 @Composable
-fun CarouselItem(
-    background: @Composable () -> Unit,
+internal fun CarouselItem(
+    slideIndex: Int,
     modifier: Modifier = Modifier,
-    overlayEnterTransitionStartDelayMillis: Long =
-        CarouselItemDefaults.OverlayEnterTransitionStartDelayMillis,
-    overlayEnterTransition: EnterTransition = CarouselItemDefaults.OverlayEnterTransition,
-    overlayExitTransition: ExitTransition = CarouselItemDefaults.OverlayExitTransition,
-    overlay: @Composable () -> Unit
+    background: @Composable () -> Unit = {},
+    contentTransform: ContentTransform =
+        CarouselItemDefaults.contentTransformForward,
+    content: @Composable () -> Unit,
 ) {
-    val overlayVisible = remember { MutableTransitionState(initialState = false) }
     var containerBoxFocusState: FocusState? by remember { mutableStateOf(null) }
     val focusManager = LocalFocusManager.current
     var exitFocus by remember { mutableStateOf(false) }
 
-    LaunchedEffect(overlayVisible) {
-        overlayVisible.onAnimationCompletion {
-            // slide has loaded completely.
-            if (containerBoxFocusState?.isFocused == true) {
-                focusManager.moveFocus(FocusDirection.Enter)
-            }
-        }
+    var isVisible by remember { mutableStateOf(false) }
+
+    DisposableEffect(slideIndex) {
+        isVisible = true
+        onDispose { isVisible = false }
     }
 
     // This box holds the focus until the overlay animation completes
-    Box(modifier = modifier
-        .onKeyEvent {
-            exitFocus = it.key.nativeKeyCode == KeyEvent.KEYCODE_BACK && it.type == KeyDown
-            false
-        }
-        .onFocusChanged {
-            containerBoxFocusState = it
-            if (it.isFocused && exitFocus) {
-                focusManager.moveFocus(FocusDirection.Exit)
-                exitFocus = false
-            } else if (it.isFocused && overlayVisible.isIdle && overlayVisible.currentState) {
-                focusManager.moveFocus(FocusDirection.Enter)
+    Box(
+        modifier = modifier
+            .onKeyEvent {
+                exitFocus = it.isBackPress() && it.isTypeKeyDown()
+                ContinuePropagation
             }
-        }
-        .focusable()
+            .onFocusChanged {
+                containerBoxFocusState = it
+                if (it.isFocused && exitFocus) {
+                    focusManager.moveFocus(FocusDirection.Exit)
+                    exitFocus = false
+                }
+            }
+            .focusable()
     ) {
         background()
 
-        LaunchedEffect(overlayVisible) {
-            // After the delay, set overlay-visibility to true and trigger the animation to show the
-            // overlay.
-            delay(overlayEnterTransitionStartDelayMillis)
-            overlayVisible.targetState = true
-        }
-
         AnimatedVisibility(
-            modifier = Modifier.align(Alignment.BottomStart),
-            visibleState = overlayVisible,
-            enter = overlayEnterTransition,
-            exit = overlayExitTransition
+            visible = isVisible,
+            enter = contentTransform.targetContentEnter,
+            exit = contentTransform.initialContentExit,
         ) {
-            overlay.invoke()
+            LaunchedEffect(transition.isRunning, containerBoxFocusState?.isFocused) {
+                if (!transition.isRunning && containerBoxFocusState?.isFocused == true) {
+                    focusManager.moveFocus(FocusDirection.Enter)
+                }
+            }
+            content.invoke()
         }
     }
 }
 
-private suspend fun MutableTransitionState<Boolean>.onAnimationCompletion(
-    action: suspend () -> Unit
-) {
-    snapshotFlow { isIdle && currentState }.first { it }
-    action.invoke()
-}
-
 @ExperimentalTvMaterial3Api
 object CarouselItemDefaults {
     /**
-     * Default delay between the background being rendered and the overlay being rendered.
+     * Transform the content from right to left
      */
-    const val OverlayEnterTransitionStartDelayMillis: Long = 200
+    // Keeping this as public so that users can access it directly without the isLTR helper
+    @Suppress("IllegalExperimentalApiUsage")
+    @OptIn(ExperimentalAnimationApi::class)
+    val contentTransformRightToLeft: ContentTransform
+        @Composable get() =
+            slideInHorizontally { it * 4 }
+                .with(slideOutHorizontally { it * 4 })
 
     /**
-     * Default transition to bring the overlay into view.
+     * Transform the content from left to right
      */
-    val OverlayEnterTransition: EnterTransition = slideInHorizontally(initialOffsetX = { it * 4 })
+    // Keeping this as public so that users can access it directly without the isLTR helper
+    @Suppress("IllegalExperimentalApiUsage")
+    @OptIn(ExperimentalAnimationApi::class)
+    val contentTransformLeftToRight: ContentTransform
+        @Composable get() =
+            slideInHorizontally()
+                .with(slideOutHorizontally())
 
     /**
-     * Default transition to remove overlay from view.
+     * Content transform applied when moving forward taking isLTR into account
      */
-    val OverlayExitTransition: ExitTransition = slideOutHorizontally()
+    @Suppress("IllegalExperimentalApiUsage")
+    @OptIn(ExperimentalAnimationApi::class)
+    val contentTransformForward
+        @Composable get() =
+            if (isLtr())
+                contentTransformRightToLeft
+            else
+                contentTransformLeftToRight
+
+    /**
+     * Content transform applied when moving backward taking isLTR into account
+     */
+    @Suppress("IllegalExperimentalApiUsage")
+    @OptIn(ExperimentalAnimationApi::class)
+    val contentTransformBackward
+        @Composable get() =
+            if (isLtr())
+                contentTransformLeftToRight
+            else
+                contentTransformRightToLeft
 }
+
+@Composable
+private fun isLtr() = LocalLayoutDirection.current == LayoutDirection.Ltr
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/CarouselScope.kt b/tv/tv-material/src/main/java/androidx/tv/material3/CarouselScope.kt
new file mode 100644
index 0000000..3cea56b
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/CarouselScope.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2023 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.tv.material3
+
+import androidx.compose.animation.ContentTransform
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+/**
+ * CarouselScope provides a [CarouselScope.CarouselItem] function which you can use to
+ * provide the slide's animation, background and the inner content.
+ */
+@ExperimentalTvMaterial3Api
+class CarouselScope @OptIn(ExperimentalTvMaterial3Api::class)
+internal constructor(private val carouselState: CarouselState) {
+    /**
+     * [CarouselScope.CarouselItem] can be used to define a slide's animation, background, and
+     * content. Using this is optional and you can choose to define your own CarouselItem from
+     * scratch
+     *
+     * @param modifier modifier applied to the CarouselItem
+     * @param background composable defining the background of the slide
+     * @param contentTransformForward content transform to be applied to the content of the slide
+     * when scrolling forward in the carousel
+     * @param contentTransformBackward content transform to be applied to the content of the slide
+     * when scrolling backward in the carousel
+     * @param content composable defining the content displayed on top of the background
+     */
+    @Composable
+    @Suppress("IllegalExperimentalApiUsage")
+    @OptIn(ExperimentalAnimationApi::class)
+    @ExperimentalTvMaterial3Api
+    fun CarouselItem(
+        modifier: Modifier = Modifier,
+        background: @Composable () -> Unit = {},
+        contentTransformForward: ContentTransform =
+            CarouselItemDefaults.contentTransformForward,
+        contentTransformBackward: ContentTransform =
+            CarouselItemDefaults.contentTransformBackward,
+        content: @Composable () -> Unit
+    ) {
+        CarouselItem(
+            background = background,
+            slideIndex = carouselState.activeSlideIndex,
+            contentTransform =
+            if (carouselState.isMovingBackward)
+                contentTransformBackward
+            else
+                contentTransformForward,
+            modifier = modifier,
+            content = content,
+        )
+    }
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/KeyEventUtils.kt b/tv/tv-material/src/main/java/androidx/tv/material3/KeyEventUtils.kt
new file mode 100644
index 0000000..d13b221
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/KeyEventUtils.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 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.tv.material3
+
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.nativeKeyCode
+import androidx.compose.ui.input.key.type
+
+/**
+ * Checks if the `Back` key is pressed
+ */
+internal fun KeyEvent.isBackPress() = key.nativeKeyCode == android.view.KeyEvent.KEYCODE_BACK
+
+/**
+ * Checks if the keyEventType is `KeyDown`
+ */
+internal fun KeyEvent.isTypeKeyDown() = type == KeyDown
\ No newline at end of file