Revert "Revert "Featured Carousel feature in TV Compose.""

This reverts commit ab6c4b828736c71a91f14f81d8a45ff84503ea03.

Reason for revert: Reintroducing the Feature Carousel with code updated to
- use FocusDirection.Enter instead of FocusDirection.In
- cleaning up build.gradle as per the suggestion
- updating the test to fix the failure

Test: Tested manually by creating a sample app and have written
Integration Tests as well.

Relnote: "Adding the feature-carousel component to
          tv-compose library"

Change-Id: I7b629ac1bb828844ba6282a3dd1eb577dd994429
diff --git a/settings.gradle b/settings.gradle
index eea363e..f5a6dcc 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -861,6 +861,7 @@
 includeProject(":transition:transition-ktx", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":tv:tv-foundation", [BuildType.COMPOSE])
 includeProject(":tv:tv-material", [BuildType.COMPOSE])
+includeProject(":tv:tv-material-samples", "tv/tv-material/samples", [BuildType.COMPOSE])
 includeProject(":tvprovider:tvprovider", [BuildType.MAIN])
 includeProject(":vectordrawable:integration-tests:testapp", [BuildType.MAIN])
 includeProject(":vectordrawable:vectordrawable", [BuildType.MAIN])
diff --git a/tv/tv-foundation/build.gradle b/tv/tv-foundation/build.gradle
index c8dd293..2584bf1 100644
--- a/tv/tv-foundation/build.gradle
+++ b/tv/tv-foundation/build.gradle
@@ -14,7 +14,6 @@
  * limitations under the License.
  */
 
