package com.rosetta.messenger.ui.chats.input

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.media.MediaRecorder
import android.os.Build
import android.view.inputmethod.InputMethodManager
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import com.airbnb.lottie.LottieProperty
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.rememberLottieDynamicProperties
import com.airbnb.lottie.compose.rememberLottieDynamicProperty
import com.airbnb.lottie.compose.rememberLottieComposition
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.ui.draw.alpha
import com.rosetta.messenger.ui.icons.TelegramIcons
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.toArgb
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalFocusManager

import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.withTransform
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition
import app.rosette.android.ui.keyboard.KeyboardTransitionCoordinator
import androidx.core.content.ContextCompat
import coil.compose.AsyncImage
import coil.request.ImageRequest
import compose.icons.TablerIcons
import compose.icons.tablericons.Microphone
import compose.icons.tablericons.Video
import com.rosetta.messenger.network.AttachmentType
import com.rosetta.messenger.repository.AvatarRepository
import com.rosetta.messenger.R
import com.rosetta.messenger.ui.components.AppleEmojiText
import com.rosetta.messenger.ui.components.AppleEmojiTextField
import com.rosetta.messenger.ui.components.AvatarImage
import com.rosetta.messenger.ui.components.OptimizedEmojiPicker
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import com.rosetta.messenger.ui.chats.models.*
import com.rosetta.messenger.ui.chats.components.*
import com.rosetta.messenger.ui.chats.utils.*
import com.rosetta.messenger.ui.chats.ChatViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import java.io.File
import java.util.Locale
import java.util.UUID
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.tan
import kotlin.random.Random

private val EaseOutQuint = CubicBezierEasing(0.23f, 1f, 0.32f, 1f)
private const val INPUT_JUMP_LOG_ENABLED = false

private fun lerpFloat(start: Float, end: Float, fraction: Float): Float {
    return start + (end - start) * fraction
}

private fun truncateEmojiSafe(text: String, maxLen: Int): String {
    if (text.length <= maxLen) return text
    var cutAt = maxLen
    val lastColon = text.lastIndexOf(':', cutAt - 1)
    if (lastColon >= 0) {
        val sub = text.substring(lastColon)
        if (sub.startsWith(":emoji_") && !sub.substring(1).contains(':')) {
            cutAt = lastColon
        }
    }
    return text.substring(0, cutAt).trimEnd() + "..."
}

private fun bytesToHexLower(bytes: ByteArray): String {
    if (bytes.isEmpty()) return ""
    val out = StringBuilder(bytes.size * 2)
    bytes.forEach { out.append(String.format("%02x", it.toInt() and 0xff)) }
    return out.toString()
}

internal fun compressVoiceWaves(source: List<Float>, targetLength: Int): List<Float> {
    if (targetLength <= 0) return emptyList()
    if (source.isEmpty()) return List(targetLength) { 0f }
    if (source.size == targetLength) return source

    if (source.size > targetLength) {
        val bucketSize = source.size / targetLength.toFloat()
        return List(targetLength) { index ->
            val start = kotlin.math.floor(index * bucketSize).toInt()
            val end = kotlin.math.max(start + 1, kotlin.math.floor((index + 1) * bucketSize).toInt())
            var maxValue = 0f
            for (i in start until end.coerceAtMost(source.size)) {
                if (source[i] > maxValue) maxValue = source[i]
            }
            maxValue
        }
    }

    if (targetLength == 1) return listOf(source.first())

    val lastIndex = source.lastIndex.toFloat()
    return List(targetLength) { index ->
        val pos = index * lastIndex / (targetLength - 1).toFloat()
        val left = kotlin.math.floor(pos).toInt()
        val right = kotlin.math.min(kotlin.math.ceil(pos).toInt(), source.lastIndex)
        if (left == right) {
            source[left]
        } else {
            val t = pos - left.toFloat()
            source[left] * (1f - t) + source[right] * t
        }
    }
}

internal fun formatVoiceRecordTimer(elapsedMs: Long): String {
    val safeTenths = (elapsedMs.coerceAtLeast(0L) / 100L).toInt()
    val totalSeconds = safeTenths / 10
    val tenths = safeTenths % 10
    val minutes = totalSeconds / 60
    val seconds = (totalSeconds % 60).toString().padStart(2, '0')
    return "$minutes:$seconds,$tenths"
}

private enum class RecordMode {
    VOICE,
    VIDEO
}

private enum class RecordUiState {
    IDLE,
    PRESSING,
    RECORDING,
    LOCKED,
    PAUSED,
    CANCELLING,
    SENDING
}

private const val TG_BLOB_MAX_SPEED = 8.2f
private const val TG_BLOB_MIN_SPEED = 0.8f
private const val TG_BLOB_SCALE_BIG_MIN = 0.878f
private const val TG_BLOB_SCALE_SMALL_MIN = 0.926f
private const val TG_BLOB_ANIM_SPEED_BIG = 0.35f
private const val TG_BLOB_ANIM_SPEED_SMALL = 0.55f

private class TelegramBlobWave(
    pointCount: Int,
    private val speedScale: Float,
    private val isBigWave: Boolean,
    seed: Int
) {
    private val random = Random(seed)
    private val lFactor = ((4.0 / 3.0) * tan(PI / (2.0 * pointCount.toDouble()))).toFloat()
    private val n = pointCount

    private val radius = FloatArray(pointCount)
    private val angle = FloatArray(pointCount)
    private val radiusNext = FloatArray(pointCount)
    private val angleNext = FloatArray(pointCount)
    private val progress = FloatArray(pointCount)
    private val speed = FloatArray(pointCount)

    private var initialized = false
    private var minRadius = 0f
    private var maxRadius = 0f

    var amplitude: Float = 0f
        private set
    private var animateToAmplitude = 0f
    private var animateAmplitudeDiff = 0f

    fun setRadiusBounds(minRadius: Float, maxRadius: Float) {
        val safeMin = minRadius.coerceAtLeast(1f)
        val safeMax = maxRadius.coerceAtLeast(safeMin + 0.1f)
        if (!initialized ||
            kotlin.math.abs(this.minRadius - safeMin) > 0.01f ||
            kotlin.math.abs(this.maxRadius - safeMax) > 0.01f
        ) {
            this.minRadius = safeMin
            this.maxRadius = safeMax
            regenerate()
            initialized = true
        }
    }

    fun setAmplitudeTarget(target: Float) {
        val clamped = target.coerceIn(0f, 1f)
        animateToAmplitude = clamped
        animateAmplitudeDiff =
            if (isBigWave) {
                if (animateToAmplitude > amplitude) {
                    (animateToAmplitude - amplitude) / (100f + 300f * TG_BLOB_ANIM_SPEED_BIG)
                } else {
                    (animateToAmplitude - amplitude) / (100f + 500f * TG_BLOB_ANIM_SPEED_BIG)
                }
            } else {
                if (animateToAmplitude > amplitude) {
                    (animateToAmplitude - amplitude) / (100f + 400f * TG_BLOB_ANIM_SPEED_SMALL)
                } else {
                    (animateToAmplitude - amplitude) / (100f + 500f * TG_BLOB_ANIM_SPEED_SMALL)
                }
            }
    }

    fun update(dtMs: Float) {
        if (!initialized) return
        updateAmplitude(dtMs)
        for (i in 0 until n) {
            progress[i] += (speed[i] * TG_BLOB_MIN_SPEED) + amplitude * speed[i] * TG_BLOB_MAX_SPEED * speedScale
            if (progress[i] >= 1f) {
                progress[i] = 0f
                radius[i] = radiusNext[i]
                angle[i] = angleNext[i]
                generatePoint(radiusNext, angleNext, i)
            }
        }
    }

    fun buildPath(center: Offset): Path {
        val path = Path()
        if (!initialized) return path

        for (i in 0 until n) {
            val nextIndex = if (i + 1 < n) i + 1 else 0
            val p1 = progress[i]
            val p2 = progress[nextIndex]

            val r1 = radius[i] * (1f - p1) + radiusNext[i] * p1
            val r2 = radius[nextIndex] * (1f - p2) + radiusNext[nextIndex] * p2
            val a1 = angle[i] * (1f - p1) + angleNext[i] * p1
            val a2 = angle[nextIndex] * (1f - p2) + angleNext[nextIndex] * p2

            val l = lFactor * ((r1 + r2) / 2f)
            val start = rotateRelative(0f, -r1, a1, center)
            val control1 = rotateRelative(l, -r1, a1, center)
            val end = rotateRelative(0f, -r2, a2, center)
            val control2 = rotateRelative(-l, -r2, a2, center)

            if (i == 0) {
                path.moveTo(start.x, start.y)
            }
            path.cubicTo(
                control1.x, control1.y,
                control2.x, control2.y,
                end.x, end.y
            )
        }

        path.close()
        return path
    }

    private fun regenerate() {
        for (i in 0 until n) {
            generatePoint(radius, angle, i)
            generatePoint(radiusNext, angleNext, i)
            progress[i] = 0f
        }
    }

    private fun generatePoint(outRadius: FloatArray, outAngle: FloatArray, i: Int) {
        val angleDiff = 360f / n * 0.05f
        val radiusDiff = (maxRadius - minRadius).coerceAtLeast(0f)
        val radiusRnd = random.nextInt(100) / 100f
        val angleRnd = random.nextInt(100) / 100f
        val speedRnd = random.nextInt(100) / 100f

        outRadius[i] = minRadius + radiusRnd * radiusDiff
        outAngle[i] = 360f / n * i + angleRnd * angleDiff
        speed[i] = 0.017f + 0.003f * speedRnd
    }

    private fun updateAmplitude(dtMs: Float) {
        if (animateToAmplitude == amplitude) return
        amplitude += animateAmplitudeDiff * dtMs
        if (animateAmplitudeDiff > 0) {
            if (amplitude > animateToAmplitude) amplitude = animateToAmplitude
        } else {
            if (amplitude < animateToAmplitude) amplitude = animateToAmplitude
        }
    }

    private fun rotateRelative(x: Float, y: Float, angleDeg: Float, center: Offset): Offset {
        val rad = Math.toRadians(angleDeg.toDouble())
        val cosV = cos(rad).toFloat()
        val sinV = sin(rad).toFloat()
        return Offset(
            x = center.x + x * cosV - y * sinV,
            y = center.y + x * sinV + y * cosV
        )
    }
}

@Composable
private fun RecordBlinkDot(
    isDarkTheme: Boolean,
    indicatorSize: Dp = 24.dp,
    modifier: Modifier = Modifier
) {
    var entered by remember { mutableStateOf(false) }
    LaunchedEffect(Unit) { entered = true }

    val enterScale by animateFloatAsState(
        targetValue = if (entered) 1f else 0f,
        animationSpec = tween(durationMillis = 180, easing = LinearOutSlowInEasing),
        label = "record_dot_enter_scale"
    )
    val blinkAlpha by rememberInfiniteTransition(label = "record_dot_blink").animateFloat(
        initialValue = 1f,
        targetValue = 0f,
        animationSpec = infiniteRepeatable(
            animation = tween(durationMillis = 600, easing = LinearEasing),
            repeatMode = RepeatMode.Reverse
        ),
        label = "record_dot_alpha"
    )

    val dotColor = if (isDarkTheme) Color(0xFFFF5A5A) else Color(0xFFE84D4D)
    Box(
        modifier = modifier
            .size(indicatorSize)
            .graphicsLayer {
                scaleX = enterScale
                scaleY = enterScale
                alpha = blinkAlpha
            },
        contentAlignment = Alignment.Center
    ) {
        Box(
            modifier = Modifier
                .size(10.dp) // Telegram fallback dot radius = 5dp
                .clip(CircleShape)
                .background(dotColor)
        )
    }
}

@Composable
private fun TelegramVoiceDeleteIndicator(
    cancelProgress: Float,
    isDarkTheme: Boolean,
    indicatorSize: Dp = 24.dp,
    modifier: Modifier = Modifier
) {
    val progress = cancelProgress.coerceIn(0f, 1f)
    // Ensure red dot is clearly visible first; trash appears with a delayed phase.
    val trashStart = 0.28f
    val reveal = ((progress - trashStart) / (1f - trashStart)).coerceIn(0f, 1f)
    val lottieProgress = FastOutSlowInEasing.transform(reveal)
    val lottieAlpha = FastOutSlowInEasing.transform(reveal)
    val composition by rememberLottieComposition(
        LottieCompositionSpec.RawRes(R.raw.chat_audio_record_delete_2)
    )
    val dangerColor = if (isDarkTheme) Color(0xFFFF5A5A) else Color(0xFFE84D4D)
    val neutralColor = if (isDarkTheme) Color(0xFF8EA2B4) else Color(0xFF8FA2B3)
    val panelBlendColor = if (isDarkTheme) Color(0xFF1A2A3A) else Color(0xFFE8F2FD)
    val dynamicProperties = rememberLottieDynamicProperties(
        rememberLottieDynamicProperty(
            property = LottieProperty.COLOR,
            value = dangerColor.toArgb(),
            keyPath = arrayOf("Cup Red", "**")
        ),
        rememberLottieDynamicProperty(
            property = LottieProperty.COLOR,
            value = dangerColor.toArgb(),
            keyPath = arrayOf("Box Red", "**")
        ),
        rememberLottieDynamicProperty(
            property = LottieProperty.COLOR,
            value = neutralColor.toArgb(),
            keyPath = arrayOf("Cup Grey", "**")
        ),
        rememberLottieDynamicProperty(
            property = LottieProperty.COLOR,
            value = neutralColor.toArgb(),
            keyPath = arrayOf("Box Grey", "**")
        ),
        rememberLottieDynamicProperty(
            property = LottieProperty.COLOR,
            value = panelBlendColor.toArgb(),
            keyPath = arrayOf("Line 1", "**")
        ),
        rememberLottieDynamicProperty(
            property = LottieProperty.COLOR,
            value = panelBlendColor.toArgb(),
            keyPath = arrayOf("Line 2", "**")
        ),
        rememberLottieDynamicProperty(
            property = LottieProperty.COLOR,
            value = panelBlendColor.toArgb(),
            keyPath = arrayOf("Line 3", "**")
        )
    )

    Box(
        modifier = modifier.size(indicatorSize),
        contentAlignment = Alignment.Center
    ) {
        // Single recording dot (no duplicate red indicators).
        RecordBlinkDot(
            isDarkTheme = isDarkTheme,
            indicatorSize = indicatorSize,
            modifier = Modifier.graphicsLayer {
                alpha = 1f - lottieAlpha
                scaleX = 1f - 0.12f * lottieAlpha
                scaleY = 1f - 0.12f * lottieAlpha
            }
        )
        if (composition != null) {
            LottieAnimation(
                composition = composition,
                progress = { lottieProgress },
                dynamicProperties = dynamicProperties,
                modifier = Modifier
                    .matchParentSize()
                    .graphicsLayer {
                        alpha = lottieAlpha
                        scaleX = 0.92f + 0.08f * lottieAlpha
                        scaleY = 0.92f + 0.08f * lottieAlpha
                    }
            )
        }
    }
}

@Composable
private fun VoiceMovingBlob(
    voiceLevel: Float,
    isDarkTheme: Boolean,
    modifier: Modifier = Modifier
) {
    val rawLevel = voiceLevel.coerceIn(0f, 1f)
    val level by animateFloatAsState(
        targetValue = rawLevel,
        animationSpec = tween(durationMillis = 100, easing = LinearOutSlowInEasing),
        label = "voice_blob_level"
    )
    val transition = rememberInfiniteTransition(label = "voice_blob_motion")
    val phase by transition.animateFloat(
        initialValue = 0f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            animation = tween(durationMillis = 1400, easing = LinearEasing),
            repeatMode = RepeatMode.Restart
        ),
        label = "voice_blob_phase"
    )

    val waveColor = if (isDarkTheme) Color(0xFF69CCFF) else Color(0xFF2D9CFF)
    Canvas(
        modifier = modifier
            .width(36.dp)
            .height(18.dp)
    ) {
        val cxBase = size.width * 0.5f
        val cy = size.height * 0.5f
        val waveShift = (size.width * 0.16f * sin(phase * 2f * PI.toFloat()))
        val cx = cxBase + waveShift
        val base = size.minDimension * 0.22f

        drawCircle(
            color = waveColor.copy(alpha = 0.2f + level * 0.15f),
            radius = base * (2.1f + level * 1.0f),
            center = Offset(cx - waveShift * 0.55f, cy)
        )
        drawCircle(
            color = waveColor.copy(alpha = 0.38f + level * 0.20f),
            radius = base * (1.55f + level * 0.75f),
            center = Offset(cx + waveShift * 0.35f, cy)
        )
        drawCircle(
            color = waveColor.copy(alpha = 0.95f),
            radius = base * (0.88f + level * 0.15f),
            center = Offset(cx, cy)
        )
    }
}

