package {{.PackageName}}.ui.components import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.* import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Warning import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.WifiOff import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale import androidx.compose.ui.unit.dp @Composable fun NetworkErrorOverlay( showError: Boolean, isConnected: Boolean, onRetry: () -> Unit, modifier: Modifier = Modifier ) { // Auto-hide when connection is restored var isVisible by remember { mutableStateOf(false) } var isRetrying by remember { mutableStateOf(false) } LaunchedEffect(showError, isConnected) { isVisible = showError && !isConnected if (isConnected) { isRetrying = false } } // Pulsing animation for icon val infiniteTransition = rememberInfiniteTransition(label = "pulse") val scale by infiniteTransition.animateFloat( initialValue = 1f, targetValue = 1.15f, animationSpec = infiniteRepeatable( animation = tween(1000, easing = EaseInOut), repeatMode = RepeatMode.Reverse ), label = "iconScale" ) AnimatedVisibility( visible = isVisible, enter = slideInVertically( initialOffsetY = { -it }, animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow ) ) + fadeIn(animationSpec = tween(300)), exit = slideOutVertically( targetOffsetY = { -it }, animationSpec = spring( dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMedium ) ) + fadeOut(animationSpec = tween(200)), modifier = modifier ) { Surface( modifier = Modifier .fillMaxWidth() .statusBarsPadding() .padding(horizontal = 16.dp, vertical = 12.dp) .clip(RoundedCornerShape(16.dp)), color = MaterialTheme.colorScheme.errorContainer, tonalElevation = 4.dp, shadowElevation = 8.dp, shape = RoundedCornerShape(16.dp) ) { Row( modifier = Modifier .fillMaxWidth() .padding(20.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f) ) { Icon( imageVector = Icons.Default.WifiOff, contentDescription = "No Internet", tint = MaterialTheme.colorScheme.error, modifier = Modifier .size(28.dp) .scale(scale) ) Spacer(modifier = Modifier.width(16.dp)) Column { Text( text = "No Internet Connection", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onErrorContainer ) Text( text = "Check your network and try again", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onErrorContainer.copy(alpha = 0.75f) ) } } Spacer(modifier = Modifier.width(12.dp)) FilledTonalButton( onClick = { isRetrying = true onRetry() }, enabled = !isRetrying, colors = ButtonDefaults.filledTonalButtonColors( containerColor = MaterialTheme.colorScheme.error, contentColor = MaterialTheme.colorScheme.onError ), contentPadding = PaddingValues(horizontal = 20.dp, vertical = 12.dp), shape = RoundedCornerShape(12.dp) ) { if (isRetrying) { CircularProgressIndicator( modifier = Modifier.size(18.dp), color = MaterialTheme.colorScheme.onError, strokeWidth = 2.dp ) } else { Icon( imageVector = Icons.Default.Refresh, contentDescription = "Retry", modifier = Modifier.size(20.dp) ) Spacer(modifier = Modifier.width(8.dp)) Text("Retry", style = MaterialTheme.typography.labelLarge) } } } } } }