-import androidx.build.AndroidXComposePlugin
 import androidx.build.LibraryType
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
@@ -31,24 +30,23 @@
 dependencies {
     api(libs.kotlinStdlib)
 
-    api("androidx.annotation:annotation:1.1.0")
-    api(project(":compose:animation:animation"))
-    api(project(':compose:runtime:runtime'))
-    api(project(":compose:ui:ui"))
+    def composeVersion = '1.2.1'
+
+    api("androidx.annotation:annotation:1.4.0")
+    api("androidx.compose.animation:animation:$composeVersion")
+    api("androidx.compose.runtime:runtime:$composeVersion")
+    api("androidx.compose.ui:ui:$composeVersion")
 
     implementation(libs.kotlinStdlibCommon)
+    implementation("androidx.compose.foundation:foundation:$composeVersion")
     implementation(project(":compose:foundation:foundation"))
-    implementation(project(":compose:foundation:foundation-layout"))
-    implementation(project(":compose:ui:ui-graphics"))
-    implementation(project(":compose:ui:ui-text"))
-    implementation(project(":compose:ui:ui-util"))
-    implementation("androidx.profileinstaller:profileinstaller:1.2.0-alpha02")
+    implementation("androidx.compose.foundation:foundation-layout:$composeVersion")
+    implementation("androidx.compose.ui:ui-graphics:$composeVersion")
+    implementation("androidx.compose.ui:ui-text:$composeVersion")
+    implementation("androidx.compose.ui:ui-util:$composeVersion")
+    implementation("androidx.profileinstaller:profileinstaller:1.2.0")
 
-    testImplementation(libs.testRules)
-    testImplementation(libs.testRunner)
-    testImplementation(libs.junit)
-    implementation(libs.truth)
-
+    androidTestImplementation(libs.truth)
     androidTestImplementation(project(":compose:ui:ui-test"))
     androidTestImplementation(project(":compose:ui:ui-test-junit4"))
     androidTestImplementation(project(":compose:test-utils"))
@@ -58,10 +56,8 @@
 android {
     namespace "androidx.tv.foundation"
     defaultConfig {
-        minSdkVersion 28
+        minSdkVersion 21
     }
-    // Use Robolectric 4.+
-    testOptions.unitTests.includeAndroidResources = true
     lintOptions {
         disable 'IllegalExperimentalApiUsage' // TODO (b/233188423): Address before moving to beta
     }
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt
index 37862be..3328e28 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt
@@ -95,12 +95,10 @@
      * be recomposed on every change causing potential performance issues.
      *
      * If you want to run some side effects like sending an analytics event or updating a state
-     * based on this value consider using "snapshotFlow":
-     * @sample androidx.compose.foundation.samples.UsingListScrollPositionForSideEffectSample
+     * based on this value consider using "snapshotFlow".
      *
      * If you need to use it in the composition then consider wrapping the calculation into a
-     * derived state in order to only have recompositions when the derived value changes:
-     * @sample androidx.compose.foundation.samples.UsingListScrollPositionInCompositionSample
+     * derived state in order to only have recompositions when the derived value changes.
      */
     val firstVisibleItemIndex: Int get() = scrollPosition.index.value
 
@@ -127,8 +125,7 @@
      * Therefore, avoid using it in the composition.
      *
      * If you want to run some side effects like sending an analytics event or updating a state
-     * based on this value consider using "snapshotFlow":
-     * @sample androidx.compose.foundation.samples.UsingListLayoutInfoForSideEffectSample
+     * based on this value consider using "snapshotFlow"
      */
     val layoutInfo: TvLazyListLayoutInfo get() = layoutInfoState.value
 
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScope.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScope.kt
index 441895d..59f42ba 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScope.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/TvLazyListItemScope.kt
@@ -83,8 +83,6 @@
      * enable item reordering animations. Aside from item reordering all other position changes
      * caused by events like arrangement or alignment changes will also be animated.
      *
-     * @sample androidx.compose.foundation.samples.ItemPlacementAnimationSample
-     *
      * @param animationSpec a finite animation that will be used to animate the item placement.
      */
     @ExperimentalFoundationApi
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt
index e6f50d0..0ed6747 100644
--- a/tv/tv-material/api/current.txt
+++ b/tv/tv-material/api/current.txt
@@ -1 +1,18 @@
 // Signature format: 4.0
+package androidx.tv.material.carousel {
+
+  public final class CarouselItemKt {
+  }
+
+  public final class CarouselKt {
+  }
+
+}
+
+package androidx.tv.material.pager {
+
+  public final class PagerKt {
+  }
+
+}
+
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
index e6f50d0..4e063ddc4 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -1 +1,59 @@
 // Signature format: 4.0
+package androidx.tv.material {
+
+  @kotlin.RequiresOptIn(message="This tv-material API is experimental and likely to change or be removed in the future.") public @interface ExperimentalTvMaterialApi {
+  }
+
+}
+
+package androidx.tv.material.carousel {
+
+  @androidx.tv.material.ExperimentalTvMaterialApi public final class CarouselDefaults {
+    method @androidx.compose.runtime.Composable @androidx.tv.material.ExperimentalTvMaterialApi public void Indicator(androidx.tv.material.carousel.CarouselState carouselState, int slideCount, optional androidx.compose.ui.Modifier modifier);
+    method public androidx.compose.animation.EnterTransition getEnterTransition();
+    method public androidx.compose.animation.ExitTransition getExitTransition();
+    method public long getTimeToDisplaySlideMillis();
+    property public final androidx.compose.animation.EnterTransition EnterTransition;
+    property public final androidx.compose.animation.ExitTransition ExitTransition;
+    property public final long TimeToDisplaySlideMillis;
+    field public static final androidx.tv.material.carousel.CarouselDefaults INSTANCE;
+  }
+
+  @androidx.tv.material.ExperimentalTvMaterialApi public final class CarouselItemDefaults {
+    method public androidx.compose.animation.EnterTransition getOverlayEnterTransition();
+    method public long getOverlayEnterTransitionStartDelayMillis();
+    method public androidx.compose.animation.ExitTransition getOverlayExitTransition();
+    property public final androidx.compose.animation.EnterTransition OverlayEnterTransition;
+    property public final long OverlayEnterTransitionStartDelayMillis;
+    property public final androidx.compose.animation.ExitTransition OverlayExitTransition;
+    field public static final androidx.tv.material.carousel.CarouselItemDefaults INSTANCE;
+  }
+
+  public final class CarouselItemKt {
+    method @androidx.compose.runtime.Composable @androidx.tv.material.ExperimentalTvMaterialApi 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.material.ExperimentalTvMaterialApi public static void Carousel(int slideCount, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material.carousel.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);
+  }
+
+  @androidx.compose.runtime.Stable @androidx.tv.material.ExperimentalTvMaterialApi public final class CarouselState {
+    ctor public CarouselState(optional int initialSlideIndex);
+    method public int getSlideIndex();
+    method public androidx.tv.material.carousel.ScrollPauseHandle pauseAutoScroll(int slideIndex);
+    property public final int slideIndex;
+  }
+
+  @androidx.tv.material.ExperimentalTvMaterialApi public sealed interface ScrollPauseHandle {
+    method public void resumeAutoScroll();
+  }
+
+}
+
+package androidx.tv.material.pager {
+
+  public final class PagerKt {
+  }
+
+}
+
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt
index e6f50d0..0ed6747 100644
--- a/tv/tv-material/api/restricted_current.txt
+++ b/tv/tv-material/api/restricted_current.txt
@@ -1 +1,18 @@
 // Signature format: 4.0
+package androidx.tv.material.carousel {
+
+  public final class CarouselItemKt {
+  }
+
+  public final class CarouselKt {
+  }
+
+}
+
+package androidx.tv.material.pager {
+
+  public final class PagerKt {
+  }
+
+}
+
diff --git a/tv/tv-material/build.gradle b/tv/tv-material/build.gradle
index 68c20e2..3e5244d 100644
--- a/tv/tv-material/build.gradle
+++ b/tv/tv-material/build.gradle
@@ -15,20 +15,48 @@
  */
 
 import androidx.build.LibraryType
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
+    id("AndroidXComposePlugin")
     id("com.android.library")
     id("org.jetbrains.kotlin.android")
 }
 
 dependencies {
     api(libs.kotlinStdlib)
-    // Add dependencies here
+
+    def composeVersion = '1.2.1'
+
+    api("androidx.annotation:annotation:1.4.0")
+    api("androidx.compose.animation:animation:$composeVersion")
+    api("androidx.compose.runtime:runtime:$composeVersion")
+
+    implementation(libs.kotlinStdlibCommon)
+    implementation("androidx.compose.foundation:foundation:$composeVersion")
+    implementation("androidx.compose.foundation:foundation-layout:$composeVersion")
+    implementation(project(":compose:ui:ui"))
+    implementation("androidx.compose.ui:ui-graphics:$composeVersion")
+    implementation("androidx.compose.ui:ui-text:$composeVersion")
+    implementation("androidx.compose.ui:ui-util:$composeVersion")
+    implementation("androidx.profileinstaller:profileinstaller:1.2.0")
+
+    androidTestImplementation(libs.truth)
+
+    androidTestImplementation(project(":compose:ui:ui-test"))
+    androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+    androidTestImplementation(project(":compose:test-utils"))
+    androidTestImplementation(libs.testRunner)
+
+    samples(projectOrArtifact(":tv:tv-material-samples"))
 }
 
 android {
     namespace "androidx.tv.material"
+    defaultConfig {
+        minSdkVersion 21
+    }
 }
 
 androidx {
@@ -37,4 +65,13 @@
     mavenGroup = LibraryGroups.TV
     inceptionYear = "2022"
     description = "build TV applications using controls that adhere to Material Design Language."
+    targetsJavaConsumers = false
+}
+
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs += [
+                "-Xjvm-default=all",
+        ]
+    }
 }