@Composable
private fun VoiceButtonBlob(
    voiceLevel: Float,
    slideToCancelProgress: Float,
    isDarkTheme: Boolean,
    modifier: Modifier = Modifier
) {
    val rawLevel = voiceLevel.coerceIn(0f, 1f)
    val slideProgress = slideToCancelProgress.coerceIn(0f, 1f)
    val slideProgress1 = if (slideProgress > 0.7f) 1f else (slideProgress / 0.7f).coerceIn(0f, 1f)
    val bigWave = remember { TelegramBlobWave(pointCount = 12, speedScale = 1.01f, isBigWave = true, seed = 1207) }
    val tinyWave = remember { TelegramBlobWave(pointCount = 11, speedScale = 1.02f, isBigWave = false, seed = 1109) }
    var frameTick by remember { mutableLongStateOf(0L) }

    LaunchedEffect(rawLevel) {
        bigWave.setAmplitudeTarget(rawLevel)
        tinyWave.setAmplitudeTarget(rawLevel)
    }

    LaunchedEffect(Unit) {
        var lastFrameNs = 0L
        while (true) {
            withFrameNanos { frameNs ->
                if (lastFrameNs != 0L) {
                    val dtMs = ((frameNs - lastFrameNs) / 1_000_000f).coerceAtLeast(1f)
                    bigWave.update(dtMs)
                    tinyWave.update(dtMs)
                    frameTick++
                }
                lastFrameNs = frameNs
            }
        }
    }

    val blobColor = if (isDarkTheme) Color(0xFF52C3FF) else Color(0xFF2D9CFF)

    Canvas(modifier = modifier) {
        val tick = frameTick
        if (tick < 0L) return@Canvas
        if (slideProgress1 <= 0f) return@Canvas

        val center = Offset(
            x = size.width * 0.5f,
            y = size.height * 0.5f
        )
        val side = size.minDimension
        tinyWave.setRadiusBounds(
            minRadius = side * 0.52f,
            maxRadius = side * 0.62f
        )
        bigWave.setRadiusBounds(
            minRadius = side * 0.55f,
            maxRadius = side * 0.63f
        )

        // Telegram-like constants:
        // SCALE_BIG_MIN=0.878, SCALE_SMALL_MIN=0.926, +1.4*amplitude
        val bigScale = slideProgress1 * (TG_BLOB_SCALE_BIG_MIN + 1.4f * bigWave.amplitude)
        val smallScale = slideProgress1 * (TG_BLOB_SCALE_SMALL_MIN + 1.4f * tinyWave.amplitude)
        val bigPath = bigWave.buildPath(center)
        val smallPath = tinyWave.buildPath(center)

        withTransform({
            scale(scaleX = bigScale, scaleY = bigScale, pivot = center)
        }) {
            drawPath(path = bigPath, color = blobColor.copy(alpha = 0.30f))
        }
        withTransform({
            scale(scaleX = smallScale, scaleY = smallScale, pivot = center)
        }) {
            drawPath(path = smallPath, color = blobColor.copy(alpha = 0.15f))
        }
    }
}

private const val LOCK_HINT_PREF_KEY = "lock_record_hint_shown_count"
private const val LOCK_HINT_MAX_SHOWS = 3

@Composable
private fun LockIcon(
    movementProgress: Float,
    isLocked: Boolean,
    isPaused: Boolean,
    isDarkTheme: Boolean,
    modifier: Modifier = Modifier
) {
    val movement = movementProgress.coerceIn(0f, 1f)
    val lockedOrPaused = isLocked || isPaused

    // Staggered snap animations — Telegram timing
    val snapAnim by animateFloatAsState(
        targetValue = if (lockedOrPaused) 1f else 0f,
        animationSpec = tween(durationMillis = 250, easing = EaseOutQuint),
        label = "lock_snap"
    )
    val pauseTransform by animateFloatAsState(
        targetValue = if (lockedOrPaused) 1f else 0f,
        animationSpec = tween(durationMillis = 300, delayMillis = 150, easing = FastOutSlowInEasing),
        label = "lock_to_pause"
    )
    // Idle "breathing" animation for shackle
    val idlePhase by rememberInfiniteTransition(label = "lock_idle").animateFloat(
        initialValue = 0f, targetValue = 1f,
        animationSpec = infiniteRepeatable(tween(1200, easing = LinearEasing), RepeatMode.Reverse),
        label = "lock_idle_phase"
    )

    val bgColor = if (isDarkTheme) Color(0xFF2A2A3E) else Color(0xFFF0F0F0)
    val iconColor = if (isDarkTheme) Color.White else Color(0xFF333333)
    val shadowColor = if (isDarkTheme) Color.Black.copy(alpha = 0.3f) else Color.Black.copy(alpha = 0.15f)
    val bgPaintColor = if (isDarkTheme) Color(0xFF3A3A4E) else Color(0xFFE8E8E8)

    // Lock is always visible during recording (Telegram shows it immediately)
    val enterAlpha by animateFloatAsState(
        targetValue = 1f,
        animationSpec = tween(durationMillis = 200),
        label = "lock_enter_alpha"
    )

    Canvas(
        modifier = modifier.graphicsLayer { alpha = enterAlpha }
    ) {
        val dp1 = (size.width / 36f).coerceAtLeast(0.001f)
        val pillW = size.width
        val pillH = size.height
        val pillLeft = 0f
        val pillTop = 0f
        val cx = pillW / 2f
        // Telegram moveProgress:
        // 1f at rest/locked, 0f near lock threshold.
        val moveProgress = movement

        // Telegram rotation: dual-phase snap
        val snapRotateBack = if (moveProgress > 0.4f) 1f else moveProgress / 0.4f
        val rotation = if (lockedOrPaused) {
            9f * (1f - moveProgress) * (1f - snapAnim) -
                15f * snapAnim * (1f - snapRotateBack)
        } else {
            9f * (1f - moveProgress)
        }

        // ── Background pill with shadow ──
        val pillRadius = pillW / 2f

        // Shadow
        drawRoundRect(
            color = shadowColor,
            topLeft = Offset(pillLeft - 3f * dp1, pillTop - 2f * dp1),
            size = androidx.compose.ui.geometry.Size(pillW + 6f * dp1, pillH + 4f * dp1),
            cornerRadius = androidx.compose.ui.geometry.CornerRadius(pillRadius + 3f * dp1)
        )
        // Pill background
        drawRoundRect(
            color = bgColor,
            topLeft = Offset(pillLeft, pillTop),
            size = androidx.compose.ui.geometry.Size(pillW, pillH),
            cornerRadius = androidx.compose.ui.geometry.CornerRadius(pillRadius)
        )

        // ── Lock icon drawing (Telegram-exact) ──
        // Body: 16dp × 16dp centered, with corner radius 3dp
        val bodyW = 16f * dp1
        val bodyH = 16f * dp1
        val bodyRadius = 3f * dp1
        val bodyCx = cx
        val bodyCy = pillTop + pillH * 0.5f + 2f * dp1 * moveProgress
        val bodyLeft = bodyCx - bodyW / 2f
        val bodyTop = bodyCy - bodyH / 2f

        // Shackle: 8dp × 8dp arc above body, stroke 1.7dp
        val shackleW = 8f * dp1
        val shackleH = 8f * dp1
        val shackleStroke = 1.7f * dp1
        val shackleLeft = bodyCx - shackleW / 2f
        val shackleTop =
            bodyTop - 6f * dp1 -
                lerpFloat(2f * dp1, 1.5f * dp1 * (1f - idlePhase), moveProgress) +
                2f * dp1 * snapAnim

        val lockIconAlpha = 1f - pauseTransform
        val idleOffset =
            4f * dp1 * idlePhase * moveProgress * if (lockedOrPaused) 0f else 1f
        val snapOffset = 4f * dp1 * snapAnim * (1f - moveProgress)

        if (lockIconAlpha > 0.01f) {
            rotate(degrees = rotation, pivot = Offset(bodyCx, bodyCy)) {
                // Shackle arc (half circle)
                drawArc(
                    color = iconColor.copy(alpha = lockIconAlpha),
                    startAngle = 180f,
                    sweepAngle = 180f,
                    useCenter = false,
                    topLeft = Offset(shackleLeft, shackleTop),
                    size = androidx.compose.ui.geometry.Size(shackleW, shackleH),
                    style = androidx.compose.ui.graphics.drawscope.Stroke(
                        width = shackleStroke,
                        cap = androidx.compose.ui.graphics.StrokeCap.Round
                    )
                )
                // Right leg (fixed)
                drawLine(
                    color = iconColor.copy(alpha = lockIconAlpha),
                    start = Offset(shackleLeft + shackleW, shackleTop + shackleH / 2f),
                    end = Offset(shackleLeft + shackleW, bodyTop + 2f * dp1),
                    strokeWidth = shackleStroke,
                    cap = androidx.compose.ui.graphics.StrokeCap.Round
                )
                // Left leg (animated — idle breathing + lock closing)
                val leftLegEnd = bodyTop + 2f * dp1 + idleOffset + snapOffset
                drawLine(
                    color = iconColor.copy(alpha = lockIconAlpha),
                    start = Offset(shackleLeft, shackleTop + shackleH / 2f),
                    end = Offset(shackleLeft, leftLegEnd),
                    strokeWidth = shackleStroke,
                    cap = androidx.compose.ui.graphics.StrokeCap.Round
                )

                // Body (filled rounded rect)
                drawRoundRect(
                    color = iconColor.copy(alpha = lockIconAlpha),
                    topLeft = Offset(bodyLeft, bodyTop),
                    size = androidx.compose.ui.geometry.Size(bodyW, bodyH),
                    cornerRadius = androidx.compose.ui.geometry.CornerRadius(bodyRadius)
                )

                // Keyhole dot (Telegram: dpf2(2) radius at center)
                drawCircle(
                    color = bgPaintColor.copy(alpha = lockIconAlpha),
                    radius = 2f * dp1,
                    center = Offset(bodyCx, bodyCy)
                )
            }
        }

        // ── Pause/Resume transform in locked mode ──
        if (pauseTransform > 0.01f) {
            val pausePlayCenterY =
                if (lockedOrPaused) {
                    pillTop + pillH * 0.5f
                } else {
                    bodyCy
                }
            if (isPaused) {
                val triW = 10f * dp1
                val triH = 12f * dp1
                val left = bodyCx - triW / 2f
                val top = pausePlayCenterY - triH / 2f
                val playPath = Path().apply {
                    moveTo(left, top)
                    lineTo(left + triW, pausePlayCenterY)
                    lineTo(left, top + triH)
                    close()
                }
                drawPath(
                    path = playPath,
                    color = iconColor.copy(alpha = pauseTransform)
                )
            } else {
                val gap = 1.66f * dp1 * pauseTransform
                val barW = 4f * dp1
                val barH = 14f * dp1
                val barRadius = 1.5f * dp1
                drawRoundRect(
                    color = iconColor.copy(alpha = pauseTransform),
                    topLeft = Offset(bodyCx - gap - barW, pausePlayCenterY - barH / 2f),
                    size = androidx.compose.ui.geometry.Size(barW, barH),
                    cornerRadius = androidx.compose.ui.geometry.CornerRadius(barRadius)
                )
                drawRoundRect(
                    color = iconColor.copy(alpha = pauseTransform),
                    topLeft = Offset(bodyCx + gap, pausePlayCenterY - barH / 2f),
                    size = androidx.compose.ui.geometry.Size(barW, barH),
                    cornerRadius = androidx.compose.ui.geometry.CornerRadius(barRadius)
                )
            }
        }
    }
}

/**
 * iOS parity slide-to-cancel transform.
 *
 * Port from VoiceRecordingPanel.updateCancelTranslation():
 * - don't move until |dx| > 8dp
 * - then translationX = -(abs(dx) - 8) * 0.5
 * - fade out while dragging left
 * - idle arrow jiggle only when close to rest
 */
@Composable
private fun SlideToCancel(
    slideDx: Float,
    isDarkTheme: Boolean,
    modifier: Modifier = Modifier
) {
    val density = LocalDensity.current
    val dragPx = (-slideDx).coerceAtLeast(0f)
    val dragTransformThresholdPx = with(density) { 8.dp.toPx() }
    val effectiveDragPx = (dragPx - dragTransformThresholdPx).coerceAtLeast(0f)
    val slideTranslationX = -effectiveDragPx * 0.5f
    val fadeDistancePx = with(density) { 90.dp.toPx() }
    val contentAlpha = (1f - (effectiveDragPx / fadeDistancePx)).coerceIn(0f, 1f)

    val maxOffsetPx = with(density) { 6.dp.toPx() }
    val speedPxPerMs = with(density) { (3f / 250f).dp.toPx() }

    // Arrow oscillation: only when near the resting position.
    var xOffset by remember { mutableFloatStateOf(0f) }
    var moveForward by remember { mutableStateOf(true) }

    LaunchedEffect(contentAlpha > 0.85f) {
        if (contentAlpha <= 0.85f) {
            xOffset = 0f
            moveForward = true
            return@LaunchedEffect
        }
        var lastTime = System.nanoTime()
        while (true) {
            delay(16)
            val now = System.nanoTime()
            val dtMs = (now - lastTime) / 1_000_000f
            lastTime = now
            val step = speedPxPerMs * dtMs
            if (moveForward) {
                xOffset += step
                if (xOffset > maxOffsetPx) { xOffset = maxOffsetPx; moveForward = false }
            } else {
                xOffset -= step
                if (xOffset < -maxOffsetPx) { xOffset = -maxOffsetPx; moveForward = true }
            }
        }
    }

    val textColor = if (isDarkTheme) Color.White else Color(0xFF1F1F1F)
    val arrowColor = if (isDarkTheme) Color.White else Color(0xFF1F1F1F)

    Box(
        modifier = modifier.clipToBounds()
    ) {
    Row(
        modifier = Modifier
            .align(Alignment.Center)
            .graphicsLayer {
                translationX = slideTranslationX + xOffset * contentAlpha
                alpha = contentAlpha
            },
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.Center
    ) {
        // Telegram: arrow path 4×5dp, stroke 1.6dp, round caps+joins, total 10dp offset to text
        Canvas(
            modifier = Modifier.size(width = 4.dp, height = 10.dp)
        ) {
            val midY = size.height / 2f
            val arrowW = size.width
            val arrowH = 5.dp.toPx()
            val strokeW = 1.6f.dp.toPx()

            drawLine(
                color = arrowColor,
                start = Offset(arrowW, midY - arrowH),
                end = Offset(0f, midY),
                strokeWidth = strokeW,
                cap = androidx.compose.ui.graphics.StrokeCap.Round
            )
            drawLine(
                color = arrowColor,
                start = Offset(0f, midY),
                end = Offset(arrowW, midY + arrowH),
                strokeWidth = strokeW,
                cap = androidx.compose.ui.graphics.StrokeCap.Round
            )
        }

        Spacer(modifier = Modifier.width(6.dp))

        Text(
            text = "Slide to cancel",
            color = textColor,
            fontSize = 14.sp,
            fontWeight = FontWeight.Normal,
            maxLines = 1
        )
    }
    }
}

