package chats.presentation.components import androidx.compose.animation.core.Animatable import androidx.compose.foundation.background import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.gestures.rememberDraggableState import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Reply import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import kotlin.math.roundToInt @Composable fun SwipeableMessageItem( onReply: () -> Unit, content: @Composable () -> Unit ) { val density = LocalDensity.current val scope = rememberCoroutineScope() val offsetX = remember { Animatable(0f) } val replyThreshold = with(density) { 60.dp.toPx() } val maxDrag = with(density) { 90.dp.toPx() } var isTriggered by remember { mutableStateOf(false) } Box( modifier = Modifier .fillMaxWidth() .draggable( orientation = Orientation.Horizontal, state = rememberDraggableState { delta -> scope.launch { // We only allow swiping to the left (negative delta) val newOffset = (offsetX.value + delta).coerceIn(-maxDrag, 0f) offsetX.snapTo(newOffset) if (newOffset <= -replyThreshold && !isTriggered) { isTriggered = true // Trigger haptic feedback if available? } else if (newOffset > -replyThreshold) { isTriggered = false } } }, onDragStopped = { if (offsetX.value <= -replyThreshold) { onReply() } scope.launch { offsetX.animateTo(0f) isTriggered = false } } ) ) { // Reply Icon (Underneath) Box( modifier = Modifier .align(Alignment.CenterEnd) .padding(end = 16.dp) .size(36.dp) .alpha(((-offsetX.value) / replyThreshold).coerceIn(0f, 1f)) .clip(CircleShape) .background(if (isTriggered) MaterialTheme.colorScheme.primary else Color.Gray.copy(alpha = 0.2f)), contentAlignment = Alignment.Center ) { Icon( imageVector = Icons.Default.Reply, contentDescription = "Reply", tint = Color.White, modifier = Modifier.size(20.dp) ) } // Message Content Box( modifier = Modifier .offset { IntOffset(offsetX.value.roundToInt(), 0) } .fillMaxWidth() ) { content() } } }