diff --git a/tv/tv-material/samples/build.gradle b/tv/tv-material/samples/build.gradle
new file mode 100644
index 0000000..f78e2c8
--- /dev/null
+++ b/tv/tv-material/samples/build.gradle
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.application")
+    id("AndroidXComposePlugin")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    implementation(project(":tv:tv-material"))
+    implementation(libs.kotlinStdlib)
+    implementation("androidx.leanback:leanback:1.0.0")
+    implementation(project(":activity:activity"))
+    implementation(project(":compose:material3:material3"))
+    implementation(project(":navigation:navigation-runtime"))
+    implementation("androidx.activity:activity-compose:1.5.1")
+    implementation(project(":tv:tv-foundation"))
+    implementation("androidx.appcompat:appcompat:1.6.0-alpha05")
+}
+
+androidx {
+    name = "TV-Compose Samples"
+    type = LibraryType.SAMPLES
+    mavenGroup = LibraryGroups.TV
+    inceptionYear = "2022"
+    description = "Contains the sample code for the APIs in the androidx.tv:tv-material library"
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 28
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled true
+            shrinkResources true
+            proguardFiles getDefaultProguardFile('proguard-android.txt')
+        }
+    }
+    namespace "androidx.tv.tvmaterial.samples"
+}
diff --git a/tv/tv-material/samples/src/main/AndroidManifest.xml b/tv/tv-material/samples/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..4c801cb
--- /dev/null
+++ b/tv/tv-material/samples/src/main/AndroidManifest.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2022 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <application
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:supportsRtl="true"
+        android:theme="@style/Theme.Androidx">
+        <activity
+            android:name=".MainActivity"
+            android:banner="@drawable/app_icon_your_company"
+            android:exported="true"
+            android:icon="@drawable/app_icon_your_company"
+            android:label="@string/app_name"
+            android:logo="@drawable/app_icon_your_company"
+            android:screenOrientation="landscape">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+    </application>
+
+    <uses-feature
+        android:name="android.software.leanback"
+        android:required="true" />
+    <uses-feature
+        android:name="android.hardware.touchscreen"
+        android:required="false" />
+
+    <uses-permission android:name="android.permission.INTERNET" />
+
+</manifest>
\ No newline at end of file
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
new file mode 100644
index 0000000..0a19b0c
--- /dev/null
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2022 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.tvmaterial.samples
+
+import androidx.compose.animation.animateColor
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.Card
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Color.Companion.Cyan
+import androidx.compose.ui.graphics.Color.Companion.Gray
+import androidx.compose.ui.graphics.Color.Companion.Red
+import androidx.compose.ui.graphics.Color.Companion.Yellow
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.tv.material.ExperimentalTvMaterialApi
+import androidx.tv.material.carousel.Carousel
+import androidx.tv.material.carousel.CarouselItem
+import androidx.tv.material.carousel.CarouselState
+
+@OptIn(ExperimentalTvMaterialApi::class)
+@Composable
+fun FeaturedCarousel() {
+    val carouselState = remember { CarouselState(0) }
+    LazyColumn {
+        item {
+            Carousel(
+                modifier = Modifier
+                    .height(400.dp)
+                    .width(950.dp),
+                carouselState = carouselState,
+                slideCount = 3
+            ) { SampleFrame(it) }
+        }
+
+        items(7) { SampleLazyRow() }
+    }
+}
+
+@OptIn(ExperimentalTvMaterialApi::class)
+@Composable
+fun SampleFrame(idx: Int) {
+    val item = mediaItems[idx]
+
+    CarouselItem(
+        background = {
+            Box(
+                Modifier.background(item.backgroundColor).fillMaxSize()
+            )
+        }) {
+        Box {
+            Column(modifier = Modifier.align(Alignment.BottomStart)) {
+                Text(
+                    text = item.title,
+                    style = MaterialTheme.typography.headlineSmall,
+                    color = Color.Black,
+                    fontWeight = FontWeight.Bold
+                )
+                Text(
+                    text = item.description,
+                    style = MaterialTheme.typography.bodyMedium,
+                    color = Color.Black,
+                    fontWeight = FontWeight.Normal,
+                    modifier = Modifier.padding(0.dp, 8.dp, 0.dp, 0.dp)
+                )
+
+                Row(modifier = Modifier.horizontalScroll(rememberScrollState())) {
+                    SampleButton(text = "PLAY")
+                    SampleButton(text = "INFO")
+                }
+            }
+        }
+    }
+}
+
+@Composable
+fun SampleButton(text: String) {
+    var cardScale
+        by remember { mutableStateOf(0.5f) }
+    val borderGlowColorTransition =
+        rememberInfiniteTransition()
+    var initialValue
+        by remember { mutableStateOf(Color.Transparent) }
+    val glowingColor
+        by borderGlowColorTransition.animateColor(
+            initialValue = initialValue,
+            targetValue = Color.Transparent,
+            animationSpec = infiniteRepeatable(
+                animation = tween(1000, easing = LinearEasing),
+                repeatMode = RepeatMode.Reverse
+            )
+        )
+
+    Button(
+        onClick = {},
+        modifier = Modifier
+            .scale(cardScale)
+            .border(
+                2.dp, glowingColor,
+                RoundedCornerShape(12.dp)
+            )
+            .onFocusChanged { focusState ->
+                if (focusState.isFocused) {
+                    cardScale = 1.0f
+                    initialValue = Color.White
+                } else {
+                    cardScale = 0.5f
+                    initialValue = Color.Transparent
+                }
+            }) {
+        Text(text = text)
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SampleLazyRow() {
+    LazyRow(
+        state = rememberLazyListState(),
+        contentPadding = PaddingValues(2.dp),
+        horizontalArrangement = Arrangement.spacedBy(4.dp),
+        modifier = Modifier
+            .fillMaxWidth()
+            .height(100.dp)) {
+        items((1..10).map { it.toString() }) {
+            var cardScale by remember { mutableStateOf(0.5f) }
+            val borderGlowColorTransition = rememberInfiniteTransition()
+            var initialValue by remember { mutableStateOf(Color.Transparent) }
+            val glowingColor by borderGlowColorTransition.animateColor(
+                initialValue = initialValue,
+                targetValue = Color.Transparent,
+                animationSpec = infiniteRepeatable(
+                    animation = tween(1000, easing = LinearEasing),
+                    repeatMode = RepeatMode.Reverse
+                )
+            )
+
+            Card(
+                modifier = Modifier
+                    .width(100.dp)
+                    .height(100.dp)
+                    .scale(cardScale)
+                    .border(2.dp, glowingColor, RoundedCornerShape(12.dp))
+                    .onFocusChanged { focusState ->
+                        if (focusState.isFocused) {
+                            cardScale = 1.0f
+                            initialValue = Color.White
+                        } else {
+                            cardScale = 0.5f
+                            initialValue = Color.Transparent
+                        }
+                    }
+                    .focusable()
+            ) {
+                Text(
+                    text = it,
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .height(100.dp)
+                        .padding(12.dp),
+                    color = Red,
+                    fontWeight = FontWeight.Bold
+
+                )
+            }
+        }
+    }
+}
+
+val mediaItems = listOf(
+    Media(id = "1", title = "Title 1", description = "Description 1", backgroundColor = Gray),
+    Media(id = "2", title = "Title 2", description = "Description 2", backgroundColor = Yellow),
+    Media(id = "3", title = "Title 3", description = "Description 3", backgroundColor = Cyan)
+)
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/MainActivity.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/MainActivity.kt
new file mode 100644
index 0000000..c329df0
--- /dev/null
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/MainActivity.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2022 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.tvmaterial.samples
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+
+class MainActivity : ComponentActivity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContent {
+            // A surface container using the 'background' color from the theme
+            Surface(color = MaterialTheme.colorScheme.background) {
+                FeaturedCarousel()
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/Media.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/Media.kt
new file mode 100644
index 0000000..dfb2685
--- /dev/null
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/Media.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 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.tvmaterial.samples
+
+import androidx.compose.ui.graphics.Color
+
+data class Media(
+    val id: String,
+    val title: String,
+    val description: String,
+    val backgroundColor: Color
+)
diff --git a/tv/tv-material/samples/src/main/res/drawable/app_icon_your_company.png b/tv/tv-material/samples/src/main/res/drawable/app_icon_your_company.png
new file mode 100644
index 0000000..0a47b01
--- /dev/null
+++ b/tv/tv-material/samples/src/main/res/drawable/app_icon_your_company.png
Binary files differ
diff --git a/tv/tv-material/samples/src/main/res/layout/activity_main.xml b/tv/tv-material/samples/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..2a1b45b
--- /dev/null
+++ b/tv/tv-material/samples/src/main/res/layout/activity_main.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2022 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.
+  -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/main_browse_fragment"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".MainActivity"
+    tools:deviceIds="tv"
+    tools:ignore="MergeRootFrame" />
\ No newline at end of file
diff --git a/tv/tv-material/samples/src/main/res/mipmap-hdpi/ic_launcher.webp b/tv/tv-material/samples/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
--- /dev/null
+++ b/tv/tv-material/samples/src/main/res/mipmap-hdpi/ic_launcher.webp
Binary files differ
diff --git a/tv/tv-material/samples/src/main/res/mipmap-mdpi/ic_launcher.webp b/tv/tv-material/samples/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
--- /dev/null
+++ b/tv/tv-material/samples/src/main/res/mipmap-mdpi/ic_launcher.webp
Binary files differ
diff --git a/tv/tv-material/samples/src/main/res/mipmap-xhdpi/ic_launcher.webp b/tv/tv-material/samples/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
--- /dev/null
+++ b/tv/tv-material/samples/src/main/res/mipmap-xhdpi/ic_launcher.webp
Binary files differ
diff --git a/tv/tv-material/samples/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/tv/tv-material/samples/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
--- /dev/null
+++ b/tv/tv-material/samples/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Binary files differ
diff --git a/tv/tv-material/samples/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/tv/tv-material/samples/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
--- /dev/null
+++ b/tv/tv-material/samples/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Binary files differ
diff --git a/tv/tv-material/samples/src/main/res/values/colors.xml b/tv/tv-material/samples/src/main/res/values/colors.xml
new file mode 100644
index 0000000..733f3f5
--- /dev/null
+++ b/tv/tv-material/samples/src/main/res/values/colors.xml
@@ -0,0 +1,8 @@
+<resources>
+    <color name="background_gradient_start">#000000</color>
+    <color name="background_gradient_end">#DDDDDD</color>
+    <color name="fastlane_background">#0096a6</color>
+    <color name="search_opaque">#ffaa3f</color>
+    <color name="selected_background">#ffaa3f</color>
+    <color name="default_background">#3d3d3d</color>
+</resources>
\ No newline at end of file
diff --git a/tv/tv-material/samples/src/main/res/values/strings.xml b/tv/tv-material/samples/src/main/res/values/strings.xml
new file mode 100644
index 0000000..2d22ec3
--- /dev/null
+++ b/tv/tv-material/samples/src/main/res/values/strings.xml
@@ -0,0 +1,35 @@
+<!--
+  Copyright 2022 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.
+  -->
+
+<resources>
+    <string name="app_name">Samples</string>
+    <string name="browse_title">Videos by Your Company</string>
+    <string name="related_movies">Related Videos</string>
+    <string name="grid_view">Grid View</string>
+    <string name="error_fragment">Error Fragment</string>
+    <string name="personal_settings">Personal Settings</string>
+    <string name="watch_trailer_1">Watch trailer</string>
+    <string name="watch_trailer_2">FREE</string>
+    <string name="rent_1">Rent By Day</string>
+    <string name="rent_2">From $1.99</string>
+    <string name="buy_1">Buy and Own</string>
+    <string name="buy_2">AT $9.99</string>
+    <string name="movie">Movie</string>
+
+    <!-- Error messages -->
+    <string name="error_fragment_message">An error occurred</string>
+    <string name="dismiss_error">Dismiss</string>
+</resources>
\ No newline at end of file
diff --git a/tv/tv-material/samples/src/main/res/values/themes.xml b/tv/tv-material/samples/src/main/res/values/themes.xml
new file mode 100644
index 0000000..402d3ea
--- /dev/null
+++ b/tv/tv-material/samples/src/main/res/values/themes.xml
@@ -0,0 +1,19 @@
+<!--
+  Copyright 2022 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.
+  -->
+
+<resources>
+    <style name="Theme.Androidx" parent="@style/Theme.AppCompat" />
+</resources>
\ No newline at end of file
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselItemTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselItemTest.kt
new file mode 100644
index 0000000..25d05ef
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselItemTest.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2022 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.material.carousel
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.tv.material.ExperimentalTvMaterialApi
+import org.junit.Rule
+import org.junit.Test
+
+class CarouselItemTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @OptIn(ExperimentalTvMaterialApi::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()
+    }
+}
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt
new file mode 100644
index 0000000..00d4043
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt
@@ -0,0 +1,484 @@
+/*
+ * Copyright 2022 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.material.carousel
+
+import androidx.compose.animation.animateColor
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.assertIsNotFocused
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.onParent
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.unit.dp
+import androidx.tv.material.ExperimentalTvMaterialApi
+import org.junit.Rule
+import org.junit.Test
+
+@OptIn(ExperimentalTvMaterialApi::class)
+class CarouselTest {
+
+    private val delayBetweenSlides = 2500L
+    private val animationTime = 900L
+    private val overlayRenderWaitTime = 1500L
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun carousel_autoScrolls() {
+
+        rule.setContent {
+            Content()
+        }
+
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(animationTime)
+        rule.onNodeWithText("Text 2").assertIsDisplayed()
+
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(animationTime)
+        rule.onNodeWithText("Text 3").assertIsDisplayed()
+    }
+
+    @Test
+    fun carousel_onFocus_stopsScroll() {
+
+        rule.setContent {
+            Content()
+        }
+
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+        rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
+
+        rule.onNodeWithText("Text 1")
+            .onParent()
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(animationTime)
+
+        rule.onNodeWithText("Text 2").assertDoesNotExist()
+        rule.onNodeWithText("Text 1").onParent().assertIsFocused()
+    }
+
+    @Test
+    fun carousel_onUserTriggeredPause_stopsScroll() {
+        var carouselState: CarouselState?
+        rule.setContent {
+            carouselState = remember { CarouselState() }
+            Content(
+                carouselState = carouselState!!,
+                content = {
+                    BasicText(text = "Text ${it + 1}")
+                    LaunchedEffect(carouselState) { carouselState?.pauseAutoScroll(it) }
+                })
+        }
+
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+        rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
+
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(animationTime)
+
+        rule.onNodeWithText("Text 2").assertDoesNotExist()
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+    }
+
+    @Test
+    fun carousel_onUserTriggeredPauseAndResume_resumeScroll() {
+        var carouselState: CarouselState?
+        var pauseHandle: ScrollPauseHandle? = null
+        rule.setContent {
+            carouselState = remember { CarouselState() }
+            Content(
+                carouselState = carouselState!!,
+                content = {
+                    BasicText(text = "Text ${it + 1}")
+                    LaunchedEffect(carouselState) {
+                        pauseHandle = carouselState?.pauseAutoScroll(it)
+                    }
+                })
+        }
+
+        rule.mainClock.autoAdvance = false
+
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+        rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
+
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(animationTime)
+
+        // pause handle has not been resumed, so Text 1 should still be on the screen.
+        rule.onNodeWithText("Text 2").assertDoesNotExist()
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+
+        rule.runOnIdle { pauseHandle?.resumeAutoScroll() }
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(animationTime)
+
+        // pause handle has been resumed, so Text 2 should be on the screen after
+        // delayBetweenSlides + animationTime
+        rule.onNodeWithText("Text 1").assertDoesNotExist()
+        rule.onNodeWithText("Text 2").assertIsDisplayed()
+    }
+
+    @Test
+    fun carousel_onMultipleUserTriggeredPauseAndResume_resumesScroll() {
+        var carouselState: CarouselState?
+        var pauseHandle1: ScrollPauseHandle? = null
+        var pauseHandle2: ScrollPauseHandle? = null
+        rule.setContent {
+            carouselState = remember { CarouselState() }
+            Content(
+                carouselState = carouselState!!,
+                content = {
+                    BasicText(text = "Text ${it + 1}")
+                    LaunchedEffect(carouselState) {
+                        if (pauseHandle1 == null) {
+                            pauseHandle1 = carouselState?.pauseAutoScroll(it)
+                        }
+                        if (pauseHandle2 == null) {
+                            pauseHandle2 = carouselState?.pauseAutoScroll(it)
+                        }
+                    }
+                })
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+        rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
+
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(animationTime)
+
+        // pause handles have not been resumed, so Text 1 should still be on the screen.
+        rule.onNodeWithText("Text 2").assertDoesNotExist()
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+
+        rule.runOnIdle { pauseHandle1?.resumeAutoScroll() }
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(animationTime)
+
+        // Second pause handle has not been resumed, so Text 1 should still be on the screen.
+        rule.onNodeWithText("Text 2").assertDoesNotExist()
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+
+        rule.runOnIdle { pauseHandle2?.resumeAutoScroll() }
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(animationTime)
+        // All pause handles have been resumed, so Text 2 should be on the screen after
+        // delayBetweenSlides + animationTime
+        rule.onNodeWithText("Text 1").assertDoesNotExist()
+        rule.onNodeWithText("Text 2").assertIsDisplayed()
+    }
+
+    @Test
+    fun carousel_onRepeatedResumesOnSamePauseHandle_ignoresSubsequentResumeCalls() {
+        var carouselState: CarouselState?
+        var pauseHandle1: ScrollPauseHandle? = null
+        var pauseHandle2: ScrollPauseHandle? = null
+        rule.setContent {
+            carouselState = remember { CarouselState() }
+            Content(
+                carouselState = carouselState!!,
+                content = {
+                    BasicText(text = "Text ${it + 1}")
+                    LaunchedEffect(carouselState) {
+                        if (pauseHandle1 == null) {
+                            pauseHandle1 = carouselState?.pauseAutoScroll(it)
+                        }
+                        if (pauseHandle2 == null) {
+                            pauseHandle2 = carouselState?.pauseAutoScroll(it)
+                        }
+                    }
+                })
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+        rule.onNodeWithText("Text 1").onParent().assertIsNotFocused()
+
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(animationTime)
+
+        // pause handles have not been resumed, so Text 1 should still be on the screen.
+        rule.onNodeWithText("Text 2").assertDoesNotExist()
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+
+        rule.runOnIdle { pauseHandle1?.resumeAutoScroll() }
+        // subsequent call to resume should be ignored
+        rule.runOnIdle { pauseHandle1?.resumeAutoScroll() }
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(animationTime)
+
+        // Second pause handle has not been resumed, so Text 1 should still be on the screen.
+        rule.onNodeWithText("Text 2").assertDoesNotExist()
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+    }
+
+    @Test
+    fun carousel_outOfFocus_resumesScroll() {
+        rule.setContent {
+            Content()
+        }
+
+        rule.onNodeWithText("Text 1")
+            .onParent()
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+
+        rule.onNodeWithText("Card").performSemanticsAction(SemanticsActions.RequestFocus)
+        rule.onNodeWithText("Card").assertIsFocused()
+
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(animationTime)
+        rule.onNodeWithText("Text 1").assertDoesNotExist()
+        rule.onNodeWithText("Text 2").assertIsDisplayed()
+    }
+
+    @Test
+    fun carousel_pagerIndicatorDisplayed() {
+        rule.setContent {
+            Content()
+        }
+
+        rule.onNodeWithTag("indicator").assertIsDisplayed()
+    }
+
+    @Test
+    fun carousel_withAnimatedContent_successfulTransition() {
+        rule.setContent {
+            AnimatedContent()
+        }
+
+        rule.onNodeWithText("Text 1").assertDoesNotExist()
+
+        rule.mainClock.advanceTimeBy(overlayRenderWaitTime + animationTime, true)
+        rule.mainClock.advanceTimeByFrame()
+
+        rule.onNodeWithText("Text 1").assertIsDisplayed()
+        rule.onNodeWithText("PLAY").assertIsDisplayed()
+    }
+
+    @Test
+    fun carousel_withAnimatedContent_successfulFocusIn() {
+        rule.setContent {
+            AnimatedContent()
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag("pager")
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+
+        // current slide overlay render delay
+        rule.mainClock.advanceTimeBy(animationTime + overlayRenderWaitTime, false)
+        rule.mainClock.advanceTimeBy(animationTime, false)
+        rule.mainClock.advanceTimeByFrame()
+
+        rule.onNodeWithText("PLAY").assertIsDisplayed()
+        rule.onNodeWithText("PLAY").assertIsFocused()
+    }
+
+    @Composable
+    fun Content(
+        carouselState: CarouselState = remember { CarouselState() },
+        content: @Composable (index: Int) -> Unit = { BasicText(text = "Text ${it + 1}")
+        }
+    ) {
+        val slideCount = 3
+        LazyColumn {
+            item {
+                Carousel(
+                    modifier = Modifier.fillMaxSize(),
+                    carouselState = carouselState,
+                    slideCount = slideCount,
+                    timeToDisplaySlideMillis = delayBetweenSlides,
+                    carouselIndicator = {
+                        CarouselDefaults.Indicator(modifier = Modifier
+                            .align(Alignment.BottomEnd)
+                            .padding(16.dp)
+                            .testTag("indicator"),
+                            carouselState = carouselState,
+                            slideCount = slideCount)
+                    },
+                    content = content
+                )
+            }
+            item {
+                Box(modifier = Modifier.focusable()
+                ) {
+                    BasicText(
+                        text = "Card",
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .height(100.dp)
+                            .padding(12.dp)
+                            .focusable()
+                    )
+                }
+            }
+        }
+    }
+
+    @Composable
+    fun AnimatedContent(carouselState: CarouselState = remember { CarouselState() }) {
+        LazyColumn {
+            item {
+                Carousel(
+                    modifier = Modifier
+                        .fillMaxSize()
+                        .testTag("pager"),
+                    slideCount = 3,
+                    timeToDisplaySlideMillis = delayBetweenSlides,
+                    carouselState = carouselState
+                ) { Frame(text = "Text ${it + 1}") }
+            }
+            item {
+                Box(modifier = Modifier.focusable()
+                ) {
+                    BasicText(
+                        text = "Card",
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .height(100.dp)
+                            .padding(12.dp)
+                            .focusable()
+                    )
+                }
+            }
+        }
+    }
+
+    @Composable
+    fun Frame(text: String) {
+        val focusRequester = FocusRequester()
+        CarouselItem(
+            overlayEnterTransitionStartDelayMillis = overlayRenderWaitTime,
+            background = {}) {
+            Column(modifier = Modifier
+                .onFocusChanged {
+                    if (it.isFocused) {
+                        focusRequester.requestFocus()
+                    }
+                }
+                .focusable()) {
+                BasicText(text = text)
+                Row(modifier = Modifier
+                    .horizontalScroll(rememberScrollState())
+                    .onFocusChanged {
+                        if (it.isFocused) {
+                            focusRequester.requestFocus()
+                        }
+                    }
+                    .focusable()) {
+                    TestButton(text = "PLAY", focusRequester)
+                }
+            }
+        }
+    }
+
+    @Composable
+    fun TestButton(text: String, focusRequester: FocusRequester? = null) {
+        var cardScale
+            by remember { mutableStateOf(0.5f) }
+        val borderGlowColorTransition =
+            rememberInfiniteTransition()
+        var initialValue
+            by remember { mutableStateOf(Color.Transparent) }
+        val glowingColor
+            by borderGlowColorTransition.animateColor(
+                initialValue = initialValue,
+                targetValue = Color.Transparent,
+                animationSpec = infiniteRepeatable(
+                    animation = tween(1000,
+                        easing = LinearEasing),
+                    repeatMode = RepeatMode.Reverse
+                )
+            )
+
+        val baseModifier =
+            if (focusRequester == null) Modifier else Modifier.focusRequester(focusRequester)
+
+        Box(
+            modifier = baseModifier
+                .scale(cardScale)
+                .border(
+                    2.dp, glowingColor,
+                    RoundedCornerShape(12.dp)
+                )
+                .onFocusChanged { focusState ->
+                    if (focusState.isFocused) {
+                        cardScale = 1.0f
+                        initialValue = Color.White
+                    } else {
+                        cardScale = 0.5f
+                        initialValue = Color.Transparent
+                    }
+                }
+                .clickable(onClick = {})) {
+            BasicText(text = text)
+        }
+    }
+
+    @Test
+    fun carousel_zeroSlideCount_drawsSomething() {
+        val testTag = "emptyCarousel"
+        rule.setContent {
+            Carousel(slideCount = 0, modifier = Modifier.testTag(testTag)) {}
+        }
+
+        rule.onNodeWithTag(testTag).assertExists()
+    }
+}
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material/pager/PagerTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material/pager/PagerTest.kt
new file mode 100644
index 0000000..23df054
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material/pager/PagerTest.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2022 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.material.pager
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.tv.material.ExperimentalTvMaterialApi
+import org.junit.Rule
+import org.junit.Test
+
+class PagerTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @OptIn(ExperimentalTvMaterialApi::class)
+    @Test
+    fun pager_zeroSlideCount_drawsSomething() {
+        val testTag = "pager"
+        rule.setContent {
+            Pager(slideCount = 0, modifier = Modifier.testTag(testTag)) {}
+        }
+
+        rule.onNodeWithTag(testTag).assertExists()
+    }
+}
\ No newline at end of file
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/ExperimentalTvMaterialApi.kt b/tv/tv-material/src/main/java/androidx/tv/material/ExperimentalTvMaterialApi.kt
new file mode 100644
index 0000000..b0e19e2
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material/ExperimentalTvMaterialApi.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2022 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.material
+
+@RequiresOptIn(
+    "This tv-material API is experimental and likely to change or be removed in the future."
+)
+annotation class ExperimentalTvMaterialApi
\ No newline at end of file
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt b/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
new file mode 100644
index 0000000..ebd71f6
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
@@ -0,0 +1,291 @@
+/*
+ * Copyright 2022 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.material.carousel
+
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+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.graphics.Color
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.unit.dp
+import androidx.tv.material.ExperimentalTvMaterialApi
+import androidx.tv.material.pager.Pager
+import java.lang.Math.floorMod
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.yield
+
+/**
+ * Composes a hero card rotator to highlight a piece of content.
+ *
+ * @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
+ * the next slide.
+ * @param enterTransition transition used to bring a slide into view.
+ * @param exitTransition transition used to remove a slide from view.
+ * @param carouselIndicator indicator showing the position of the current slide among all slides.
+ * @param content defines the slides for a given index.
+ */
+
+@Suppress("IllegalExperimentalApiUsage")
+@OptIn(ExperimentalComposeUiApi::class)
+@ExperimentalTvMaterialApi
+@Composable
+fun Carousel(
+    slideCount: Int,
+    modifier: Modifier = Modifier,
+    carouselState: CarouselState = remember { CarouselState() },
+    timeToDisplaySlideMillis: Long = CarouselDefaults.TimeToDisplaySlideMillis,
+    enterTransition: EnterTransition = CarouselDefaults.EnterTransition,
+    exitTransition: ExitTransition = CarouselDefaults.ExitTransition,
+    carouselIndicator:
+    @Composable BoxScope.() -> Unit = {
+        CarouselDefaults.Indicator(
+            modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
+            carouselState = carouselState,
+            slideCount = slideCount)
+    },
+    content: @Composable (index: Int) -> Unit
+) {
+    CarouselStateUpdater(carouselState, slideCount)
+    var focusState: FocusState? by remember { mutableStateOf(null) }
+    val focusManager = LocalFocusManager.current
+
+    AutoScrollSideEffect(
+        timeToDisplaySlideMillis,
+        slideCount,
+        carouselState,
+        focusState)
+    Box(modifier = modifier
+        .onFocusChanged {
+            focusState = it
+            if (it.isFocused) {
+                focusManager.moveFocus(FocusDirection.Enter)
+            }
+        }
+        .focusable()) {
+        Pager(
+            enterTransition = enterTransition,
+            exitTransition = exitTransition,
+            currentSlide = carouselState.slideIndex,
+            slideCount = slideCount
+        ) { content.invoke(it) }
+
+        this.carouselIndicator()
+    }
+}
+
+@OptIn(ExperimentalTvMaterialApi::class)
+@Composable
+private fun AutoScrollSideEffect(
+    timeToDisplaySlideMillis: Long,
+    slideCount: Int,
+    carouselState: CarouselState,
+    focusState: FocusState?
+) {
+    val currentTimeToDisplaySlideMillis by rememberUpdatedState(timeToDisplaySlideMillis)
+    val currentSlideCount by rememberUpdatedState(slideCount)
+    val carouselIsFocused = focusState?.isFocused ?: false
+    val carouselHasFocus = focusState?.hasFocus ?: false
+
+    if (!(carouselIsFocused || carouselHasFocus)) {
+        LaunchedEffect(carouselState) {
+            while (true) {
+                yield()
+                delay(currentTimeToDisplaySlideMillis)
+                if (carouselState.activePauseHandlesCount > 0) {
+                    snapshotFlow { carouselState.activePauseHandlesCount }
+                        .first { pauseHandleCount -> pauseHandleCount == 0 }
+                }
+                carouselState.nextSlide(currentSlideCount)
+            }
+        }
+    }
+}
+
+@OptIn(ExperimentalTvMaterialApi::class)
+@Composable
+private fun CarouselStateUpdater(carouselState: CarouselState, slideCount: Int) {
+    LaunchedEffect(carouselState, slideCount) {
+        if (slideCount != 0) {
+            carouselState.slideIndex = floorMod(carouselState.slideIndex, slideCount)
+        }
+    }
+}
+
+/**
+ * State of the Carousel which allows the user to specify the first slide that is shown when the
+ * Carousel is instantiated in the constructor.
+ *
+ * It also provides the user with support to pause and resume the auto-scroll behaviour of the
+ * Carousel.
+ * @param initialSlideIndex the index of the first slide that is displayed.
+ */
+@Stable
+@ExperimentalTvMaterialApi
+class CarouselState(initialSlideIndex: Int = 0) {
+    internal var activePauseHandlesCount by mutableStateOf(0)
+
+    /**
+     * The index of the slide that is currently displayed by the carousel
+     */
+    var slideIndex by mutableStateOf(initialSlideIndex)
+        internal 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
+     */
+    fun pauseAutoScroll(slideIndex: Int): ScrollPauseHandle {
+        if (this.slideIndex != slideIndex) {
+            return NoOpScrollPauseHandle
+        }
+        return ScrollPauseHandleImpl(this)
+    }
+
+    internal fun nextSlide(slideCount: Int) {
+        if (slideCount != 0) {
+            slideIndex = floorMod(slideIndex + 1, slideCount)
+        }
+    }
+}
+
+@ExperimentalTvMaterialApi
+/**
+ * Handle returned by [CarouselState.pauseAutoScroll] that can be used to resume auto-scroll.
+ */
+sealed interface ScrollPauseHandle {
+    /**
+     * Resumes the auto-scroll behaviour if there are no other active [ScrollPauseHandle]s.
+     */
+    fun resumeAutoScroll()
+}
+
+@OptIn(ExperimentalTvMaterialApi::class)
+internal object NoOpScrollPauseHandle : ScrollPauseHandle {
+    /**
+     * Resumes the auto-scroll behaviour if there are no other active [ScrollPauseHandle]s.
+     */
+    override fun resumeAutoScroll() {}
+}
+
+@OptIn(ExperimentalTvMaterialApi::class)
+internal class ScrollPauseHandleImpl(private val carouselState: CarouselState) : ScrollPauseHandle {
+    private var active by mutableStateOf(true)
+    init {
+        carouselState.activePauseHandlesCount += 1
+    }
+    /**
+     * Resumes the auto-scroll behaviour if there are no other active [ScrollPauseHandle]s.
+     */
+    override fun resumeAutoScroll() {
+        if (active) {
+            active = false
+            carouselState.activePauseHandlesCount -= 1
+        }
+    }
+}
+
+@ExperimentalTvMaterialApi
+object CarouselDefaults {
+    /**
+     * Default time for which the slide is visible to the user.
+     */
+    val TimeToDisplaySlideMillis: Long = 5000
+
+    /**
+     * Default transition used to bring the slide into view
+     */
+    val EnterTransition: EnterTransition = fadeIn(animationSpec = tween(900))
+
+    /**
+     * Default transition used to remove the slide from view
+     */
+    val ExitTransition: ExitTransition = fadeOut(animationSpec = tween(900))
+
+    /**
+     * An indicator showing the position of the current slide among the slides of the carousel.
+     *
+     * @param carouselState is the state associated with the carousel of which this indicator is a
+     * part.
+     * @param slideCount total number of slides in the carousel
+     */
+    @ExperimentalTvMaterialApi
+    @Composable
+    fun Indicator(
+        carouselState: CarouselState,
+        slideCount: Int,
+        modifier: Modifier = Modifier
+    ) {
+        if (slideCount <= 0) {
+            Box(modifier = modifier)
+        } else {
+            val defaultSize = 8.dp
+            val inactiveColor = Color.LightGray
+            val activeColor = Color.White
+            val shape = CircleShape
+            val indicatorModifier = Modifier.size(defaultSize)
+
+            Box(modifier = modifier) {
+                Row(
+                    horizontalArrangement = Arrangement.spacedBy(defaultSize),
+                    verticalAlignment = Alignment.CenterVertically,
+                ) {
+                    repeat(slideCount) {
+                        Box(indicatorModifier.background(
+                            color =
+                              if (it == carouselState.slideIndex) {
+                                  activeColor
+                              } else {
+                                  inactiveColor
+                              },
+                            shape = shape
+                        ))
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt b/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt
new file mode 100644
index 0000000..0a58e20
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2022 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.material.carousel
+
+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.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.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.platform.LocalFocusManager
+import androidx.tv.material.ExperimentalTvMaterialApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.first
+
+/**
+ * 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].
+ *
+ * @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.
+ */
+@Suppress("IllegalExperimentalApiUsage")
+@OptIn(ExperimentalComposeUiApi::class)
+@ExperimentalTvMaterialApi
+@Composable
+fun CarouselItem(
+    background: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    overlayEnterTransitionStartDelayMillis: Long =
+        CarouselItemDefaults.OverlayEnterTransitionStartDelayMillis,
+    overlayEnterTransition: EnterTransition = CarouselItemDefaults.OverlayEnterTransition,
+    overlayExitTransition: ExitTransition = CarouselItemDefaults.OverlayExitTransition,
+    overlay: @Composable () -> Unit
+) {
+    val overlayVisible = remember { MutableTransitionState(initialState = false) }
+    var focusState: FocusState? by remember { mutableStateOf(null) }
+    val focusManager = LocalFocusManager.current
+
+    LaunchedEffect(overlayVisible) {
+        snapshotFlow { overlayVisible.isIdle && overlayVisible.currentState }.first { it }
+        // slide has loaded completely.
+        if (focusState?.isFocused == true) {
+	    focusManager.moveFocus(FocusDirection.Enter)
+        }
+    }
+
+    Box(modifier = modifier
+            .onFocusChanged {
+                focusState = it
+                if (it.isFocused && overlayVisible.isIdle && overlayVisible.currentState) {
+                    focusManager.moveFocus(FocusDirection.Enter)
+                }
+             }.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)
+                .onFocusChanged {
+                    if (it.isFocused) { focusManager.moveFocus(FocusDirection.Enter) }
+                }
+                .focusable(),
+            visibleState = overlayVisible,
+            enter = overlayEnterTransition,
+            exit = overlayExitTransition
+        ) {
+            overlay.invoke()
+        }
+    }
+}
+
+@ExperimentalTvMaterialApi
+object CarouselItemDefaults {
+    /**
+     * Default delay between the background being rendered and the overlay being rendered.
+     */
+    val OverlayEnterTransitionStartDelayMillis: Long = 1500
+
+    /**
+     * Default transition to bring the overlay into view.
+     */
+    val OverlayEnterTransition: EnterTransition = slideInHorizontally(initialOffsetX = { it * 4 })
+
+    /**
+     * Default transition to remove overlay from view.
+     */
+    val OverlayExitTransition: ExitTransition = slideOutHorizontally()
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/pager/Pager.kt b/tv/tv-material/src/main/java/androidx/tv/material/pager/Pager.kt
new file mode 100644
index 0000000..24f88ed
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material/pager/Pager.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2022 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.material.pager
+
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.with
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.tv.material.ExperimentalTvMaterialApi
+
+/**
+ * Composable that accepts a lambda that generates the slides based on the index provided and
+ * displays the slide associated with the index [currentSlide].
+ *
+ * @param modifier the modifier to apply to this component.
+ * @param enterTransition defines how the slide is animated into view.
+ * @param exitTransition defines how the slide is animated out of view.
+ * @param currentSlide the slide that is currently displayed by the pager.
+ * @param slideCount the total number of slides.
+ * @param content defines the slide composable for a given index.
+ */
+@Suppress("IllegalExperimentalApiUsage")
+@OptIn(ExperimentalAnimationApi::class)
+@ExperimentalTvMaterialApi
+@Composable
+internal fun Pager(
+    slideCount: Int,
+    modifier: Modifier = Modifier,
+    enterTransition: EnterTransition = PagerDefaults.EnterTransition,
+    exitTransition: ExitTransition = PagerDefaults.ExitTransition,
+    currentSlide: Int = 0,
+    content: @Composable (index: Int) -> Unit
+) {
+    if (slideCount <= 0) {
+        Box(modifier)
+    } else {
+        AnimatedContent(
+            modifier = modifier,
+            targetState = currentSlide.coerceIn(0, slideCount - 1),
+            transitionSpec = { enterTransition.with(exitTransition) }
+        ) {
+            content(it)
+        }
+    }
+}
+
+@ExperimentalTvMaterialApi
+private object PagerDefaults {
+    /**
+     * Default transition used to bring a slide into view
+     */
+    val EnterTransition: EnterTransition = fadeIn(animationSpec = tween(900))
+
+    /**
+     * Default transition used to remove a slide from view
+     */
+    val ExitTransition: ExitTransition = fadeOut(animationSpec = tween(900))
+}