@Composable
private fun LockTooltip(
    visible: Boolean,
    isDarkTheme: Boolean,
    modifier: Modifier = Modifier
) {
    val alpha by animateFloatAsState(
        targetValue = if (visible) 1f else 0f,
        animationSpec = tween(durationMillis = 150),
        label = "tooltip_alpha"
    )

    if (alpha > 0.01f) {
        Row(
            modifier = modifier
                .graphicsLayer { this.alpha = alpha }
                .background(
                    color = Color(0xFF333333),
                    shape = RoundedCornerShape(5.dp)
                )
                .padding(horizontal = 10.dp, vertical = 5.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text(
                text = "Slide Up to Lock",
                color = Color.White,
                fontSize = 14.sp,
                maxLines = 1
            )
            Spacer(modifier = Modifier.width(4.dp))
            Text(
                text = "↑",
                color = Color.White,
                fontSize = 14.sp
            )
        }
    }
}

/**
 * Message input bar and related components
 * Extracted from ChatDetailScreen.kt for better organization
 * 
 * Telegram UX rules:
 * 1. Input always linked to keyboard (imePadding)
 * 2. Last message always visible

 * 3. No layout jumps
 * 4. After sending: input clears, keyboard stays open
 * 5. Input grows upward for multi-line (up to 6 lines)
 */

data class MentionCandidate(
    val username: String,
    val title: String,
    val publicKey: String
)

private const val LARGE_INPUT_ANALYSIS_THRESHOLD = 2000

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun MessageInputBar(
    value: String,
    onValueChange: (String) -> Unit,
    onSend: () -> Unit,
    onSendVoiceMessage: (voiceHex: String, durationSec: Int, waves: List<Float>) -> Unit = { _, _, _ -> },
    isDarkTheme: Boolean,
    backgroundColor: Color,
    textColor: Color,
    secondaryTextColor: Color,
    replyMessages: List<ChatViewModel.ReplyMessage> = emptyList(),
    isForwardMode: Boolean = false,
    onCloseReply: () -> Unit = {},
    onShowForwardOptions: (List<ChatViewModel.ReplyMessage>) -> Unit = {},
    chatTitle: String = "",
    isBlocked: Boolean = false,
    showEmojiPicker: Boolean = false,
    onToggleEmojiPicker: (Boolean) -> Unit = {},
    coordinator: KeyboardTransitionCoordinator,
    displayReplyMessages: List<ChatViewModel.ReplyMessage> = emptyList(),
    onReplyClick: (String) -> Unit = {},
    onAttachClick: () -> Unit = {},
    myPublicKey: String = "",
    opponentPublicKey: String = "",
    myPrivateKey: String = "",
    isGroupChat: Boolean = false,
    mentionCandidates: List<MentionCandidate> = emptyList(),
    avatarRepository: AvatarRepository? = null,
    inputFocusTrigger: Int = 0,
    suppressKeyboard: Boolean = false,
    hasNativeNavigationBar: Boolean = true
) {
    val hasReply = replyMessages.isNotEmpty()
    val liveReplyMessages =
        remember(replyMessages, displayReplyMessages) {
            if (displayReplyMessages.isNotEmpty()) displayReplyMessages else replyMessages
        }

    var animatedReplyMessages by remember {
        mutableStateOf<List<ChatViewModel.ReplyMessage>>(emptyList())
    }
    var animatedForwardMode by remember { mutableStateOf(isForwardMode) }

    LaunchedEffect(liveReplyMessages, isForwardMode) {
        if (liveReplyMessages.isNotEmpty()) {
            animatedReplyMessages = liveReplyMessages
            animatedForwardMode = isForwardMode
        }
    }

    val focusManager = LocalFocusManager.current
    val scope = rememberCoroutineScope()
    val context = LocalContext.current
    val view = LocalView.current
    val density = LocalDensity.current

    var editTextView by remember { mutableStateOf<com.rosetta.messenger.ui.components.AppleEmojiEditTextView?>(null) }
    var selectionStart by remember { mutableIntStateOf(value.length) }
    var selectionEnd by remember { mutableIntStateOf(value.length) }

    // Auto-focus when reply panel opens
    LaunchedEffect(hasReply, editTextView) {
        if (hasReply && !suppressKeyboard) {
            delay(50)
            editTextView?.let { editText ->
                if (!showEmojiPicker) {
                    editText.requestFocus()
                    val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
                    imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
                }
            }
        }
    }

    // Return focus to input after closing the photo editor
    LaunchedEffect(inputFocusTrigger) {
        if (inputFocusTrigger > 0 && !suppressKeyboard) {
            delay(100)
            editTextView?.let { editText ->
                editText.requestFocus()
                val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
                imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
            }
        }
    }

    // Camera overlay opened: hard-stop any keyboard/focus restoration from input effects.
    LaunchedEffect(suppressKeyboard) {
        if (suppressKeyboard) {
            val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
            imm.hideSoftInputFromWindow(view.windowToken, 0)
            focusManager.clearFocus(force = true)
            view.findFocus()?.clearFocus()
        }
    }

    val imeInsets = WindowInsets.ime
    var isKeyboardVisible by remember { mutableStateOf(false) }
    var lastStableKeyboardHeight by remember { mutableStateOf(0.dp) }
    var lastToggleTime by remember { mutableLongStateOf(0L) }
    val toggleCooldownMs = 500L
    var isKeyboardAnimating by remember { mutableStateOf(false) }
    var lastKeyboardHeightChange by remember { mutableLongStateOf(0L) }
    val keyboardAnimationStabilizeMs = 250L

    // Update coordinator through snapshotFlow (no recomposition)
    LaunchedEffect(Unit) {
        snapshotFlow { with(density) { imeInsets.getBottom(density).toDp() } }
            .distinctUntilChanged()
            .collect { currentImeHeight ->
            val now = System.currentTimeMillis()
            val heightChanged = kotlin.math.abs((currentImeHeight - lastStableKeyboardHeight).value) > 5f
            if (heightChanged && currentImeHeight.value > 0) {
                lastKeyboardHeightChange = now
                isKeyboardAnimating = true
            }
            if (now - lastKeyboardHeightChange > keyboardAnimationStabilizeMs && isKeyboardAnimating) {
                isKeyboardAnimating = false
            }

            isKeyboardVisible = currentImeHeight > 50.dp
            coordinator.updateKeyboardHeight(currentImeHeight)
            // Update "stable" height only after IME animation settles,
            // otherwise low-end devices get many unnecessary recompositions.
            if (!isKeyboardAnimating && currentImeHeight > 100.dp) {
                coordinator.syncHeights()
                if (kotlin.math.abs((currentImeHeight - lastStableKeyboardHeight).value) > 2f) {
                    lastStableKeyboardHeight = currentImeHeight
                }
            }
        }
    }

    // Load saved keyboard height
    LaunchedEffect(Unit) {
        com.rosetta.messenger.ui.components.KeyboardHeightProvider.getSavedKeyboardHeight(context)
    }

    // Save keyboard height when stable
    LaunchedEffect(isKeyboardVisible, showEmojiPicker) {
        if (isKeyboardVisible && !showEmojiPicker) {
            delay(350)
            if (isKeyboardVisible && !showEmojiPicker && lastStableKeyboardHeight > 300.dp) {
                val heightPx = with(density) { lastStableKeyboardHeight.toPx().toInt() }
                com.rosetta.messenger.ui.components.KeyboardHeightProvider.saveKeyboardHeight(context, heightPx)
            }
        }
    }

    var voiceRecorder by remember { mutableStateOf<MediaRecorder?>(null) }
    var voiceOutputFile by remember { mutableStateOf<File?>(null) }
    var isVoiceRecording by remember { mutableStateOf(false) }
    var isVoiceRecordTransitioning by remember { mutableStateOf(false) }
    var isVoiceCancelAnimating by remember { mutableStateOf(false) }
    var holdCancelVisualUntilHidden by remember { mutableStateOf(false) }
    var cancelProgressSeed by remember { mutableFloatStateOf(0f) }
    var cancelFrozenElapsedMs by remember { mutableLongStateOf(0L) }
    var keepMicGestureCapture by remember { mutableStateOf(false) }
    var recordMode by rememberSaveable { mutableStateOf(RecordMode.VOICE) }
    var recordUiState by remember { mutableStateOf(RecordUiState.IDLE) }
    var pressStartX by remember { mutableFloatStateOf(0f) }
    var pressStartY by remember { mutableFloatStateOf(0f) }
    var rawSlideDx by remember { mutableFloatStateOf(0f) }
    var rawSlideDy by remember { mutableFloatStateOf(0f) }
    var slideDx by remember { mutableFloatStateOf(0f) }
    var slideDy by remember { mutableFloatStateOf(0f) }
    var lockProgress by remember { mutableFloatStateOf(0f) }
    var isLockTransitioning by remember { mutableStateOf(false) }
    var lockTransitionStartDx by remember { mutableFloatStateOf(0f) }
    var lockTransitionStartLockProgress by remember { mutableFloatStateOf(0f) }
    var dragVelocityX by remember { mutableFloatStateOf(0f) }
    var dragVelocityY by remember { mutableFloatStateOf(0f) }
    var lastDragDx by remember { mutableFloatStateOf(0f) }
    var lastDragDy by remember { mutableFloatStateOf(0f) }
    var lastDragEventTimeMs by remember { mutableLongStateOf(0L) }
    var didCancelHaptic by remember { mutableStateOf(false) }
    var didLockHaptic by remember { mutableStateOf(false) }
    var pendingLongPressJob by remember { mutableStateOf<Job?>(null) }
    var pendingRecordAfterPermission by remember { mutableStateOf(false) }
    var voiceRecordStartedAtMs by remember { mutableLongStateOf(0L) }
    var voiceElapsedMs by remember { mutableLongStateOf(0L) }
    var voiceWaves by remember { mutableStateOf<List<Float>>(emptyList()) }
    var isVoicePaused by remember { mutableStateOf(false) }
    var voicePausedElapsedMs by remember { mutableLongStateOf(0L) }
    var inputPanelHeightPx by remember { mutableIntStateOf(0) }
    var inputPanelWidthPx by remember { mutableIntStateOf(0) }
    var inputPanelY by remember { mutableFloatStateOf(0f) }
    var normalInputRowHeightPx by remember { mutableIntStateOf(0) }
    var normalInputRowY by remember { mutableFloatStateOf(0f) }
    var recordingInputRowHeightPx by remember { mutableIntStateOf(0) }
    var recordingInputRowY by remember { mutableFloatStateOf(0f) }
    fun inputJumpLog(msg: String) {
        if (!INPUT_JUMP_LOG_ENABLED) return
    }

    fun inputHeightsSnapshot(): String {
        val panelDp = with(density) { inputPanelHeightPx.toDp().value.toInt() }
        val normalDp = with(density) { normalInputRowHeightPx.toDp().value.toInt() }
        val recDp = with(density) { recordingInputRowHeightPx.toDp().value.toInt() }
        return "panel=${inputPanelHeightPx}px(${panelDp}dp) normal=${normalInputRowHeightPx}px(${normalDp}dp) rec=${recordingInputRowHeightPx}px(${recDp}dp)"
    }

    fun setRecordUiState(newState: RecordUiState, reason: String) {
        if (recordUiState == newState) return
        val oldState = recordUiState
        recordUiState = newState
        if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("recordState $oldState -> $newState reason=$reason mode=$recordMode")
    }

    fun resetGestureState() {
        rawSlideDx = 0f
        rawSlideDy = 0f
        slideDx = 0f
        slideDy = 0f
        isLockTransitioning = false
        lockTransitionStartDx = 0f
        lockTransitionStartLockProgress = 0f
        dragVelocityX = 0f
        dragVelocityY = 0f
        lastDragDx = 0f
        lastDragDy = 0f
        lastDragEventTimeMs = 0L
        didCancelHaptic = false
        didLockHaptic = false
        pressStartX = 0f
        pressStartY = 0f
        lockProgress = 0f
        pendingLongPressJob?.cancel()
        pendingLongPressJob = null
    }

    fun startLockTransition(reason: String) {
        if (recordUiState == RecordUiState.LOCKED && !isLockTransitioning) return
        lockTransitionStartDx = slideDx
        lockTransitionStartLockProgress = lockProgress.coerceIn(0f, 1f)
        isLockTransitioning = true
        setRecordUiState(RecordUiState.LOCKED, reason)
    }

    fun toggleRecordModeByTap() {
        recordMode = if (recordMode == RecordMode.VOICE) RecordMode.VIDEO else RecordMode.VOICE
        if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("recordMode toggled -> $recordMode (short tap)")
    }

    val shouldPinBottomForInput =
        isKeyboardVisible ||
            coordinator.isEmojiBoxVisible ||
            isVoiceRecordTransitioning
    val shouldAddNavBarPadding = hasNativeNavigationBar && !shouldPinBottomForInput

    fun stopVoiceRecording(
        send: Boolean,
        preserveCancelAnimation: Boolean = false
    ) {
        val terminalState = if (send) RecordUiState.SENDING else RecordUiState.CANCELLING
        setRecordUiState(terminalState, "stop-init(send=$send)")
        isVoiceRecordTransitioning = false
        if (!preserveCancelAnimation) {
            isVoiceCancelAnimating = false
            cancelFrozenElapsedMs = 0L
            cancelProgressSeed = 0f
        }
        keepMicGestureCapture = false
        if (INPUT_JUMP_LOG_ENABLED) inputJumpLog(
            "stopVoiceRecording begin send=$send mode=$recordMode state=$recordUiState voice=$isVoiceRecording kb=$isKeyboardVisible " +
                "emojiBox=${coordinator.isEmojiBoxVisible} panelH=$inputPanelHeightPx " +
                "normalH=$normalInputRowHeightPx recH=$recordingInputRowHeightPx"
        )
        val recorder = voiceRecorder
        val outputFile = voiceOutputFile
        val elapsedSnapshot =
            if (isVoicePaused && voicePausedElapsedMs > 0L) {
                voicePausedElapsedMs
            } else if (voiceRecordStartedAtMs > 0L) {
                maxOf(voiceElapsedMs, System.currentTimeMillis() - voiceRecordStartedAtMs)
            } else {
                voiceElapsedMs
            }
        val durationSnapshot = ((elapsedSnapshot + 999L) / 1000L).toInt().coerceAtLeast(1)
        val wavesSnapshot = voiceWaves

        voiceRecorder = null
        voiceOutputFile = null
        isVoiceRecording = false
        isVoicePaused = false
        voicePausedElapsedMs = 0L
        voiceRecordStartedAtMs = 0L
        voiceElapsedMs = 0L
        voiceWaves = emptyList()
        resetGestureState()
        if (!preserveCancelAnimation) {
            // Keep terminal states real, but don't block UI while recorder.stop() runs on IO.
            setRecordUiState(RecordUiState.IDLE, "stop-ui-finished(send=$send)")
        }

        // Heavy I/O off main thread to prevent ANR
        scope.launch(kotlinx.coroutines.Dispatchers.IO) {
            var recordedOk = false
            if (recorder != null) {
                if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("stopVoiceRecording IO: calling recorder.stop() send=$send")
                val stopResult = runCatching {
                    recorder.stop()
                    true
                }
                recordedOk = stopResult.getOrDefault(false)
                if (stopResult.isFailure) {
                    if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("stopVoiceRecording IO: recorder.stop() FAILED: ${stopResult.exceptionOrNull()?.message}")
                }
                if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("stopVoiceRecording IO: recorder.stop() ok=$recordedOk, calling reset+release")
                runCatching { recorder.reset() }
                runCatching { recorder.release() }
                if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("stopVoiceRecording IO: recorder released")
            }

            if (send && recordedOk && outputFile != null && outputFile.exists() && outputFile.length() > 0L) {
                val fileSize = outputFile.length()
                if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("stopVoiceRecording IO: reading file ${outputFile.name} size=${fileSize}bytes duration=${durationSnapshot}s")
                val voiceHex =
                    runCatching { bytesToHexLower(outputFile.readBytes()) }.getOrDefault("")
                if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("stopVoiceRecording IO: hex length=${voiceHex.length} sending=${voiceHex.isNotBlank()}")
                if (voiceHex.isNotBlank()) {
                    kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.Main) {
                        if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("stopVoiceRecording Main: calling onSendVoiceMessage duration=$durationSnapshot waves=${wavesSnapshot.size}")
                        onSendVoiceMessage(
                            voiceHex,
                            durationSnapshot,
                            compressVoiceWaves(wavesSnapshot, 35)
                        )
                        if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("stopVoiceRecording Main: onSendVoiceMessage done")
                    }
                }
            } else if (send) {
                if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("stopVoiceRecording IO: NOT sending — recordedOk=$recordedOk file=${outputFile?.name} exists=${outputFile?.exists()} size=${outputFile?.length()}")
            }
            runCatching { outputFile?.delete() }
            if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("stopVoiceRecording IO: cleanup done")
        }

        if (INPUT_JUMP_LOG_ENABLED) inputJumpLog(
            "stopVoiceRecording end send=$send mode=$recordMode state=$recordUiState voice=$isVoiceRecording kb=$isKeyboardVisible " +
                "emojiBox=${coordinator.isEmojiBoxVisible} panelH=$inputPanelHeightPx " +
                "normalH=$normalInputRowHeightPx recH=$recordingInputRowHeightPx"
        )
    }

    fun startVoiceRecording() {
        if (isVoiceRecording || isVoiceRecordTransitioning || voiceRecorder != null) return
        // New recording session must never inherit stale cancel visuals.
        isVoiceCancelAnimating = false
        holdCancelVisualUntilHidden = false
        cancelProgressSeed = 0f
        cancelFrozenElapsedMs = 0L
        if (INPUT_JUMP_LOG_ENABLED) inputJumpLog(
            "startVoiceRecording begin mode=$recordMode state=$recordUiState kb=$isKeyboardVisible emojiBox=${coordinator.isEmojiBoxVisible} " +
                "emojiPicker=$showEmojiPicker panelH=$inputPanelHeightPx normalH=$normalInputRowHeightPx"
        )

        try {
            val voiceDir = File(context.cacheDir, "voice_recordings").apply { mkdirs() }
            val output = File(voiceDir, "voice_${UUID.randomUUID()}.webm")

            val recorder =
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                    MediaRecorder(context)
                } else {
                    @Suppress("DEPRECATION")
                    MediaRecorder()
                }
            recorder.setAudioSource(MediaRecorder.AudioSource.MIC)
            recorder.setOutputFormat(MediaRecorder.OutputFormat.WEBM)
            recorder.setAudioEncoder(MediaRecorder.AudioEncoder.OPUS)
            recorder.setAudioEncodingBitRate(32_000)
            recorder.setAudioSamplingRate(48_000)
            recorder.setOutputFile(output.absolutePath)
            recorder.setMaxDuration(15 * 60 * 1000) // 15 min safety limit
            if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("startVoiceRecording: calling prepare() file=${output.name}")
            recorder.prepare()
            if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("startVoiceRecording: calling start()")
            recorder.start()
            if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("startVoiceRecording: recorder started OK")
            recorder.setOnErrorListener { _, what, extra ->
                if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("MediaRecorder error what=$what extra=$extra")
                stopVoiceRecording(send = false)
            }

            voiceRecorder = recorder
            voiceOutputFile = output
            voiceRecordStartedAtMs = System.currentTimeMillis()
            voiceElapsedMs = 0L
            voiceWaves = emptyList()

            isVoiceRecordTransitioning = true
            val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
            imm.hideSoftInputFromWindow(view.windowToken, 0)
            focusManager.clearFocus(force = true)
            if (showEmojiPicker || coordinator.isEmojiBoxVisible) {
                onToggleEmojiPicker(false)
            }
            if (INPUT_JUMP_LOG_ENABLED) inputJumpLog(
                "startVoiceRecording armed mode=$recordMode state=$recordUiState voice=$isVoiceRecording kb=$isKeyboardVisible " +
                    "emojiBox=${coordinator.isEmojiBoxVisible} transitioning=$isVoiceRecordTransitioning " +
                    "pinBottom=$shouldPinBottomForInput " +
                    "panelH=$inputPanelHeightPx recH=$recordingInputRowHeightPx"
            )

            scope.launch {
                try {
                    repeat(12) {
                        if (!isKeyboardVisible && !coordinator.isEmojiBoxVisible) return@repeat
                        delay(16)
                    }
                    isVoiceRecording = true
                    isVoiceRecordTransitioning = false
                    if (recordUiState == RecordUiState.PRESSING || recordUiState == RecordUiState.IDLE) {
                        setRecordUiState(RecordUiState.RECORDING, "voice-recorder-started")
                    }
                    if (INPUT_JUMP_LOG_ENABLED) inputJumpLog(
                        "startVoiceRecording ui-enter mode=$recordMode state=$recordUiState voice=$isVoiceRecording kb=$isKeyboardVisible " +
                            "emojiBox=${coordinator.isEmojiBoxVisible} transitioning=$isVoiceRecordTransitioning " +
                            "panelH=$inputPanelHeightPx recH=$recordingInputRowHeightPx"
                    )
                } catch (e: Exception) {
                    isVoiceRecordTransitioning = false
                    if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("startVoiceRecording launch failed: ${e.message}")
                }
            }
        } catch (_: Exception) {
            isVoiceRecordTransitioning = false
            stopVoiceRecording(send = false)
            android.widget.Toast.makeText(
                context,
                "Voice recording is not supported on this device",
                android.widget.Toast.LENGTH_SHORT
            ).show()
        }
    }

    fun cancelVoiceRecordingWithAnimation(origin: String) {
        if (isVoiceCancelAnimating) {
            if (INPUT_JUMP_LOG_ENABLED) inputJumpLog(
                "cancelVoiceRecordingWithAnimation already animating origin=$origin " +
                    "voice=$isVoiceRecording recorder=${voiceRecorder != null}"
            )
            if (isVoiceRecording || voiceRecorder != null) {
                stopVoiceRecording(send = false)
            }
            return
        }
        if (!isVoiceRecording && voiceRecorder == null) {
            setRecordUiState(RecordUiState.IDLE, "cancel-no-recorder origin=$origin")
            return
        }
        keepMicGestureCapture = false
        // Freeze current swipe progress so cancel animation never "jumps back"
        // after slideDx is reset by stopVoiceRecording().
        val swipeSnapshot =
            ((-slideDx).coerceAtLeast(0f) / with(density) { 150.dp.toPx() })
                .coerceIn(0f, 1f)
        cancelProgressSeed = maxOf(cancelProgressSeed, swipeSnapshot)
        cancelFrozenElapsedMs =
            if (isVoicePaused && voicePausedElapsedMs > 0L) {
                voicePausedElapsedMs
            } else if (voiceRecordStartedAtMs > 0L) {
                maxOf(voiceElapsedMs, System.currentTimeMillis() - voiceRecordStartedAtMs)
            } else {
                voiceElapsedMs
            }
        holdCancelVisualUntilHidden = true
        isVoiceCancelAnimating = true
        if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("cancelVoiceRecordingWithAnimation start origin=$origin")
        // Stop recorder immediately (off-main) to avoid stuck recording state / ANR on cancel.
        stopVoiceRecording(send = false, preserveCancelAnimation = true)
    }

    fun pauseVoiceRecording() {
        val recorder = voiceRecorder ?: return
        if (!isVoiceRecording || isVoicePaused) return
        if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("pauseVoiceRecording mode=$recordMode state=$recordUiState")
        try {
            recorder.pause()
            isVoicePaused = true
            voicePausedElapsedMs = voiceElapsedMs
            setRecordUiState(RecordUiState.PAUSED, "pause-pressed")
        } catch (e: Exception) {
            if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("pauseVoiceRecording failed: ${e.message}")
        }
    }

    fun resumeVoiceRecording() {
        val recorder = voiceRecorder ?: return
        if (!isVoiceRecording || !isVoicePaused) return
        if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("resumeVoiceRecording mode=$recordMode state=$recordUiState")
        try {
            recorder.resume()
            voiceRecordStartedAtMs = System.currentTimeMillis() - voicePausedElapsedMs
            isVoicePaused = false
            voicePausedElapsedMs = 0L
            setRecordUiState(RecordUiState.LOCKED, "resume-pressed")
        } catch (e: Exception) {
            if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("resumeVoiceRecording failed: ${e.message}")
        }
    }

    if (INPUT_JUMP_LOG_ENABLED) {
        LaunchedEffect(Unit) {
            snapshotFlow {
                val kb = coordinator.keyboardHeight.value.toInt()
                val em = coordinator.emojiHeight.value.toInt()
                val panelY = (inputPanelY * 10f).toInt() / 10f
                val normalY = (normalInputRowY * 10f).toInt() / 10f
                val recY = (recordingInputRowY * 10f).toInt() / 10f
                val pinBottom =
                    isKeyboardVisible ||
                        coordinator.isEmojiBoxVisible ||
                        isVoiceRecordTransitioning ||
                        recordUiState == RecordUiState.PAUSED
                val navPad = hasNativeNavigationBar && !pinBottom
                "mode=$recordMode state=$recordUiState slideDx=${slideDx.toInt()} slideDy=${slideDy.toInt()} " +
                    "voice=$isVoiceRecording kbVis=$isKeyboardVisible kbDp=$kb emojiBox=${coordinator.isEmojiBoxVisible} " +
                    "emojiVisible=$showEmojiPicker emojiDp=$em suppress=$suppressKeyboard " +
                    "voiceTransitioning=$isVoiceRecordTransitioning " +
                    "pinBottom=$pinBottom navPad=$navPad " +
                    "panelH=$inputPanelHeightPx panelY=$panelY normalH=$normalInputRowHeightPx " +
                    "normalY=$normalY recH=$recordingInputRowHeightPx recY=$recY"
            }.distinctUntilChanged().collect { stateLine ->
                inputJumpLog(stateLine)
            }
        }
    }

    val recordAudioPermissionLauncher =
        rememberLauncherForActivityResult(
            contract = ActivityResultContracts.RequestPermission()
        ) { granted ->
            if (granted) {
                if (pendingRecordAfterPermission) {
                    pendingRecordAfterPermission = false
                    setRecordUiState(RecordUiState.RECORDING, "audio-permission-granted")
                    startVoiceRecording()
                }
            } else {
                pendingRecordAfterPermission = false
                setRecordUiState(RecordUiState.IDLE, "audio-permission-denied")
                android.widget.Toast.makeText(
                    context,
                    "Microphone permission is required for voice messages",
                    android.widget.Toast.LENGTH_SHORT
                ).show()
            }
        }

    fun requestVoiceRecordingFromHold(): Boolean {
        if (INPUT_JUMP_LOG_ENABLED) inputJumpLog(
            "requestVoiceRecordingFromHold mode=$recordMode state=$recordUiState voice=$isVoiceRecording kb=$isKeyboardVisible " +
                "emojiBox=${coordinator.isEmojiBoxVisible} ${inputHeightsSnapshot()}"
        )
        val granted =
            ContextCompat.checkSelfPermission(
                context,
                Manifest.permission.RECORD_AUDIO
            ) == PackageManager.PERMISSION_GRANTED
        if (granted) {
            startVoiceRecording()
            return true
        } else {
            pendingRecordAfterPermission = true
            recordAudioPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
            return true
        }
    }

    // Telegram parity:
    // hold=150ms, slide cancel progress based on dynamic distCanMove=min(width*0.35, 140dp),
    // release-cancel threshold alpha<0.45, lock threshold=57dp (only when slide progress>=0.7).
    val holdToRecordDelayMs = 150L
    val preHoldCancelDistancePx = with(density) { 10.dp.toPx() }
    val maxCancelDragDistancePx = with(density) { 140.dp.toPx() }
    val lockDragThresholdPx = with(density) { 57.dp.toPx() }
    val releaseCancelAlphaThreshold = 0.45f
    val slideToCancelLockGate = 0.7f
    val dragSmoothingPrev = 0.7f
    val dragSmoothingNew = 0.3f
    var cancelDragDistancePx by remember { mutableFloatStateOf(maxCancelDragDistancePx) }

    fun resolveCancelDragDistancePx(): Float {
        if (inputPanelWidthPx <= 0) return maxCancelDragDistancePx
        val byWidth = inputPanelWidthPx.toFloat() * 0.35f
        return byWidth.coerceAtMost(maxCancelDragDistancePx).coerceAtLeast(1f)
    }

    fun slideToCancelProgressForDx(dx: Float): Float {
        val distance = cancelDragDistancePx.coerceAtLeast(1f)
        return (1f + dx.coerceAtMost(0f) / distance).coerceIn(0f, 1f)
    }

    var showLockTooltip by remember { mutableStateOf(false) }
    val lockHintPrefs = remember(context) {
        context.getSharedPreferences("chat_input_hints", Context.MODE_PRIVATE)
    }
    var lockHintShownCount by remember {
        mutableIntStateOf(lockHintPrefs.getInt(LOCK_HINT_PREF_KEY, 0))
    }
    var lockHintShownForCurrentRecord by remember { mutableStateOf(false) }

    fun tryStartRecordingForCurrentMode(): Boolean {
        return if (recordMode == RecordMode.VOICE) {
            setRecordUiState(RecordUiState.RECORDING, "hold-threshold-passed")
            requestVoiceRecordingFromHold()
        } else {
            setRecordUiState(RecordUiState.IDLE, "video-mode-record-not-ready")
            android.widget.Toast.makeText(
                context,
                "Video circles recording will be enabled in next step",
                android.widget.Toast.LENGTH_SHORT
            ).show()
            false
        }
    }

    LaunchedEffect(isVoiceRecording, voiceRecorder, isVoicePaused) {
        if (!isVoiceRecording || isVoicePaused) return@LaunchedEffect
        while (isVoiceRecording && voiceRecorder != null && !isVoicePaused) {
            if (voiceRecordStartedAtMs > 0L) {
                voiceElapsedMs =
                    (System.currentTimeMillis() - voiceRecordStartedAtMs).coerceAtLeast(0L)
            }
            delay(100)
        }
    }

    LaunchedEffect(isVoiceRecording, voiceRecorder, isVoicePaused) {
        if (!isVoiceRecording || isVoicePaused) return@LaunchedEffect
        while (isVoiceRecording && voiceRecorder != null && !isVoicePaused) {
            val amplitude = runCatching { voiceRecorder?.maxAmplitude ?: 0 }.getOrDefault(0)
            val normalized = (amplitude.toFloat() / 32_767f).coerceIn(0f, 1f)
            voiceWaves = (voiceWaves + normalized).takeLast(120)
            delay(90)
        }
    }

    val liveSlideToCancelProgress = slideToCancelProgressForDx(slideDx)

    LaunchedEffect(recordUiState) {
        if (recordUiState != RecordUiState.RECORDING) {
            showLockTooltip = false
            lockHintShownForCurrentRecord = false
        }
    }

    LaunchedEffect(
        recordUiState,
        liveSlideToCancelProgress,
        isLockTransitioning,
        isVoiceCancelAnimating,
        lockHintShownCount
    ) {
        val canShowTooltipNow =
            recordUiState == RecordUiState.RECORDING &&
                !isLockTransitioning &&
                !isVoiceCancelAnimating &&
                liveSlideToCancelProgress >= 0.8f &&
                lockHintShownCount < LOCK_HINT_MAX_SHOWS
        if (!canShowTooltipNow) {
            showLockTooltip = false
            return@LaunchedEffect
        }

        delay(200)
        val canStillShowTooltip =
            recordUiState == RecordUiState.RECORDING &&
                !isLockTransitioning &&
                !isVoiceCancelAnimating &&
                slideToCancelProgressForDx(slideDx) >= 0.8f &&
                lockHintShownCount < LOCK_HINT_MAX_SHOWS
        if (!canStillShowTooltip) return@LaunchedEffect

        showLockTooltip = true
        if (!lockHintShownForCurrentRecord) {
            lockHintShownForCurrentRecord = true
            val newCount = (lockHintShownCount + 1).coerceAtMost(LOCK_HINT_MAX_SHOWS)
            lockHintShownCount = newCount
            lockHintPrefs.edit().putInt(LOCK_HINT_PREF_KEY, newCount).apply()
        }
    }

    val lockTranslateTransitionProgress by animateFloatAsState(
        targetValue = if (isLockTransitioning) 1f else 0f,
        animationSpec = tween(durationMillis = 350, delayMillis = 100, easing = LinearOutSlowInEasing),
        label = "lock_translate_transition_progress"
    )

    val lockSlideTransitionProgress by animateFloatAsState(
        targetValue = if (isLockTransitioning) 1f else 0f,
        animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing),
        label = "lock_slide_transition_progress"
    )

    LaunchedEffect(isLockTransitioning, lockTranslateTransitionProgress) {
        if (!isLockTransitioning) return@LaunchedEffect
        if (lockTranslateTransitionProgress >= 0.999f) {
            isLockTransitioning = false
            rawSlideDx = 0f
            rawSlideDy = 0f
            slideDx = 0f
            slideDy = 0f
            lockProgress = 0f
        }
    }

    // Deterministic cancel commit: after animation, always finalize recording stop.
    LaunchedEffect(isVoiceCancelAnimating) {
        if (!isVoiceCancelAnimating) return@LaunchedEffect
        if (INPUT_JUMP_LOG_ENABLED) inputJumpLog(
            "cancel animation commit scheduled voice=$isVoiceRecording recorder=${voiceRecorder != null}"
        )
        delay(220)
        if (!isVoiceCancelAnimating) return@LaunchedEffect
        if (isVoiceRecording || voiceRecorder != null) {
            if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("cancel animation commit -> stopVoiceRecording(send=false)")
            stopVoiceRecording(send = false)
        } else {
            if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("cancel animation commit -> no recorder, reset UI")
            isVoiceCancelAnimating = false
            keepMicGestureCapture = false
            setRecordUiState(RecordUiState.IDLE, "cancel-commit-no-recorder")
            resetGestureState()
        }
    }

    // Safety guard: never allow cancel animation flag to stick and block UI.
    LaunchedEffect(isVoiceCancelAnimating) {
        if (!isVoiceCancelAnimating) return@LaunchedEffect
        delay(1300)
        if (isVoiceCancelAnimating) {
            if (INPUT_JUMP_LOG_ENABLED) inputJumpLog(
                "cancel animation watchdog: force-finish voice=$isVoiceRecording recorder=${voiceRecorder != null}"
            )
            if (isVoiceRecording || voiceRecorder != null) {
                stopVoiceRecording(send = false)
            } else {
                isVoiceCancelAnimating = false
                keepMicGestureCapture = false
                setRecordUiState(RecordUiState.IDLE, "cancel-watchdog-reset")
                resetGestureState()
            }
        }
    }

    DisposableEffect(Unit) {
        onDispose {
            pendingRecordAfterPermission = false
            isVoiceRecordTransitioning = false
            isVoiceCancelAnimating = false
            holdCancelVisualUntilHidden = false
            cancelProgressSeed = 0f
            cancelFrozenElapsedMs = 0L
            keepMicGestureCapture = false
            resetGestureState()
            if (isVoiceRecording || voiceRecorder != null) {
                stopVoiceRecording(send = false)
            } else {
                setRecordUiState(RecordUiState.IDLE, "dispose")
            }
        }
    }

    val canSend = remember(value, hasReply) { value.isNotBlank() || hasReply }
    var isSending by remember { mutableStateOf(false) }
    var showForwardCancelDialog by remember { mutableStateOf(false) }
    var forwardCancelDialogCount by remember { mutableIntStateOf(0) }
    var forwardCancelDialogMessages by remember {
        mutableStateOf<List<ChatViewModel.ReplyMessage>>(emptyList())
    }
    val mentionPattern = remember { Regex("@([\\w\\d_]*)$") }
    val mentionedPattern = remember { Regex("@([\\w\\d_]{1,})") }
    val skipHeavyInputAnalysis = remember(value) { value.length > LARGE_INPUT_ANALYSIS_THRESHOLD }
    val mentionMatch =
        remember(value, isGroupChat, skipHeavyInputAnalysis) {
            if (isGroupChat && !skipHeavyInputAnalysis) mentionPattern.find(value) else null
        }

    val mentionQuery =
        remember(mentionMatch) {
            mentionMatch?.groupValues?.getOrNull(1)?.lowercase(Locale.ROOT).orEmpty()
        }

    val shouldShowMentionSuggestions = remember(mentionMatch, isGroupChat) { isGroupChat && mentionMatch != null }

    val mentionSuggestions =
        remember(
            value,
            mentionCandidates,
            mentionQuery,
            shouldShowMentionSuggestions,
            skipHeavyInputAnalysis
        ) {
            if (!shouldShowMentionSuggestions || skipHeavyInputAnalysis) {
                emptyList()
            } else {
                val mentionedInText =
                    mentionedPattern
                        .findAll(value)
                        .mapNotNull { match ->
                            match.groupValues.getOrNull(1)?.takeIf { it.isNotBlank() }?.lowercase(Locale.ROOT)
                        }
                        .toSet()

                val result = mutableListOf<MentionCandidate>()

                mentionCandidates.forEach { candidate ->
                    val normalized = candidate.username.trim().trimStart('@').lowercase(Locale.ROOT)
                    if (normalized.isBlank()) return@forEach
                    if (!normalized.startsWith(mentionQuery, ignoreCase = true)) return@forEach
                    if (mentionedInText.contains(normalized)) return@forEach
                    if (result.any { it.username.equals(normalized, ignoreCase = true) }) return@forEach
                    result.add(candidate.copy(username = normalized))
                }
                result
            }
        }

    val emojiWordMatch =
        remember(value, selectionStart, selectionEnd, skipHeavyInputAnalysis) {
            if (skipHeavyInputAnalysis) {
                null
            } else {
                EmojiKeywordSuggester.findWordAtCursor(value, selectionStart, selectionEnd)
            }
        }

    val emojiSuggestions =
        remember(emojiWordMatch) {
            emojiWordMatch?.let { EmojiKeywordSuggester.suggest(it) }.orEmpty()
        }

    val shouldShowEmojiSuggestions =
        remember(emojiSuggestions, mentionSuggestions, isGroupChat, suppressKeyboard, showEmojiPicker) {
            emojiSuggestions.isNotEmpty() &&
                mentionSuggestions.isEmpty() &&
                !(isGroupChat && shouldShowMentionSuggestions) &&
                !showEmojiPicker &&
                !suppressKeyboard
        }

    // Close keyboard when user is blocked
    LaunchedEffect(isBlocked) {
        if (isBlocked) {
            val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
            imm.hideSoftInputFromWindow(view.windowToken, 0)
            focusManager.clearFocus(force = true)
            if (isVoiceRecording) {
                stopVoiceRecording(send = false)
            }
        }
    }

    fun hideKeyboardCompletely() {
        val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        imm.hideSoftInputFromWindow(view.windowToken, 0)
        focusManager.clearFocus(force = true)
    }

    fun toggleEmojiPicker() {
        if (suppressKeyboard || isVoiceRecording) return

        val currentTime = System.currentTimeMillis()
        val timeSinceLastToggle = currentTime - lastToggleTime

        if (timeSinceLastToggle < toggleCooldownMs) {
            return
        }

        lastToggleTime = currentTime
        val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager

        if (coordinator.isEmojiVisible) {
            // EMOJI → KEYBOARD
            coordinator.requestShowKeyboard(
                showKeyboard = {
                    editTextView?.let { editText ->
                        editText.requestFocus()
                        @Suppress("DEPRECATION")
                        imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
                    }
                },
                hideEmoji = { onToggleEmojiPicker(false) }
            )
        } else if (coordinator.isKeyboardVisible || coordinator.keyboardHeight > 0.dp) {
            // KEYBOARD → EMOJI (клавиатура открыта)
            coordinator.requestShowEmoji(
                hideKeyboard = { imm.hideSoftInputFromWindow(view.windowToken, 0) },
                showEmoji = { onToggleEmojiPicker(true) }
            )
        } else {
            // Клавиатура НЕ открыта → открываем emoji напрямую
            // Устанавливаем высоту из сохранённой или дефолт
            if (coordinator.emojiHeight == 0.dp) {
                val savedPx = com.rosetta.messenger.ui.components.KeyboardHeightProvider
                    .getSavedKeyboardHeight(context)
                if (savedPx > 0) {
                    coordinator.emojiHeight = with(density) { savedPx.toDp() }
                } else {
                    coordinator.emojiHeight = 300.dp // разумный дефолт
                }
            }
            coordinator.isEmojiBoxVisible = true
            coordinator.openEmojiOnly(
                showEmoji = { onToggleEmojiPicker(true) }
            )
        }
    }

    fun handleSend() {
        if (isVoiceRecording) {
            stopVoiceRecording(send = true)
            return
        }
        if (value.isNotBlank() || hasReply) {
            isSending = true
            onSend()
            scope.launch {
                delay(150)
                isSending = false
            }
        }
    }

    fun onSelectMention(mention: MentionCandidate) {
        val mentionToken = "@${mention.username} "
        val updated =
            if (mentionPattern.containsMatchIn(value)) {
                mentionPattern.replace(value, mentionToken)
            } else {
                "$value$mentionToken"
            }
        onValueChange(updated)
        editTextView?.requestFocus()
    }

    fun onSelectEmojiSuggestion(emoji: String) {
        val updated = EmojiKeywordSuggester.applySuggestion(value, selectionStart, selectionEnd, emoji)
        if (updated != value) {
            onValueChange(updated)
        }
        editTextView?.requestFocus()
    }

    Column(modifier = Modifier.fillMaxWidth().graphicsLayer { clip = false }) {
        if (isBlocked) {
            // BLOCKED CHAT FOOTER
            Column(modifier = Modifier.fillMaxWidth().background(backgroundColor)) {
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(0.5.dp)
                        .background(
                            if (isDarkTheme) Color.White.copy(alpha = 0.1f)
                            else Color.Black.copy(alpha = 0.08f)
                        )
                )

                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(horizontal = 12.dp, vertical = 16.dp)
                        .padding(bottom = 20.dp)
                        .then(
                            if (hasNativeNavigationBar) Modifier.navigationBarsPadding()
                            else Modifier
                        ),
                    verticalAlignment = Alignment.CenterVertically,
                    horizontalArrangement = Arrangement.Center
                ) {
                    Icon(
                        painter = TelegramIcons.Block,
                        contentDescription = null,
                        tint = Color(0xFFFF6B6B),
                        modifier = Modifier.size(20.dp)
                    )
                    Spacer(modifier = Modifier.width(8.dp))
                    Text(
                        text = "You need to unblock user to send messages.",
                        fontSize = 14.sp,
                        color = secondaryTextColor,
                        textAlign = androidx.compose.ui.text.style.TextAlign.Center
                    )
                }
            }
        } else {
            Column(modifier = Modifier.fillMaxWidth().graphicsLayer { clip = false }) {
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(0.5.dp)
                        .background(
                            if (isDarkTheme) Color.White.copy(alpha = 0.1f)
                            else Color.Black.copy(alpha = 0.08f)
                        )
                )

                Column(
                    modifier = Modifier
                        .fillMaxWidth()
                        .background(color = backgroundColor)
                        .padding(
                            bottom = if (shouldPinBottomForInput) 0.dp else 16.dp
                        )
                        .then(if (shouldAddNavBarPadding) Modifier.navigationBarsPadding() else Modifier)
                        .onGloballyPositioned { coordinates ->
                            inputPanelHeightPx = coordinates.size.height
                            inputPanelWidthPx = coordinates.size.width
                            inputPanelY = coordinates.positionInWindow().y
                        }
                ) {
                    AnimatedVisibility(
                        visible = mentionSuggestions.isNotEmpty(),
                        enter = fadeIn(animationSpec = tween(120)) + expandVertically(animationSpec = tween(120)),
                        exit = fadeOut(animationSpec = tween(100)) + shrinkVertically(animationSpec = tween(100))
                    ) {
                        val mentionCardShape = RoundedCornerShape(14.dp)
                        val mentionCardColor = if (isDarkTheme) Color(0xFF272829) else Color.White
                        val mentionBorderColor =
                            if (isDarkTheme) Color(0xFF1C1D1F) else Color(0xFFF0F0F2)
                        val mentionDividerColor =
                            if (isDarkTheme) Color(0xFF1C1D1F) else Color(0xFFF5F5F5)
                        val mentionPrimaryText = if (isDarkTheme) Color.White else Color(0xFF222222)
                        val mentionSecondaryText = if (isDarkTheme) Color.White.copy(alpha = 0.47f) else Color(0xFF676B70)

                        Box(
                            modifier =
                                Modifier
                                    .fillMaxWidth()
                                    .padding(horizontal = 10.dp, vertical = 5.dp)
                                    .shadow(
                                        elevation = if (isDarkTheme) 6.dp else 3.dp,
                                        shape = mentionCardShape,
                                        clip = false
                                    )
                                    .clip(mentionCardShape)
                                    .background(color = mentionCardColor)
                                    .border(
                                        width = 1.dp,
                                        color = mentionBorderColor,
                                        shape = mentionCardShape
                                    )
                        ) {
                            LazyColumn(
                                modifier =
                                    Modifier
                                        .fillMaxWidth()
                                        .heightIn(max = 196.dp)
                                        .padding(vertical = 2.dp)
                            ) {
                                items(
                                    mentionSuggestions,
                                    key = { candidate ->
                                        if (candidate.publicKey.isNotBlank()) candidate.publicKey else candidate.username
                                    }
                                ) { candidate ->
                                    Column {
                                        Row(
                                            modifier =
                                                Modifier
                                                    .fillMaxWidth()
                                                    .clickable { onSelectMention(candidate) }
                                                    .padding(horizontal = 10.dp, vertical = 7.dp),
                                            verticalAlignment = Alignment.CenterVertically
                                        ) {
                                            if (candidate.publicKey.isNotBlank()) {
                                                AvatarImage(
                                                    publicKey = candidate.publicKey,
                                                    avatarRepository = avatarRepository,
                                                    size = 34.dp,
                                                    isDarkTheme = isDarkTheme,
                                                    displayName = candidate.title
                                                )
                                            } else {
                                                val fallbackColor = PrimaryBlue
                                                val fallbackText =
                                                    candidate.title.firstOrNull()?.uppercaseChar()?.toString() ?: "A"
                                                Box(
                                                    modifier =
                                                        Modifier
                                                            .size(34.dp)
                                                            .clip(CircleShape)
                                                            .background(fallbackColor),
                                                    contentAlignment = Alignment.Center
                                                ) {
                                                    Text(
                                                        text = fallbackText,
                                                        color = Color.White,
                                                        fontSize = 12.sp,
                                                        fontWeight = FontWeight.Bold
                                                    )
                                                }
                                            }

                                            Spacer(modifier = Modifier.width(9.dp))

                                            val titleText = candidate.title.ifBlank { candidate.username }
                                            val usernameText = "@${candidate.username}"
                                            val showInlineUsername = candidate.username.isNotBlank()

                                            Row(
                                                modifier = Modifier.weight(1f),
                                                verticalAlignment = Alignment.CenterVertically
                                            ) {
                                                Text(
                                                    text = titleText,
                                                    fontSize = 14.sp,
                                                    fontWeight = FontWeight.Medium,
                                                    color = mentionPrimaryText,
                                                    maxLines = 1,
                                                    overflow = TextOverflow.Ellipsis,
                                                    modifier = Modifier.weight(1f, fill = false)
                                                )
                                                if (showInlineUsername) {
                                                    Spacer(modifier = Modifier.width(8.dp))
                                                    Text(
                                                        text = usernameText,
                                                        fontSize = 13.sp,
                                                        color = mentionSecondaryText,
                                                        maxLines = 1,
                                                        overflow = TextOverflow.Ellipsis,
                                                        modifier = Modifier.alpha(0.95f)
                                                    )
                                                }
                                            }
                                        }

                                        if (candidate != mentionSuggestions.last()) {
                                            Box(
                                                modifier =
                                                    Modifier
                                                        .fillMaxWidth()
                                                        .padding(start = 52.dp)
                                                        .height(0.5.dp)
                                                        .background(mentionDividerColor)
                                            )
                                        }
                                    }
                                }
                            }
                        }
                    }

                    // REPLY PANEL
                    AnimatedVisibility(
                        visible = hasReply,
                        enter = fadeIn(animationSpec = tween(200, easing = FastOutSlowInEasing)) +
                                expandVertically(
                                    animationSpec = spring(
                                        dampingRatio = Spring.DampingRatioMediumBouncy,
                                        stiffness = Spring.StiffnessMedium
                                    ),
                                    expandFrom = Alignment.Bottom
                                ),
                        exit = fadeOut(animationSpec = tween(150, easing = FastOutLinearInEasing)) +
                                shrinkVertically(
                                    animationSpec = tween(150, easing = FastOutLinearInEasing),
                                    shrinkTowards = Alignment.Bottom
                                )
                    ) {
                        val panelReplyMessages =
                            if (liveReplyMessages.isNotEmpty()) liveReplyMessages
                            else animatedReplyMessages
                        val panelIsForwardMode =
                            if (liveReplyMessages.isNotEmpty()) isForwardMode
                            else animatedForwardMode

                        Row(
                            modifier = Modifier
                                .fillMaxWidth()
                                .clickable {
                                    if (panelReplyMessages.isNotEmpty()) {
                                        onReplyClick(panelReplyMessages.first().messageId)
                                    }
                                }
                                .background(backgroundColor)
                                .padding(horizontal = 12.dp, vertical = 8.dp),
                            verticalAlignment = Alignment.CenterVertically
                        ) {
                            Box(
                                modifier = Modifier
                                    .width(3.dp)
                                    .height(32.dp)
                                    .background(PrimaryBlue, androidx.compose.foundation.shape.RoundedCornerShape(1.5.dp))
                            )
                            Spacer(modifier = Modifier.width(10.dp))
                            Column(modifier = Modifier.weight(1f)) {
                                val replyTargetName =
                                    panelReplyMessages.firstOrNull()?.let { reply ->
                                        if (reply.isOutgoing) {
                                            "You"
                                        } else {
                                            reply.senderName.ifBlank { chatTitle }
                                        }
                                    } ?: "You"
                                Text(
                                    text = if (panelIsForwardMode)
                                        "Forward message${if (panelReplyMessages.size > 1) "s" else ""}"
                                    else
                                        "Reply to $replyTargetName",
                                    fontSize = 13.sp,
                                    fontWeight = FontWeight.SemiBold,
                                    color = PrimaryBlue,
                                    maxLines = 1
                                )
                                Spacer(modifier = Modifier.height(2.dp))
                                if (panelReplyMessages.isNotEmpty()) {
                                    val msg = panelReplyMessages.first()
                                    val hasImageAttachment = msg.attachments.any {
                                        it.type == AttachmentType.IMAGE
                                    }
                                    val hasCallAttachment = msg.attachments.any {
                                        it.type == AttachmentType.CALL
                                    }
                                    val hasVoiceAttachment = msg.attachments.any {
                                        it.type == AttachmentType.VOICE
                                    }
                                    val hasVideoCircleAttachment = msg.attachments.any {
                                        it.type == AttachmentType.VIDEO_CIRCLE
                                    }
                                    AppleEmojiText(
                                        text = if (panelReplyMessages.size == 1) {
                                            if (msg.text.isEmpty() && hasVoiceAttachment) {
                                                "Voice Message"
                                            } else if (msg.text.isEmpty() && hasVideoCircleAttachment) {
                                                "Video Message"
                                            } else if (msg.text.isEmpty() && hasCallAttachment) {
                                                "Call"
                                            } else if (msg.text.isEmpty() && hasImageAttachment) {
                                                "Photo"
                                            } else {
                                                truncateEmojiSafe(msg.text, 80)
                                            }
                                        } else "${panelReplyMessages.size} messages",
                                        fontSize = 13.sp,
                                        color = if (isDarkTheme) Color.White.copy(alpha = 0.6f)
                                        else Color.Black.copy(alpha = 0.5f),
                                        maxLines = 1,
                                        overflow = android.text.TextUtils.TruncateAt.END,
                                        enableLinks = false
                                    )
                                }
                            }
                            
                            // 🔥 Превью изображения (как в Telegram)
                            if (panelReplyMessages.size == 1) {
                                val msg = panelReplyMessages.first()
                                val imageAttachment = msg.attachments.find { 
                                    it.type == AttachmentType.IMAGE
                                }
                                if (imageAttachment != null) {
                                    Spacer(modifier = Modifier.width(8.dp))
                                    ReplyImagePreview(
                                        attachment = imageAttachment,
                                        modifier = Modifier.size(36.dp),
                                        senderPublicKey = msg.publicKey.ifEmpty {
                                            if (msg.isOutgoing) myPublicKey else opponentPublicKey
                                        },
                                        recipientPrivateKey = myPrivateKey
                                    )
                                    Spacer(modifier = Modifier.width(4.dp))
                                }
                            }
                            
                            Box(
                                modifier = Modifier
                                    .size(32.dp)
                                    .clickable(
                                        interactionSource = remember { MutableInteractionSource() },
                                        indication = null,
                                        onClick = {
                                            if (panelIsForwardMode && panelReplyMessages.isNotEmpty()) {
                                                val sourceMessages =
                                                    if (replyMessages.isNotEmpty()) replyMessages
                                                    else panelReplyMessages
                                                forwardCancelDialogCount = sourceMessages.size
                                                forwardCancelDialogMessages = sourceMessages
                                                showForwardCancelDialog = true
                                            } else {
                                                onCloseReply()
                                            }
                                        }
                                    ),
                                contentAlignment = Alignment.Center
                            ) {
                                Icon(
                                    painter = TelegramIcons.Close,
                                    contentDescription = "Cancel",
                                    tint = if (isDarkTheme) Color.White.copy(alpha = 0.5f)
                                    else Color.Black.copy(alpha = 0.4f),
                                    modifier = Modifier.size(18.dp)
                                )
                            }
                        }
                    }

                    // INPUT ROW + Telegram-like flat emoji suggestion strip overlay
                    Box(
                        modifier = Modifier
                            .fillMaxWidth()
                            .graphicsLayer { clip = false }
                    ) {
                        androidx.compose.animation.AnimatedVisibility(
                            visible = shouldShowEmojiSuggestions,
                            enter = fadeIn(animationSpec = tween(120)) +
                                scaleIn(animationSpec = tween(150), initialScale = 0.92f,
                                    transformOrigin = TransformOrigin(0.5f, 1f)),
                            exit = fadeOut(animationSpec = tween(90)) +
                                scaleOut(animationSpec = tween(90), targetScale = 0.92f,
                                    transformOrigin = TransformOrigin(0.5f, 1f)),
                            modifier = Modifier
                                .align(Alignment.TopStart)
                                .offset(y = (-52).dp)
                                .fillMaxWidth()
                                .padding(horizontal = 8.dp)
                                .zIndex(4f)
                        ) {
                            val barBackground = if (isDarkTheme) Color(0xFF2B2B2F) else Color.White

                            Surface(
                                modifier = Modifier.fillMaxWidth(),
                                color = barBackground,
                                shape = RoundedCornerShape(20.dp),
                                shadowElevation = 8.dp,
                                tonalElevation = 0.dp
                            ) {
                                Row(
                                    modifier = Modifier
                                        .fillMaxWidth()
                                        .padding(horizontal = 6.dp, vertical = 6.dp),
                                    verticalAlignment = Alignment.CenterVertically
                                ) {
                                    // Smile icon — opens emoji keyboard
                                    Box(
                                        modifier = Modifier
                                            .size(44.dp)
                                            .clip(CircleShape)
                                            .background(
                                                if (isDarkTheme) Color.White.copy(alpha = 0.08f)
                                                else Color(0xFFF0F0F5)
                                            )
                                            .clickable(
                                                interactionSource = remember { MutableInteractionSource() },
                                                indication = null
                                            ) { toggleEmojiPicker() },
                                        contentAlignment = Alignment.Center
                                    ) {
                                        Icon(
                                            painter = TelegramIcons.Smile,
                                            contentDescription = null,
                                            tint = if (isDarkTheme) Color.White.copy(alpha = 0.5f) else Color(0xFF9E9EA6),
                                            modifier = Modifier.size(22.dp)
                                        )
                                    }

                                    Spacer(modifier = Modifier.width(2.dp))

                                    // Emoji row
                                    LazyRow(
                                        modifier = Modifier.weight(1f),
                                        horizontalArrangement = Arrangement.spacedBy(0.dp),
                                        verticalAlignment = Alignment.CenterVertically
                                    ) {
                                        items(emojiSuggestions, key = { it }) { emoji ->
                                            var pressed by remember { mutableStateOf(false) }
                                            val scale by animateFloatAsState(
                                                targetValue = if (pressed) 0.78f else 1f,
                                                animationSpec = tween(if (pressed) 80 else 200)
                                            )
                                            Box(
                                                modifier = Modifier
                                                    .size(44.dp)
                                                    .graphicsLayer {
                                                        scaleX = scale
                                                        scaleY = scale
                                                    }
                                                    .clip(RoundedCornerShape(12.dp))
                                                    .pointerInput(emoji) {
                                                        detectTapGestures(
                                                            onPress = {
                                                                pressed = true
                                                                tryAwaitRelease()
                                                                pressed = false
                                                            },
                                                            onTap = { onSelectEmojiSuggestion(emoji) }
                                                        )
                                                    },
                                                contentAlignment = Alignment.Center
                                            ) {
                                                AppleEmojiText(
                                                    text = emoji,
                                                    fontSize = 26.sp,
                                                    color = if (isDarkTheme) Color.White else Color.Black,
                                                    maxLines = 1,
                                                    enableLinks = false,
                                                    minHeightMultiplier = 1f,
                                                    onClick = { onSelectEmojiSuggestion(emoji) }
                                                )
                                            }
                                        }
                                    }
                                }
                            }
                        }

                        // ── Recording panel (layered architecture) ──
                        // Layer 1: panel bar (timer + center content)
                        // Layer 2: mic/send circle OVERLAY at right edge (extends beyond panel)
                        // Layer 3: lock icon ABOVE circle (extends above panel)
                        val isRecordingPanelVisible = isVoiceRecording || isVoiceCancelAnimating
                        val recordingPanelTransitionState =
                            remember { MutableTransitionState(false) }.apply {
                                targetState = isRecordingPanelVisible
                            }
                        // True while visible OR while enter/exit animation is still running.
                            val isRecordingPanelComposed =
                            recordingPanelTransitionState.currentState || recordingPanelTransitionState.targetState
                        LaunchedEffect(isRecordingPanelComposed) {
                            if (!isRecordingPanelComposed) {
                                holdCancelVisualUntilHidden = false
                                cancelProgressSeed = 0f
                                cancelFrozenElapsedMs = 0L
                            }
                        }
                        androidx.compose.animation.AnimatedVisibility(
                            modifier = Modifier.graphicsLayer { clip = false },
                            visibleState = recordingPanelTransitionState,
                            // Telegram-like smooth dissolve without any vertical resize.
                            enter = fadeIn(tween(durationMillis = 170, easing = LinearOutSlowInEasing)),
                            exit = fadeOut(tween(durationMillis = 210, easing = FastOutLinearInEasing))
                        ) {
                            val recordingPanelColor =
                                if (isDarkTheme) Color(0xFF1A2A3A) else Color(0xFFE8F2FD)
                            val recordingTextColor =
                                if (isDarkTheme) Color.White.copy(alpha = 0.92f) else Color(0xFF1E2A37)
                            val baseInputRowHeightDp =
                                if (normalInputRowHeightPx > 0) with(density) { normalInputRowHeightPx.toDp() } else 56.dp
                            val fixedRecordingRowHeightDp = baseInputRowHeightDp.coerceAtLeast(56.dp)
                            // Keep layout height equal to normal input row.
                            // The button "grows" visually via scale, not via measured size.
                            val recordingActionButtonBaseSize = 40.dp
                            // Telegram-like proportions: 40dp base grows to ~57dp visually.
                            val recordingActionVisualScaleMax = 1.42f
                            val recordingActionInset = 34.dp
                            // Keep scaled circle safely inside screen bounds (right/bottom).
                            val recordingActionVisualOverflow =
                                (recordingActionButtonBaseSize * (recordingActionVisualScaleMax - 1f)) / 2f
                            val recordingActionOverflowX = -recordingActionVisualOverflow + 2.dp
                            val recordingActionOverflowY = -recordingActionVisualOverflow + 2.dp
                            val voiceLevel = remember(voiceWaves) { voiceWaves.lastOrNull() ?: 0f }
                            val cancelAnimProgress by animateFloatAsState(
                                targetValue = if (isVoiceCancelAnimating || holdCancelVisualUntilHidden) 1f else 0f,
                                animationSpec = tween(durationMillis = 220, easing = FastOutSlowInEasing),
                                label = "voice_cancel_anim"
                            )
                            var recordUiEntered by remember { mutableStateOf(false) }
                            val keepRecordUiVisible =
                                isVoiceRecording || isVoiceCancelAnimating || holdCancelVisualUntilHidden
                            LaunchedEffect(keepRecordUiVisible) {
                                if (keepRecordUiVisible) {
                                    recordUiEntered = false
                                    delay(16)
                                    recordUiEntered = true
                                } else {
                                    recordUiEntered = false
                                }
                            }
                            val recordUiAlpha by animateFloatAsState(
                                targetValue = if (recordUiEntered) 1f else 0f,
                                animationSpec = tween(durationMillis = 180, easing = LinearOutSlowInEasing),
                                label = "record_ui_alpha"
                            )
                            val recordUiShift by animateDpAsState(
                                targetValue = if (recordUiEntered) 0.dp else 20.dp,
                                animationSpec = tween(durationMillis = 180, easing = FastOutLinearInEasing),
                                label = "record_ui_shift"
                            )
                            val recordingActionVisualScale by animateFloatAsState(
                                targetValue = if (recordUiEntered) recordingActionVisualScaleMax else 1f,
                                animationSpec = spring(
                                    dampingRatio = 0.72f,
                                    stiffness = 420f
                                ),
                                label = "record_action_visual_scale"
                            )

                            // ── Telegram-exact recording layout ──
                            // RECORDING: [dot][timer] [◀ Slide to cancel] ... [Circle+Blob]
                            // LOCKED:    [Cancel]     [Timer]             ... [Circle=Send]  + Lock→Pause above
                            // PAUSED:    [Cancel]     [Timer]             ... [Circle=Send]  + Lock→Play above
                            // Timer and dot are HIDDEN in LOCKED/PAUSED (Telegram exact)
                            Box(
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .height(fixedRecordingRowHeightDp)
                                    .padding(horizontal = 12.dp)
                                    .zIndex(2f)
                                    .onGloballyPositioned { coordinates ->
                                        recordingInputRowHeightPx = coordinates.size.height
                                        recordingInputRowY = coordinates.positionInWindow().y
                                    },
                                contentAlignment = Alignment.BottomStart
                            ) {
                                val isLockedOrPaused = recordUiState == RecordUiState.LOCKED || recordUiState == RecordUiState.PAUSED
                                val effectiveSlideDx =
                                    if (isLockTransitioning) {
                                        lerpFloat(lockTransitionStartDx, 0f, lockSlideTransitionProgress)
                                    } else {
                                        slideDx
                                    }
                                val lockDragProgress =
                                    if (isLockTransitioning) {
                                        lerpFloat(lockTransitionStartLockProgress, 0f, lockTranslateTransitionProgress)
                                    } else if (recordUiState == RecordUiState.RECORDING) {
                                        lockProgress.coerceIn(0f, 1f)
                                    } else {
                                        0f
                                    }
                                val lockMoveProgress = (1f - lockDragProgress).coerceIn(0f, 1f)
                                val lockControlsScale by animateFloatAsState(
                                    targetValue =
                                        if (
                                            recordUiState == RecordUiState.RECORDING ||
                                            recordUiState == RecordUiState.LOCKED ||
                                            recordUiState == RecordUiState.PAUSED
                                        ) 1f else 0f,
                                    animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing),
                                    label = "lock_controls_scale"
                                )
                                // Telegram parity:
                                // slideToCancelScale = 0.7 + progress*0.3,
                                // slideDelta = -distCanMove * (1 - progress), where progress in [0..1].
                                val slideDistanceX = effectiveSlideDx.coerceAtMost(0f)
                                val slideToCancelProgress = slideToCancelProgressForDx(slideDistanceX)
                                val circleSlideCancelScale =
                                    if (isVoiceCancelAnimating || holdCancelVisualUntilHidden) {
                                        0.7f * EaseOut.transform(1f - slideToCancelProgress)
                                    } else {
                                        0.7f + slideToCancelProgress * 0.3f
                                    }
                                val circleSlideDelta =
                                    -cancelDragDistancePx.coerceAtLeast(1f) * (1f - slideToCancelProgress)
                                val lockSlideToCancelTarget =
                                    when (recordUiState) {
                                        RecordUiState.RECORDING ->
                                            if (slideToCancelProgress >= slideToCancelLockGate && !isVoiceCancelAnimating) 1f else 0f
                                        RecordUiState.LOCKED,
                                        RecordUiState.PAUSED -> 1f
                                        else -> 0f
                                    }
                                val lockSlideToCancelProgress by animateFloatAsState(
                                    targetValue = lockSlideToCancelTarget,
                                    animationSpec = tween(durationMillis = 140, easing = LinearOutSlowInEasing),
                                    label = "lock_slide_to_cancel_progress"
                                )
                                val lockIdleProgress by rememberInfiniteTransition(label = "lock_idle_offset")
                                    .animateFloat(
                                        initialValue = 0f,
                                        targetValue = 1f,
                                        animationSpec = infiniteRepeatable(
                                            animation = tween(durationMillis = 1200, easing = LinearEasing),
                                            repeatMode = RepeatMode.Reverse
                                        ),
                                        label = "lock_idle_offset_phase"
                                    )

                                // Crossfade between RECORDING panel and LOCKED panel
                                AnimatedContent(
                                    targetState = isLockedOrPaused,
                                    modifier = Modifier
                                        .fillMaxWidth()
                                        .height(40.dp)
                                        .padding(end = recordingActionInset), // keep panel under large circle (Telegram-like overlap)
                                    transitionSpec = {
                                        fadeIn(tween(200)) togetherWith fadeOut(tween(200))
                                    },
                                    label = "record_panel_mode"
                                ) { locked ->
                                    if (locked) {
                                        // ── LOCKED/PAUSED panel (Telegram-like) ──
                                        // No live waveform in input row: show Cancel + Timer.
                                        val lockedTimerMs =
                                            if (isVoicePaused && voicePausedElapsedMs > 0L) {
                                                voicePausedElapsedMs
                                            } else {
                                                voiceElapsedMs
                                            }
                                        Box(
                                            modifier = Modifier
                                                .fillMaxSize()
                                                .clip(RoundedCornerShape(24.dp))
                                                .background(recordingPanelColor)
                                        ) {
                                            Text(
                                                text = formatVoiceRecordTimer(lockedTimerMs),
                                                color = recordingTextColor,
                                                fontSize = 15.sp,
                                                fontWeight = FontWeight.Bold,
                                                modifier = Modifier
                                                    .align(Alignment.CenterStart)
                                                    .padding(start = 14.dp)
                                            )

                                            Box(
                                                modifier = Modifier
                                                    .align(Alignment.Center)
                                                    .height(40.dp)
                                                    .padding(horizontal = 14.dp)
                                                    .clickable(
                                                        interactionSource = remember { MutableInteractionSource() },
                                                        indication = null
                                                    ) {
                                                        if (INPUT_JUMP_LOG_ENABLED) inputJumpLog("tap CANCEL (locked/paused) mode=$recordMode state=$recordUiState")
                                                        stopVoiceRecording(send = false)
                                                    },
                                                contentAlignment = Alignment.Center
                                            ) {
                                                Text(
                                                    text = "CANCEL",
                                                    fontSize = 15.sp,
                                                    fontWeight = FontWeight.Bold,
                                                    color = PrimaryBlue
                                                )
                                            }
                                        }
                                    } else {
                                        // ── RECORDING panel ──
                                        // [attach-slot => dot/trash morph][timer] [◀ Slide to cancel]
                                        val dragCancelProgress =
                                            ((-effectiveSlideDx).coerceAtLeast(0f) / cancelDragDistancePx.coerceAtLeast(1f))
                                                .coerceIn(0f, 1f)
                                        val seededCancelProgress =
                                            if (isVoiceCancelAnimating || holdCancelVisualUntilHidden) {
                                                cancelProgressSeed
                                            } else {
                                                0f
                                            }
                                        val leftDeleteProgress =
                                            maxOf(
                                                cancelAnimProgress,
                                                seededCancelProgress,
                                                FastOutSlowInEasing.transform(
                                                    (dragCancelProgress * 0.85f).coerceIn(0f, 1f)
                                                )
                                            )
                                        val collapseToTrash =
                                            FastOutSlowInEasing.transform(
                                                ((leftDeleteProgress - 0.14f) / 0.86f).coerceIn(0f, 1f)
                                            )
                                        val collapseShiftPx =
                                            with(density) { (-58).dp.toPx() * collapseToTrash }
                                        val collapseScale = 1f - 0.14f * collapseToTrash
                                        val collapseAlpha = 1f - 0.55f * collapseToTrash
                                        // Telegram-like timer -> trash flight.
                                        // Start a little later than trash reveal, then accelerate into bin.
                                        val timerFlyProgress =
                                            FastOutLinearInEasing.transform(
                                                ((leftDeleteProgress - 0.1f) / 0.9f).coerceIn(0f, 1f)
                                            )
                                        // Stop motion at bin and fade out before any overshoot.
                                        val timerReachBinProgress =
                                            FastOutLinearInEasing.transform(
                                                (timerFlyProgress / 0.78f).coerceIn(0f, 1f)
                                            )
                                        val timerToTrashShiftPx =
                                            with(density) { (-46).dp.toPx() } * timerReachBinProgress
                                        val timerToTrashScale = lerpFloat(1f, 0.52f, timerReachBinProgress)
                                        val timerToTrashAlpha = 1f - timerReachBinProgress
                                        val timerSpacerDp = lerpFloat(10f, 2f, collapseToTrash).dp
                                        // Hard-hide trash right after cancel commit to prevent any one-frame reappear.
                                        val hideIndicatorAfterCancelProgress =
                                            if (holdCancelVisualUntilHidden && !isVoiceCancelAnimating) 1f else 0f
                                        val timerDisplayMs =
                                            if (isVoiceCancelAnimating || holdCancelVisualUntilHidden) {
                                                if (cancelFrozenElapsedMs > 0L) cancelFrozenElapsedMs else voiceElapsedMs
                                            } else {
                                                voiceElapsedMs
                                            }
                                        Row(
                                            modifier = Modifier
                                                .fillMaxSize()
                                                .clip(RoundedCornerShape(24.dp))
                                                .background(recordingPanelColor)
                                                .padding(start = 4.dp),
                                            verticalAlignment = Alignment.CenterVertically
                                        ) {
                                            // Left slot (same anchor as attach icon in normal input):
                                            // morphs from recording dot to trash while user cancels.
                                            Box(
                                                modifier = Modifier
                                                    .size(40.dp)
                                                    .graphicsLayer {
                                                        alpha = recordUiAlpha * (1f - hideIndicatorAfterCancelProgress)
                                                        translationX = with(density) { recordUiShift.toPx() }
                                                    },
                                                contentAlignment = Alignment.Center
                                            ) {
                                                TelegramVoiceDeleteIndicator(
                                                    cancelProgress = leftDeleteProgress,
                                                    isDarkTheme = isDarkTheme,
                                                    indicatorSize = 24.dp
                                                )
                                            }

                                            Spacer(modifier = Modifier.width(2.dp))

                                            Text(
                                                text = formatVoiceRecordTimer(timerDisplayMs),
                                                color = recordingTextColor,
                                                fontSize = 15.sp,
                                                fontWeight = FontWeight.Bold,
                                                modifier = Modifier.graphicsLayer {
                                                    alpha =
                                                        recordUiAlpha *
                                                            timerToTrashAlpha *
                                                            collapseAlpha
                                                    translationX =
                                                        with(density) { recordUiShift.toPx() } + timerToTrashShiftPx
                                                    scaleX = collapseScale * timerToTrashScale
                                                    scaleY = collapseScale * timerToTrashScale
                                                }
                                            )

                                            Spacer(modifier = Modifier.width(timerSpacerDp))

                                            // Slide to cancel
                                            SlideToCancel(
                                                slideDx = effectiveSlideDx,
                                                isDarkTheme = isDarkTheme,
                                                modifier = Modifier
                                                    .weight(1f)
                                                    .graphicsLayer {
                                                        alpha =
                                                            recordUiAlpha *
                                                                (1f - leftDeleteProgress) *
                                                                collapseAlpha
                                                        translationX =
                                                            with(density) { recordUiShift.toPx() } + collapseShiftPx
                                                        scaleX = collapseScale
                                                        scaleY = collapseScale
                                                    }
                                            )
                                        }
                                    }
                                }

                                // Lock icon above circle (independent from circle scale/translation)
                                if (recordUiState == RecordUiState.RECORDING ||
                                    recordUiState == RecordUiState.LOCKED ||
                                    recordUiState == RecordUiState.PAUSED
                                ) {
                                    val lockPillHeightDp =
                                        if (recordUiState == RecordUiState.RECORDING) {
                                            36.dp + 14.dp * lockMoveProgress
                                        } else {
                                            36.dp
                                        }
                                    val lockBaseRecordingYPx = with(density) { (-80).dp.toPx() }
                                    val lockDragLiftPx = with(density) { 57.dp.toPx() } * lockDragProgress
                                    val lockIdleOffsetPx =
                                        if (recordUiState == RecordUiState.RECORDING) {
                                            with(density) { (-8).dp.toPx() } * lockMoveProgress * lockIdleProgress
                                        } else {
                                            0f
                                        }
                                    val lockYTrajectoryPx =
                                        if (recordUiState == RecordUiState.RECORDING) {
                                            lockBaseRecordingYPx - lockDragLiftPx + lockIdleOffsetPx
                                        } else {
                                            lockBaseRecordingYPx +
                                                with(density) { 14.dp.toPx() } * lockMoveProgress -
                                                lockDragLiftPx
                                        }
                                    val maxLockTranslationPx = with(density) { 72.dp.toPx() }
                                    val lockTranslationDyPx =
                                        (
                                            maxLockTranslationPx * (1f - lockControlsScale) +
                                                maxLockTranslationPx * (1f - lockSlideToCancelProgress)
                                            ).coerceAtMost(maxLockTranslationPx)
                                    val lockLayerScale =
                                        (lockControlsScale * lockSlideToCancelProgress).coerceIn(0f, 1f)
                                    val canTogglePause = recordUiState == RecordUiState.LOCKED || recordUiState == RecordUiState.PAUSED
                                    LockIcon(
                                        movementProgress = lockMoveProgress,
                                        isLocked = recordUiState == RecordUiState.LOCKED,
                                        isPaused = recordUiState == RecordUiState.PAUSED,
                                        isDarkTheme = isDarkTheme,
                                        modifier = Modifier
                                            .align(Alignment.BottomEnd)
                                            .offset(x = recordingActionOverflowX, y = recordingActionOverflowY)
                                            .width(36.dp)
                                            .height(lockPillHeightDp)
                                            .graphicsLayer {
                                                translationY = lockYTrajectoryPx + lockTranslationDyPx
                                                alpha = lockLayerScale
                                                scaleX = lockLayerScale
                                                scaleY = lockLayerScale
                                                clip = false
                                            }
                                            .clickable(
                                                enabled = canTogglePause,
                                                interactionSource = remember { MutableInteractionSource() },
                                                indication = null
                                            ) {
                                                view.performHapticFeedback(android.view.HapticFeedbackConstants.KEYBOARD_TAP)
                                                if (recordUiState == RecordUiState.PAUSED) {
                                                    resumeVoiceRecording()
                                                } else {
                                                    pauseVoiceRecording()
                                                }
                                            }
                                            .zIndex(10f)
                                    )

                                    if (showLockTooltip &&
                                        recordUiState == RecordUiState.RECORDING &&
                                        lockSlideToCancelProgress > 0.8f
                                    ) {
                                        LockTooltip(
                                            visible = showLockTooltip,
                                            isDarkTheme = isDarkTheme,
                                            modifier = Modifier
                                                .align(Alignment.BottomEnd)
                                                .offset(x = recordingActionOverflowX, y = recordingActionOverflowY)
                                                .graphicsLayer {
                                                    translationX = with(density) { (-90).dp.toPx() }
                                                    translationY = lockYTrajectoryPx + lockTranslationDyPx + with(density) { (-80).dp.toPx() }
                                                    clip = false
                                                }
                                                .zIndex(11f)
                                        )
                                    }
                                }

                                // ── Layer 2: Circle overlay ──
                                Box(
                                    modifier = Modifier
                                        .size(recordingActionButtonBaseSize)
                                        .align(Alignment.BottomEnd)
                                        .offset(x = recordingActionOverflowX, y = recordingActionOverflowY)
                                        .graphicsLayer {
                                            translationX = circleSlideDelta
                                            scaleX = recordingActionVisualScale * circleSlideCancelScale
                                            scaleY = recordingActionVisualScale * circleSlideCancelScale
                                            transformOrigin = TransformOrigin(0.5f, 0.5f)
                                            clip = false
                                        }
                                        .zIndex(5f),
                                    contentAlignment = Alignment.Center
                                ) {
                                    // Blob: visible in RECORDING + LOCKED (and during lock transition)
                                    // so locked state keeps the same "alive" blue bubbles behind the circle.
                                    val showVoiceBlob =
                                        (
                                            recordUiState == RecordUiState.RECORDING ||
                                                recordUiState == RecordUiState.LOCKED ||
                                                recordUiState == RecordUiState.PAUSED ||
                                                isLockTransitioning
                                        ) && !isVoiceCancelAnimating
                                    val blobInLockedMode =
                                        recordUiState == RecordUiState.LOCKED ||
                                            recordUiState == RecordUiState.PAUSED ||
                                            isLockTransitioning
                                    val blobSlideProgress =
                                        if (blobInLockedMode) {
                                            1f
                                        } else {
                                            slideToCancelProgress
                                        }
                                    val blobVoiceLevel =
                                        if (blobInLockedMode) {
                                            maxOf(voiceLevel, 0.26f)
                                        } else {
                                            voiceLevel
                                        }
                                    if (showVoiceBlob) {
                                        VoiceButtonBlob(
                                            voiceLevel = blobVoiceLevel,
                                            slideToCancelProgress = blobSlideProgress,
                                            isDarkTheme = isDarkTheme,
                                            modifier = Modifier
                                                .size(recordingActionButtonBaseSize)
                                                .graphicsLayer {
                                                    scaleX = recordingActionVisualScale * 1.1f
                                                    scaleY = recordingActionVisualScale * 1.1f
                                                    clip = false
                                                }
                                        )
                                    }

                                    // Mic/Send circle — same size as panel height
                                    val sendScale by animateFloatAsState(
                                        targetValue = if (recordUiState == RecordUiState.LOCKED || recordUiState == RecordUiState.PAUSED) 1f else 0f,
                                        animationSpec = tween(durationMillis = 150, easing = FastOutSlowInEasing),
                                        label = "send_btn_scale"
                                    )
                                    if (recordUiState == RecordUiState.LOCKED || recordUiState == RecordUiState.PAUSED) {
                                        Box(
                                            modifier = Modifier
                                                .size(recordingActionButtonBaseSize)
                                                .graphicsLayer {
                                                    scaleX = sendScale * recordingActionVisualScale
                                                    scaleY = sendScale * recordingActionVisualScale
                                                    shadowElevation = 8f
                                                    shape = CircleShape
                                                    clip = false
                                                }
                                                .clip(CircleShape)
                                                .background(PrimaryBlue)
                                                .clickable(
                                                    interactionSource = remember { MutableInteractionSource() },
                                                    indication = null
                                                ) {
                                                    if (INPUT_JUMP_LOG_ENABLED) inputJumpLog(
                                                        "tap SEND (locked/paused) mode=$recordMode state=$recordUiState voice=$isVoiceRecording " +
                                                            "kb=$isKeyboardVisible emojiBox=${coordinator.isEmojiBoxVisible} ${inputHeightsSnapshot()}"
                                                    )
                                                    stopVoiceRecording(send = true)
                                                },
                                            contentAlignment = Alignment.Center
                                        ) {
                                            Icon(
                                                imageVector = TelegramSendIcon,
                                                contentDescription = "Send voice message",
                                                tint = Color.White,
                                                modifier = Modifier.size(20.dp)
                                            )
                                        }
                                    } else {
                                        Box(
                                            modifier = Modifier
                                                .size(recordingActionButtonBaseSize)
                                                .graphicsLayer {
                                                    scaleX = recordingActionVisualScale
                                                    scaleY = recordingActionVisualScale
                                                    shadowElevation = 8f
                                                    shape = CircleShape
                                                    clip = false
                                                }
                                                .clip(CircleShape)
                                                .background(PrimaryBlue),
                                            contentAlignment = Alignment.Center
                                        ) {
                                            Icon(
                                                imageVector = if (recordMode == RecordMode.VOICE) ThinMicrophoneIcon else TablerIcons.Video,
                                                contentDescription = null,
                                                tint = Color.White,
                                                modifier = Modifier.size(19.dp)
                                            )
                                        }
                                    }
                                }
                            }
                        }
                        val gestureCaptureOnly = isRecordingPanelComposed && keepMicGestureCapture
                        if (!isRecordingPanelComposed || keepMicGestureCapture) {
                            Row(
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .then(
                                        if (gestureCaptureOnly) {
                                            Modifier.requiredHeight(0.dp)
                                        } else {
                                            Modifier
                                                .heightIn(min = 48.dp)
                                                .padding(horizontal = 12.dp, vertical = 8.dp)
                                        }
                                    )
                                    .zIndex(1f)
                                    .graphicsLayer {
                                        // Keep gesture layer alive during hold, but never show base input under recording panel.
                                        alpha = if (isRecordingPanelComposed) 0f else 1f
                                    }
                                    .onGloballyPositioned { coordinates ->
                                        normalInputRowHeightPx = coordinates.size.height
                                        normalInputRowY = coordinates.positionInWindow().y
                                    },
                                verticalAlignment = Alignment.Bottom
                            ) {
                                IconButton(
                                    onClick = onAttachClick,
                                    modifier = Modifier.size(40.dp)
                                ) {
                                    Icon(
                                        painter = TelegramIcons.Attach,
                                        contentDescription = "Attach",
                                        tint = if (isDarkTheme) Color(0xFF8E8E93).copy(alpha = 0.6f)
                                        else Color(0xFF8E8E93).copy(alpha = 0.6f),
                                        modifier = Modifier.size(24.dp)
                                    )
                                }

                                Spacer(modifier = Modifier.width(4.dp))

                                Box(
                                    modifier = Modifier
                                        .weight(1f)
                                        .heightIn(min = 40.dp, max = 150.dp)
                                        .background(color = backgroundColor)
                                        .padding(horizontal = 12.dp, vertical = 8.dp),
                                    contentAlignment = Alignment.TopStart
                                ) {
                                    AppleEmojiTextField(
                                        value = value,
                                        onValueChange = { newValue -> onValueChange(newValue) },
                                        textColor = textColor,
                                        textSize = 16f,
                                        hint = "Type message...",
                                        hintColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93),
                                        modifier = Modifier.fillMaxWidth(),
                                        requestFocus = hasReply,
                                        onViewCreated = { view -> editTextView = view },
                                        onFocusChanged = { hasFocus ->
                                            if (hasFocus) {
                                                if (INPUT_JUMP_LOG_ENABLED) inputJumpLog(
                                                    "tap INPUT focus=true voice=$isVoiceRecording kb=$isKeyboardVisible " +
                                                        "emojiBox=${coordinator.isEmojiBoxVisible} ${inputHeightsSnapshot()}"
                                                )
                                            }
                                            if (hasFocus && showEmojiPicker) {
                                                onToggleEmojiPicker(false)
                                            }
                                        },
                                        onSelectionChanged = { start, end ->
                                            selectionStart = start
                                            selectionEnd = end
                                        }
                                    )
                                }

                                Spacer(modifier = Modifier.width(6.dp))

                                IconButton(
                                    onClick = { toggleEmojiPicker() },
                                    modifier = Modifier.size(40.dp)
                                ) {
                                    Icon(
                                        painter = if (showEmojiPicker) TelegramIcons.Keyboard
                                        else TelegramIcons.Smile,
                                        contentDescription = "Emoji",
                                        tint = if (isDarkTheme) Color(0xFF8E8E93).copy(alpha = 0.6f)
                                        else Color(0xFF8E8E93).copy(alpha = 0.6f),
                                        modifier = Modifier.size(24.dp)
                                    )
                                }

                                Spacer(modifier = Modifier.width(2.dp))

                                AnimatedVisibility(
                                    visible = canSend || isSending,
                                    enter = scaleIn(tween(150)) + fadeIn(tween(150)),
                                    exit = scaleOut(tween(100)) + fadeOut(tween(100))
                                ) {
                                    IconButton(
                                        onClick = { handleSend() },
                                        modifier = Modifier.size(40.dp)
                                    ) {
                                        Icon(
                                            imageVector = TelegramSendIcon,
                                            contentDescription = "Send",
                                            tint = PrimaryBlue,
                                            modifier = Modifier.size(24.dp)
                                        )
                                    }
                                }

                                AnimatedVisibility(
                                    visible = !canSend && !isSending,
                                    enter = scaleIn(tween(140)) + fadeIn(tween(140)),
                                    exit = scaleOut(tween(100)) + fadeOut(tween(100))
                                ) {
                                    Box(
                                        modifier =
                                            Modifier
                                                .size(40.dp)
                                                .pointerInput(Unit) {
                                                    awaitEachGesture {
                                                        if (
                                                            canSend ||
                                                            isSending ||
                                                            isVoiceRecording ||
                                                            isVoiceRecordTransitioning ||
                                                            recordUiState == RecordUiState.SENDING ||
                                                            recordUiState == RecordUiState.CANCELLING
                                                        ) {
                                                            return@awaitEachGesture
                                                        }

                                                        val down = awaitFirstDown(requireUnconsumed = false)
                                                        val tapSlopPx = viewConfiguration.touchSlop
                                                        var pointerIsDown = true
                                                        var armingCancelledByMove = false
                                                        var maxAbsDx = 0f
                                                        var maxAbsDy = 0f
                                                        pressStartX = down.position.x
                                                        pressStartY = down.position.y
                                                        cancelDragDistancePx = resolveCancelDragDistancePx()
                                                        keepMicGestureCapture = true
                                                        rawSlideDx = 0f
                                                        rawSlideDy = 0f
                                                        slideDx = 0f
                                                        slideDy = 0f
                                                        dragVelocityX = 0f
                                                        dragVelocityY = 0f
                                                        lastDragDx = 0f
                                                        lastDragDy = 0f
                                                        lastDragEventTimeMs = System.currentTimeMillis()
                                                        didCancelHaptic = false
                                                        didLockHaptic = false
                                                        pendingRecordAfterPermission = false
                                                        setRecordUiState(RecordUiState.PRESSING, "mic-down")
                                                        view.performHapticFeedback(android.view.HapticFeedbackConstants.KEYBOARD_TAP)
                                                        if (INPUT_JUMP_LOG_ENABLED) inputJumpLog(
                                                            "mic DOWN mode=$recordMode state=$recordUiState " +
                                                                "voice=$isVoiceRecording kb=$isKeyboardVisible ${inputHeightsSnapshot()}"
                                                        )

                                                        pendingLongPressJob?.cancel()
                                                        pendingLongPressJob =
                                                            scope.launch {
                                                                delay(holdToRecordDelayMs)
                                                                if (pointerIsDown && recordUiState == RecordUiState.PRESSING) {
                                                                    val started = tryStartRecordingForCurrentMode()
                                                                    if (!started) {
                                                                        resetGestureState()
                                                                        setRecordUiState(RecordUiState.IDLE, "hold-start-failed")
                                                                    }
                                                                }
                                                            }

                                                        fun finalizePointerRelease(
                                                            rawReleaseDx: Float,
                                                            rawReleaseDy: Float,
                                                            source: String
                                                        ) {
                                                            keepMicGestureCapture = false
                                                            pointerIsDown = false
                                                            pendingLongPressJob?.cancel()
                                                            pendingLongPressJob = null
                                                            pendingRecordAfterPermission = false
                                                            when (recordUiState) {
                                                                RecordUiState.PRESSING -> {
                                                                    val movedBeyondTap =
                                                                        maxAbsDx > tapSlopPx || maxAbsDy > tapSlopPx
                                                                    if (!movedBeyondTap) {
                                                                        toggleRecordModeByTap()
                                                                        setRecordUiState(RecordUiState.IDLE, "$source-short-tap-toggle")
                                                                    } else {
                                                                        setRecordUiState(RecordUiState.IDLE, "$source-press-release-after-move")
                                                                    }
                                                                }
                                                                RecordUiState.RECORDING -> {
                                                                    // Telegram parity:
                                                                    // evaluate horizontal cancel by normalized slide progress alpha.
                                                                    var releaseDx = rawReleaseDx.coerceAtMost(0f)
                                                                    var releaseDy = rawReleaseDy.coerceAtMost(0f)
                                                                    if (kotlin.math.abs(releaseDx) > kotlin.math.abs(releaseDy)) {
                                                                        releaseDy = 0f
                                                                    } else {
                                                                        releaseDx = 0f
                                                                    }
                                                                    val releaseAlpha = slideToCancelProgressForDx(releaseDx)
                                                                    val cancelOnRelease =
                                                                        releaseAlpha < releaseCancelAlphaThreshold
                                                                    if (cancelOnRelease && !didCancelHaptic) {
                                                                        didCancelHaptic = true
                                                                        view.performHapticFeedback(android.view.HapticFeedbackConstants.KEYBOARD_TAP)
                                                                    }
                                                                    if (INPUT_JUMP_LOG_ENABLED) inputJumpLog(
                                                                        "$source mode=$recordMode state=$recordUiState releaseDx=${releaseDx.toInt()} " +
                                                                            "releaseDy=${releaseDy.toInt()} vX=${dragVelocityX.toInt()} vY=${dragVelocityY.toInt()} " +
                                                                            "alpha=${"%.2f".format(java.util.Locale.US, releaseAlpha)} cancel=$cancelOnRelease"
                                                                    )
                                                                    if (cancelOnRelease) {
                                                                        if (isVoiceRecording || voiceRecorder != null) {
                                                                            cancelVoiceRecordingWithAnimation("$source-cancel")
                                                                        } else {
                                                                            setRecordUiState(RecordUiState.IDLE, "$source-cancel-without-recorder")
                                                                        }
                                                                    } else {
                                                                        if (isVoiceRecording || voiceRecorder != null) {
                                                                            stopVoiceRecording(send = true)
                                                                        } else {
                                                                            setRecordUiState(RecordUiState.IDLE, "$source-without-recorder")
                                                                        }
                                                                    }
                                                                }
                                                                RecordUiState.LOCKED -> {
                                                                    if (INPUT_JUMP_LOG_ENABLED) inputJumpLog(
                                                                        "$source while LOCKED -> keep recording mode=$recordMode state=$recordUiState"
                                                                    )
                                                                }
                                                                RecordUiState.PAUSED -> {
                                                                    if (INPUT_JUMP_LOG_ENABLED) inputJumpLog(
                                                                        "$source while PAUSED -> stay paused mode=$recordMode state=$recordUiState"
                                                                    )
                                                                }
                                                                RecordUiState.CANCELLING -> {
                                                                    if (INPUT_JUMP_LOG_ENABLED) inputJumpLog(
                                                                        "$source while CANCELLING -> ignore release mode=$recordMode state=$recordUiState"
                                                                    )
                                                                }
                                                                RecordUiState.SENDING -> {
                                                                    if (INPUT_JUMP_LOG_ENABLED) inputJumpLog(
                                                                        "$source while SENDING -> ignore release mode=$recordMode state=$recordUiState"
                                                                    )
                                                                }
                                                                RecordUiState.IDLE -> Unit
                                                            }
                                                            resetGestureState()
                                                        }

                                                        var finished = false
                                                        while (!finished) {
                                                            val event = awaitPointerEvent()
                                                            val trackedChange = event.changes.firstOrNull { it.id == down.id }
                                                            val change = trackedChange
                                                                ?: event.changes.firstOrNull()
                                                                ?: continue
                                                            val allPointersReleased = event.changes.none { it.pressed }
                                                            val releaseDetected =
                                                                change.changedToUpIgnoreConsumed() || !change.pressed ||
                                                                    (trackedChange == null && allPointersReleased)

                                                            if (releaseDetected) {
                                                                val releaseDx =
                                                                    if (trackedChange != null) change.position.x - pressStartX else rawSlideDx
                                                                val releaseDy =
                                                                    if (trackedChange != null) change.position.y - pressStartY else rawSlideDy
                                                                val source =
                                                                    if (trackedChange == null) "mic UP fallback-lost-pointer"
                                                                    else "mic UP"
                                                                finalizePointerRelease(releaseDx, releaseDy, source)
                                                                finished = true
                                                            } else if (recordUiState == RecordUiState.PRESSING) {
                                                                val dx = change.position.x - pressStartX
                                                                val dy = change.position.y - pressStartY
                                                                val absDx = kotlin.math.abs(dx)
                                                                val absDy = kotlin.math.abs(dy)
                                                                if (absDx > maxAbsDx) maxAbsDx = absDx
                                                                if (absDy > maxAbsDy) maxAbsDy = absDy
                                                                val totalDistance = kotlin.math.sqrt(dx * dx + dy * dy)
                                                                if (!armingCancelledByMove && totalDistance > preHoldCancelDistancePx) {
                                                                    armingCancelledByMove = true
                                                                    pendingLongPressJob?.cancel()
                                                                    pendingLongPressJob = null
                                                                    pendingRecordAfterPermission = false
                                                                    keepMicGestureCapture = false
                                                                    setRecordUiState(
                                                                        RecordUiState.IDLE,
                                                                        "pre-hold-move-cancel dist=${totalDistance.toInt()}"
                                                                    )
                                                                }
                                                            } else if (recordUiState == RecordUiState.RECORDING) {
                                                                // Telegram parity:
                                                                // raw drag + smoothed UI drag for horizontal cancel progress
                                                                // and vertical lock threshold.
                                                                val rawDx = (change.position.x - pressStartX).coerceAtMost(0f)
                                                                val rawDy = (change.position.y - pressStartY).coerceAtMost(0f)
                                                                rawSlideDx = rawDx
                                                                rawSlideDy = rawDy

                                                                val nowMs = System.currentTimeMillis()
                                                                val dtMs = (nowMs - lastDragEventTimeMs).coerceAtLeast(1L).toFloat()
                                                                dragVelocityX = ((rawDx - lastDragDx) / dtMs) * 1000f
                                                                dragVelocityY = ((rawDy - lastDragDy) / dtMs) * 1000f
                                                                lastDragDx = rawDx
                                                                lastDragDy = rawDy
                                                                lastDragEventTimeMs = nowMs

                                                                slideDx = (slideDx * dragSmoothingPrev) + (rawDx * dragSmoothingNew)
                                                                slideDy = (slideDy * dragSmoothingPrev) + (rawDy * dragSmoothingNew)
                                                                val slideToCancelProgress = slideToCancelProgressForDx(rawDx)
                                                                val lockDistancePx = (-rawDy).coerceAtLeast(0f)
                                                                lockProgress =
                                                                    if (slideToCancelProgress >= slideToCancelLockGate) {
                                                                        (lockDistancePx / lockDragThresholdPx).coerceIn(0f, 1f)
                                                                    } else {
                                                                        0f
                                                                    }

                                                                if (!didCancelHaptic && slideToCancelProgress < releaseCancelAlphaThreshold) {
                                                                    didCancelHaptic = true
                                                                    view.performHapticFeedback(android.view.HapticFeedbackConstants.KEYBOARD_TAP)
                                                                }
                                                                val lockReached =
                                                                    slideToCancelProgress >= slideToCancelLockGate &&
                                                                        lockDistancePx >= lockDragThresholdPx
                                                                if (lockReached) {
                                                                    if (!didLockHaptic) {
                                                                        didLockHaptic = true
                                                                        view.performHapticFeedback(android.view.HapticFeedbackConstants.KEYBOARD_TAP)
                                                                    }
                                                                    if (INPUT_JUMP_LOG_ENABLED) inputJumpLog(
                                                                        "gesture LOCK dy=${lockDistancePx.toInt()} threshold=${lockDragThresholdPx.toInt()} " +
                                                                            "slideProgress=${"%.2f".format(java.util.Locale.US, slideToCancelProgress)} mode=$recordMode"
                                                                    )
                                                                    keepMicGestureCapture = false
                                                                    pointerIsDown = false
                                                                    startLockTransition("gesture-lock")
                                                                    finished = true
                                                                } else if (slideToCancelProgress <= 0f) {
                                                                    if (INPUT_JUMP_LOG_ENABLED) inputJumpLog(
                                                                        "gesture CANCEL alpha=0.00 dx=${rawDx.toInt()} distCanMove=${cancelDragDistancePx.toInt()} mode=$recordMode"
                                                                    )
                                                                    cancelVoiceRecordingWithAnimation("slide-cancel")
                                                                    finished = true
                                                                }
                                                            }
                                                            change.consume()
                                                        }

                                                        pendingLongPressJob?.cancel()
                                                        pendingLongPressJob = null
                                                        if (recordUiState == RecordUiState.PRESSING) {
                                                            keepMicGestureCapture = false
                                                            setRecordUiState(RecordUiState.IDLE, "gesture-end")
                                                            resetGestureState()
                                                        }
                                                    }
                                                },
                                        contentAlignment = Alignment.Center
                                    ) {
                                        Icon(
                                            imageVector = if (recordMode == RecordMode.VOICE) ThinMicrophoneIcon else TablerIcons.Video,
                                            contentDescription = "Record message",
                                            tint = Color(0xFF8E8E93).copy(alpha = 0.6f),
                                            modifier = Modifier.size(24.dp)
                                        )
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        if (showForwardCancelDialog) {
            val titleText =
                if (forwardCancelDialogCount == 1) "1 message"
                else "$forwardCancelDialogCount messages"
            val bodyText =
                "What would you like to do with $titleText from your chat with ${chatTitle.ifBlank { "this user" }}?"
            val popupInteraction = remember { MutableInteractionSource() }
            val cardInteraction = remember { MutableInteractionSource() }
            val dividerColor = if (isDarkTheme) Color(0xFF3A3A3C) else Color(0xFFE5E5EA)
            val cardColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White
            val bodyColor = if (isDarkTheme) Color(0xFFD1D1D6) else Color(0xFF3C3C43)

            Popup(
                alignment = Alignment.Center,
                onDismissRequest = {
                    showForwardCancelDialog = false
                    forwardCancelDialogMessages = emptyList()
                },
                properties =
                    PopupProperties(
                        focusable = false,
                        dismissOnBackPress = true,
                        dismissOnClickOutside = true,
                        clippingEnabled = false
                    )
            ) {
                Box(
                    modifier =
                        Modifier
                            .fillMaxSize()
                            .background(Color.Black.copy(alpha = 0.36f))
                            .clickable(
                                interactionSource = popupInteraction,
                                indication = null
                            ) { showForwardCancelDialog = false },
                    contentAlignment = Alignment.Center
                ) {
                    Surface(
                        modifier =
                            Modifier
                                .fillMaxWidth(0.88f)
                                .widthIn(max = 360.dp)
                                .clip(RoundedCornerShape(16.dp))
                                .clickable(
                                    interactionSource = cardInteraction,
                                    indication = null
                                ) {},
                        color = cardColor,
                        shadowElevation = 18.dp
                    ) {
                        Column(modifier = Modifier.fillMaxWidth()) {
                            Spacer(modifier = Modifier.height(18.dp))
                            Text(
                                text = titleText,
                                fontSize = 22.sp,
                                fontWeight = FontWeight.Bold,
                                color = if (isDarkTheme) Color.White else Color.Black,
                                modifier = Modifier.padding(horizontal = 20.dp)
                            )
                            Spacer(modifier = Modifier.height(10.dp))
                            Text(
                                text = bodyText,
                                fontSize = 16.sp,
                                lineHeight = 21.sp,
                                color = bodyColor,
                                modifier = Modifier.padding(horizontal = 20.dp)
                            )
                            Spacer(modifier = Modifier.height(14.dp))
                            Divider(thickness = 0.5.dp, color = dividerColor)
                            TextButton(
                                onClick = {
                                    showForwardCancelDialog = false
                                    val panelMessages =
                                        if (replyMessages.isNotEmpty()) {
                                            replyMessages
                                        } else if (forwardCancelDialogMessages.isNotEmpty()) {
                                            forwardCancelDialogMessages
                                        } else if (liveReplyMessages.isNotEmpty()) {
                                            liveReplyMessages
                                        } else {
                                            animatedReplyMessages
                                        }
                                    onShowForwardOptions(panelMessages)
                                    forwardCancelDialogMessages = emptyList()
                                },
                                modifier = Modifier.fillMaxWidth().height(52.dp)
                            ) {
                                Text(
                                    text = "Show forwarding options",
                                    fontSize = 18.sp,
                                    fontWeight = FontWeight.Medium,
                                    color = PrimaryBlue
                                )
                            }
                            TextButton(
                                onClick = {
                                    showForwardCancelDialog = false
                                    forwardCancelDialogMessages = emptyList()
                                    onCloseReply()
                                },
                                modifier = Modifier.fillMaxWidth().height(52.dp)
                            ) {
                                Text(
                                    text = "Cancel Forwarding",
                                    fontSize = 18.sp,
                                    fontWeight = FontWeight.Medium,
                                    color = Color(0xFFFF5D73)
                                )
                            }
                            Spacer(modifier = Modifier.height(6.dp))
                        }
                    }
                }
            }
        }

        // EMOJI PICKER
        if (!isBlocked) {
            AnimatedKeyboardTransition(
                coordinator = coordinator,
                showEmojiPicker = showEmojiPicker
            ) {
                OptimizedEmojiPicker(
                    isVisible = true,
                    isDarkTheme = isDarkTheme,
                    onEmojiSelected = { emoji -> onValueChange(value + emoji) },
                    onClose = { toggleEmojiPicker() },
                    modifier = Modifier.fillMaxWidth()
                )
            }
        }
    }
}
