Merge "[Carousel] Add maskClip and maskBorder modifiers to CarouselItemScope" into androidx-main
diff --git a/compose/material3/material3/api/1.3.0-beta01.txt b/compose/material3/material3/api/1.3.0-beta01.txt
index ca9302d..4f91e60 100644
--- a/compose/material3/material3/api/1.3.0-beta01.txt
+++ b/compose/material3/material3/api/1.3.0-beta01.txt
@@ -2168,6 +2168,9 @@
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public sealed interface CarouselItemScope {
     method public androidx.compose.material3.carousel.CarouselItemInfo getCarouselItemInfo();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.Modifier maskBorder(androidx.compose.ui.Modifier, androidx.compose.foundation.BorderStroke border, androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.Modifier maskClip(androidx.compose.ui.Modifier, androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Composable public androidx.compose.foundation.shape.GenericShape rememberMaskShape(androidx.compose.ui.graphics.Shape shape);
     property public abstract androidx.compose.material3.carousel.CarouselItemInfo carouselItemInfo;
   }
 
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index ca9302d..4f91e60 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -2168,6 +2168,9 @@
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public sealed interface CarouselItemScope {
     method public androidx.compose.material3.carousel.CarouselItemInfo getCarouselItemInfo();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.Modifier maskBorder(androidx.compose.ui.Modifier, androidx.compose.foundation.BorderStroke border, androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.Modifier maskClip(androidx.compose.ui.Modifier, androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Composable public androidx.compose.foundation.shape.GenericShape rememberMaskShape(androidx.compose.ui.graphics.Shape shape);
     property public abstract androidx.compose.material3.carousel.CarouselItemInfo carouselItemInfo;
   }
 
diff --git a/compose/material3/material3/api/restricted_1.3.0-beta01.txt b/compose/material3/material3/api/restricted_1.3.0-beta01.txt
index ca9302d..4f91e60 100644
--- a/compose/material3/material3/api/restricted_1.3.0-beta01.txt
+++ b/compose/material3/material3/api/restricted_1.3.0-beta01.txt
@@ -2168,6 +2168,9 @@
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public sealed interface CarouselItemScope {
     method public androidx.compose.material3.carousel.CarouselItemInfo getCarouselItemInfo();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.Modifier maskBorder(androidx.compose.ui.Modifier, androidx.compose.foundation.BorderStroke border, androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.Modifier maskClip(androidx.compose.ui.Modifier, androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Composable public androidx.compose.foundation.shape.GenericShape rememberMaskShape(androidx.compose.ui.graphics.Shape shape);
     property public abstract androidx.compose.material3.carousel.CarouselItemInfo carouselItemInfo;
   }
 
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index ca9302d..4f91e60 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -2168,6 +2168,9 @@
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public sealed interface CarouselItemScope {
     method public androidx.compose.material3.carousel.CarouselItemInfo getCarouselItemInfo();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.Modifier maskBorder(androidx.compose.ui.Modifier, androidx.compose.foundation.BorderStroke border, androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.Modifier maskClip(androidx.compose.ui.Modifier, androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Composable public androidx.compose.foundation.shape.GenericShape rememberMaskShape(androidx.compose.ui.graphics.Shape shape);
     property public abstract androidx.compose.material3.carousel.CarouselItemInfo carouselItemInfo;
   }
 
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt
index cf25fa1..f142563 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt
@@ -19,26 +19,50 @@
 import androidx.annotation.DrawableRes
 import androidx.annotation.Sampled
 import androidx.annotation.StringRes
+import androidx.compose.foundation.BorderStroke
 import androidx.compose.foundation.Image
+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.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
-import androidx.compose.material3.Card
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Image
+import androidx.compose.material3.AssistChipDefaults
+import androidx.compose.material3.ElevatedAssistChip
 import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.material3.carousel.HorizontalMultiBrowseCarousel
 import androidx.compose.material3.carousel.HorizontalUncontainedCarousel
 import androidx.compose.material3.carousel.rememberCarouselState
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.RoundRect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Outline
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.lerp
+import kotlin.math.max
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Preview
@@ -70,16 +94,12 @@
         contentPadding = PaddingValues(horizontal = 16.dp)
     ) { i ->
         val item = items[i]
-        Card(
-            modifier = Modifier.height(205.dp)
-        ) {
-            Image(
-                painter = painterResource(id = item.imageResId),
-                contentDescription = stringResource(item.contentDescriptionResId),
-                modifier = Modifier.fillMaxSize(),
-                contentScale = ContentScale.Crop
-            )
-        }
+        Image(
+            modifier = Modifier.height(205.dp).maskClip(MaterialTheme.shapes.extraLarge),
+            painter = painterResource(id = item.imageResId),
+            contentDescription = stringResource(item.contentDescriptionResId),
+            contentScale = ContentScale.Crop
+        )
     }
 }
 
@@ -112,17 +132,12 @@
         contentPadding = PaddingValues(horizontal = 16.dp)
     ) { i ->
         val item = items[i]
-        Card(
-            modifier = Modifier
-                .height(205.dp)
-        ) {
-            Image(
-                painter = painterResource(id = item.imageResId),
-                contentDescription = stringResource(item.contentDescriptionResId),
-                modifier = Modifier.fillMaxSize(),
-                contentScale = ContentScale.Crop
-            )
-        }
+        Image(
+            modifier = Modifier.height(205.dp).maskClip(MaterialTheme.shapes.extraLarge),
+            painter = painterResource(id = item.imageResId),
+            contentDescription = stringResource(item.contentDescriptionResId),
+            contentScale = ContentScale.Crop
+        )
     }
 }
 
@@ -151,20 +166,67 @@
         modifier = Modifier
             .width(412.dp)
             .height(221.dp),
-        preferredItemWidth = 130.dp,
+        preferredItemWidth = 186.dp,
         itemSpacing = 8.dp,
         contentPadding = PaddingValues(horizontal = 16.dp)
     ) { i ->
         val item = items[i]
-        Card(
-            modifier = Modifier
-                .height(205.dp)
-        ) {
+        // For item 1 and 4, create a stacked item layout that clips two images independently
+        // to the item's mask
+        if (i == 1 || i == 4) {
+            Column(
+                modifier = Modifier.height(205.dp),
+                verticalArrangement = Arrangement.spacedBy(8.dp)
+            ) {
+                Image(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .fillMaxHeight(.5f)
+                        .maskClip(MaterialTheme.shapes.extraLarge)
+                        .maskBorder(
+                            BorderStroke(3.dp, Color.Magenta),
+                            MaterialTheme.shapes.extraLarge
+                        ),
+                    painter = painterResource(id = item.imageResId),
+                    contentDescription = stringResource(item.contentDescriptionResId),
+                    contentScale = ContentScale.Crop
+                )
+                Image(
+                    modifier = Modifier
+                        .fillMaxSize()
+                        .maskClip(RoundedCornerShape(8.dp))
+                        .maskBorder(
+                            BorderStroke(5.dp, Color.Green),
+                            RoundedCornerShape(8.dp)
+                        ),
+                    painter = painterResource(id = item.imageResId),
+                    contentDescription = stringResource(item.contentDescriptionResId),
+                    contentScale = ContentScale.Crop
+                )
+            }
+        } else {
+            // Mask using a generic path shape
+            val pathShape = remember {
+                object : Shape {
+                    override fun createOutline(
+                        size: Size,
+                        layoutDirection: LayoutDirection,
+                        density: Density
+                    ): Outline {
+                        val roundRect =
+                            RoundRect(0f, 0f, size.width, size.height, CornerRadius(30f))
+                        val shapePath = Path().apply {
+                            addRoundRect(roundRect)
+                        }
+                        return Outline.Generic(shapePath)
+                    }
+                }
+            }
             Box(
                 modifier = Modifier
-                    .graphicsLayer {
-                        alpha = carouselItemInfo.size / carouselItemInfo.maxSize
-                    }
+                    .height(205.dp)
+                    .maskClip(pathShape)
+                    .maskBorder(BorderStroke(5.dp, Color.Red), pathShape),
             ) {
                 Image(
                     painter = painterResource(id = item.imageResId),
@@ -172,10 +234,29 @@
                     modifier = Modifier.fillMaxSize(),
                     contentScale = ContentScale.Crop
                 )
-                Text(
-                    text = "sample text",
+                ElevatedAssistChip(
+                    onClick = { /* Do something! */ },
+                    label = { Text("Image $i") },
                     modifier = Modifier.graphicsLayer {
-                        translationX = carouselItemInfo.maskRect.left
+                        // Fade the chip in once the carousel item's size is large enough to
+                        // display the entire chip
+                        alpha = lerp(
+                            0f,
+                            1f,
+                            max(
+                                size.width - (carouselItemInfo.maxSize) + carouselItemInfo.size,
+                                0f
+                            ) / size.width
+                        )
+                        // Translate the chip to be pinned to the left side of the item's mask
+                        translationX = carouselItemInfo.maskRect.left + 8.dp.toPx()
+                    },
+                    leadingIcon = {
+                        Icon(
+                            Icons.Filled.Image,
+                            contentDescription = "Localized description",
+                            Modifier.size(AssistChipDefaults.IconSize)
+                        )
                     }
                 )
             }
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselItemScopeTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselItemScopeTest.kt
new file mode 100644
index 0000000..26faa0f
--- /dev/null
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselItemScopeTest.kt
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.carousel
+
+import android.os.Build
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.GenericShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.GOLDEN_MATERIAL3
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.material3.setMaterialContent
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpRect
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@OptIn(ExperimentalMaterial3Api::class)
+class CarouselItemScopeTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @get:Rule
+    val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
+
+    val testTag = "ItemTag"
+
+    @Test
+    fun mask_fullyUnmaskedShouldMatchSize() {
+        rule.setMaterialContent(lightColorScheme()) {
+            val scope = createCarouselItemScope(
+                size = 100.dp,
+                minSize = 10.dp,
+                maxSize = 100.dp,
+                maskRect = DpRect(0.dp, 0.dp, 100.dp, 100.dp)
+            )
+            with(scope) {
+                Box(modifier = Modifier
+                    .testTag(testTag)
+                    .size(100.dp)
+                    .maskClip(shape = RoundedCornerShape(28.dp))
+                    .background(Color.Red)
+                )
+            }
+        }
+
+        assertCarouselAgainstGolden("mask_fullyUnmaskedShouldMatchSize")
+    }
+
+    @Test
+    fun mask_halfMaksedShouldIntersectSize() {
+        rule.setMaterialContent(lightColorScheme()) {
+            val scope = createCarouselItemScope(
+                size = 50.dp,
+                minSize = 10.dp,
+                maxSize = 100.dp,
+                maskRect = DpRect(25.dp, 25.dp, 75.dp, 75.dp)
+            )
+            with(scope) {
+                Box(modifier = Modifier
+                    .testTag(testTag)
+                    .size(100.dp)
+                    .maskClip(shape = RoundedCornerShape(10.dp))
+                    .background(Color.Red)
+                )
+            }
+        }
+
+        assertCarouselAgainstGolden("mask_halfMaskedShouldIntersectSize")
+    }
+
+    @Test
+    fun mask_genericMaskedPathShouldIntersectSize() {
+        val ovalPathShape = GenericShape { size, _ ->
+            addOval(Rect(0f, 0f, size.width, size.height))
+        }
+        rule.setMaterialContent(lightColorScheme()) {
+            val scope = createCarouselItemScope(
+                size = 50.dp,
+                minSize = 10.dp,
+                maxSize = 100.dp,
+                maskRect = DpRect(25.dp, 0.dp, 75.dp, 100.dp)
+            )
+            with(scope) {
+                Box(modifier = Modifier
+                    .testTag(testTag)
+                    .size(100.dp)
+                    .maskClip(shape = ovalPathShape)
+                    .background(Color.Red)
+                )
+            }
+        }
+
+        assertCarouselAgainstGolden("mask_genericMaskedPathShouldIntersectSize")
+    }
+
+    @Test
+    fun mask_squareMaskShouldIntersectSize() {
+        rule.setMaterialContent(lightColorScheme()) {
+            val scope = createCarouselItemScope(
+                size = 50.dp,
+                minSize = 10.dp,
+                maxSize = 100.dp,
+                maskRect = DpRect(25.dp, 0.dp, 75.dp, 100.dp)
+            )
+            with(scope) {
+                Box(modifier = Modifier
+                    .testTag(testTag)
+                    .size(100.dp)
+                    .maskClip(shape = RoundedCornerShape(0.dp))
+                    .background(Color.Red)
+                )
+            }
+        }
+
+        assertCarouselAgainstGolden("mask_squareMaskShouldIntersectSize")
+    }
+
+    @Test
+    fun maskBorder_fullyUnmaskedShouldMatchSize() {
+        rule.setMaterialContent(lightColorScheme()) {
+            val scope = createCarouselItemScope(
+                size = 100.dp,
+                minSize = 10.dp,
+                maxSize = 100.dp,
+                maskRect = DpRect(0.dp, 0.dp, 100.dp, 100.dp)
+            )
+            with(scope) {
+                Box(modifier = Modifier
+                    .testTag(testTag)
+                    .size(100.dp)
+                    .maskClip(shape = RoundedCornerShape(10.dp))
+                    .maskBorder(
+                        border = BorderStroke(5.dp, Color.Blue),
+                        shape = RoundedCornerShape(10.dp)
+                    )
+                    .background(Color.Red)
+                )
+            }
+        }
+
+        assertCarouselAgainstGolden("maskBorder_fullyUnmaskedShouldMatchSize")
+    }
+
+    @Test
+    fun maskBorder_triangleMaskShouldIntersectSize() {
+        val triangle = GenericShape { size, _ ->
+            moveTo(size.width / 2f, 0f)
+            lineTo(size.width, size.height)
+            lineTo(0f, size.height)
+            close()
+        }
+        rule.setMaterialContent(lightColorScheme()) {
+            val scope = createCarouselItemScope(
+                size = 100.dp,
+                minSize = 10.dp,
+                maxSize = 100.dp,
+                maskRect = DpRect(25.dp, 25.dp, 75.dp, 75.dp)
+            )
+            with(scope) {
+                Box(modifier = Modifier
+                    .testTag(testTag)
+                    .size(100.dp)
+                    .maskClip(shape = triangle)
+                    .maskBorder(
+                        border = BorderStroke(5.dp, Color.Blue),
+                        shape = triangle
+                    )
+                    .background(Color.Red)
+                )
+            }
+        }
+
+        assertCarouselAgainstGolden("maskBorder_triangleMaskShouldIntersectSize")
+    }
+
+    private fun createCarouselItemScope(
+        size: Dp,
+        minSize: Dp,
+        maxSize: Dp,
+        maskRect: DpRect
+    ): CarouselItemScope {
+        return CarouselItemScopeImpl(CarouselItemInfoImpl().apply {
+
+            with(rule.density) {
+                sizeState = size.toPx()
+                minSizeState = minSize.toPx()
+                maxSizeState = maxSize.toPx()
+                maskRectState = maskRect.toRect()
+            }
+        })
+    }
+
+    private fun assertCarouselAgainstGolden(goldenIdentifier: String) {
+        rule
+            .onNodeWithTag(testTag)
+            .captureToImage()
+            .assertAgainstGolden(
+                screenshotRule,
+                "carousel_$goldenIdentifier"
+            )
+    }
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
index dfbc26f..2a6e41e 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
@@ -35,18 +35,15 @@
 import androidx.compose.foundation.pager.PagerDefaults
 import androidx.compose.foundation.pager.PagerSnapDistance
 import androidx.compose.foundation.pager.VerticalPager
-import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ShapeDefaults
 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.geometry.CornerRadius
+import androidx.compose.ui.draw.clip
 import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.geometry.RoundRect
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Outline
 import androidx.compose.ui.graphics.Shape
@@ -262,6 +259,17 @@
         ) { page ->
             val carouselItemInfo = remember { CarouselItemInfoImpl() }
             val scope = remember { CarouselItemScopeImpl(itemInfo = carouselItemInfo) }
+            val clipShape = remember {
+                object : Shape {
+                    override fun createOutline(
+                        size: Size,
+                        layoutDirection: LayoutDirection,
+                        density: Density
+                    ): Outline {
+                        return Outline.Rectangle(carouselItemInfo.maskRect)
+                    }
+                }
+            }
 
             Box(
                 modifier = Modifier.carouselItem(
@@ -269,6 +277,7 @@
                     state = state,
                     strategy = { pageSize.strategy },
                     carouselItemInfo = carouselItemInfo,
+                    clipShape = clipShape
                 )
             ) {
                 scope.content(page)
@@ -291,6 +300,17 @@
         ) { page ->
             val carouselItemInfo = remember { CarouselItemInfoImpl() }
             val scope = remember { CarouselItemScopeImpl(itemInfo = carouselItemInfo) }
+            val clipShape = remember {
+                object : Shape {
+                    override fun createOutline(
+                        size: Size,
+                        layoutDirection: LayoutDirection,
+                        density: Density
+                    ): Outline {
+                        return Outline.Rectangle(carouselItemInfo.maskRect)
+                    }
+                }
+            }
 
             Box(
                 modifier = Modifier.carouselItem(
@@ -298,6 +318,7 @@
                     state = state,
                     strategy = { pageSize.strategy },
                     carouselItemInfo = carouselItemInfo,
+                    clipShape = clipShape
                 )
             ) {
                 scope.content(page)
@@ -390,6 +411,9 @@
  * @param state the carousel state
  * @param strategy the strategy used to mask and translate items in the carousel
  * @param carouselItemInfo the item info that should be updated with the changes in this modifier
+ * @param clipShape the shape the item will clip itself to. This should be a rectangle with a bounds
+ * that match the carousel item info's mask rect. Corner radii and other shape customizations can
+ * be done by the client using [CarouselItemScope.maskClip] and [CarouselItemScope.maskBorder].
  */
 @OptIn(ExperimentalMaterial3Api::class)
 internal fun Modifier.carouselItem(
@@ -397,6 +421,7 @@
     state: CarouselState,
     strategy: () -> Strategy,
     carouselItemInfo: CarouselItemInfoImpl,
+    clipShape: Shape,
 ): Modifier {
     return layout { measurable, constraints ->
         val strategyResult = strategy.invoke()
@@ -480,33 +505,8 @@
                 carouselItemInfo.maskRectState = maskRect
 
                 // Clip the item
-                clip = true
-                shape = object : Shape {
-                    // TODO: Find a way to use the shape of the item set by the client for each item
-                    // TODO: Allow corner size customization
-                    val roundedCornerShape = RoundedCornerShape(ShapeDefaults.ExtraLarge.topStart)
-                    override fun createOutline(
-                        size: Size,
-                        layoutDirection: LayoutDirection,
-                        density: Density
-                    ): Outline {
-                        val cornerSize =
-                            roundedCornerShape.topStart.toPx(
-                                Size(maskRect.width, maskRect.height),
-                                density
-                            )
-                        val cornerRadius = CornerRadius(cornerSize)
-                        return Outline.Rounded(
-                            RoundRect(
-                                rect = maskRect,
-                                topLeft = cornerRadius,
-                                topRight = cornerRadius,
-                                bottomRight = cornerRadius,
-                                bottomLeft = cornerRadius
-                            )
-                        )
-                    }
-                }
+                clip = maskRect != Rect(0f, 0f, size.width, size.height)
+                shape = clipShape
 
                 // After clipping, the items will have white space between them. Translate the
                 // items to pin their edges together
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselItemScope.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselItemScope.kt
index fea9940..47a7aaf 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselItemScope.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselItemScope.kt
@@ -16,7 +16,19 @@
 
 package androidx.compose.material3.carousel
 
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.border
+import androidx.compose.foundation.shape.GenericShape
 import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.toRect
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.addOutline
+import androidx.compose.ui.platform.LocalDensity
 
 /**
  * Receiver scope for [Carousel] item content.
@@ -31,6 +43,38 @@
      * in the composition.
      */
     val carouselItemInfo: CarouselItemInfo
+
+    /**
+     * Clips the composable to the given [shape], taking into account the item's size in the cross
+     * axis and mask in the main axis.
+     *
+     * @param shape the shape to be applied to the composable
+     */
+    @Composable
+    fun Modifier.maskClip(shape: Shape): Modifier
+
+    /**
+     * Draw a border on the composable using the given [shape], taking into account the item's size
+     * in the cross axis and mask in the main axis.
+     *
+     * @param border the border to be drawn around the composable
+     * @param shape the shape of the border
+     */
+    @Composable
+    fun Modifier.maskBorder(border: BorderStroke, shape: Shape): Modifier
+
+    /**
+     * Converts and remembers [shape] into a [GenericShape] that uses the intersection of the
+     * carousel item's mask Rect and Size as the final shape's bounds.
+     *
+     * This method is useful if using a [Shape] in a Modifier other than [maskClip] and [maskBorder]
+     * where the shape should follow the changes in the item's mask size.
+     *
+     * @param shape The shape that will be converted and remembered and react to changes in the
+     * item's mask.
+     */
+    @Composable
+    fun rememberMaskShape(shape: Shape): GenericShape
 }
 
 @ExperimentalMaterial3Api
@@ -39,4 +83,26 @@
 ) : CarouselItemScope {
     override val carouselItemInfo: CarouselItemInfo
         get() = itemInfo
+
+    @Composable
+    override fun Modifier.maskClip(shape: Shape): Modifier =
+        clip(rememberMaskShape(shape = shape))
+
+    @Composable
+    override fun Modifier.maskBorder(
+        border: BorderStroke,
+        shape: Shape
+    ): Modifier = border(border, rememberMaskShape(shape = shape))
+
+    @Composable
+    override fun rememberMaskShape(shape: Shape): GenericShape {
+        val density = LocalDensity.current
+        return remember(carouselItemInfo, density) {
+            GenericShape { size, direction ->
+                val rect = carouselItemInfo.maskRect.intersect(size.toRect())
+                addOutline(shape.createOutline(rect.size, direction, density))
+                translate(Offset(rect.left, rect.top))
+            }
+        }
+    }
 }