Hover con mouse — feedback visual

Cuando el usuario usa un mouse o trackpad, espera feedback visual al pasar el cursor sobre elementos interactivos. Los componentes de Material3 ya incluyen hover states. Para componentes custom:

@Composable
fun ItemConHover(onClick: () -> Unit, content: @Composable () -> Unit) {
    val interactionSource = remember { MutableInteractionSource() }
    val isHovered by interactionSource.collectIsHoveredAsState()

    Box(
        modifier = Modifier
            .hoverable(interactionSource = interactionSource)
            .clickable(
                interactionSource = interactionSource,
                indication = ripple(),
                onClick = onClick
            )
            .background(
                color = if (isHovered)
                    MaterialTheme.colorScheme.surfaceVariant
                else
                    Color.Transparent,
                shape = RoundedCornerShape(8.dp)
            )
            .padding(8.dp)
    ) {
        content()
    }
}

// Los componentes estándar de Material3 (Button, Card, ListItem) ya tienen hover
// Solo necesitás custom hover para componentes propios

Click derecho y ContextMenu

// En Compose — ContextMenuArea para click derecho con mouse
@Composable
fun ArchivoConMenu(archivo: Archivo, onRenombrar: () -> Unit, onEliminar: () -> Unit) {
    ContextMenuArea(
        items = {
            listOf(
                ContextMenuItem("Renombrar") { onRenombrar() },
                ContextMenuItem("Eliminar") { onEliminar() },
                ContextMenuItem("Propiedades") { /* abrir diálogo */ }
            )
        }
    ) {
        // El contenido sobre el que se puede hacer click derecho
        Row(modifier = Modifier.padding(8.dp)) {
            Icon(Icons.Default.InsertDriveFile, contentDescription = null)
            Text(archivo.nombre)
        }
    }
}

// Para Views (XML) — el sistema maneja showContextMenu automáticamente:
// view.setOnLongClickListener { view.showContextMenu(); true }
// view.setOnCreateContextMenuListener { menu, _, _ ->
//     menu.add("Renombrar").setOnMenuItemClickListener { true }
//     menu.add("Eliminar").setOnMenuItemClickListener { true }
// }

Drag & drop

// Drag & drop entre elementos — API de Compose:
@Composable
fun ItemArrastrable(item: Item) {
    var isDragging by remember { mutableStateOf(false) }

    Box(
        modifier = Modifier
            .dragAndDropSource {
                detectTapGestures(
                    onLongPress = {
                        startTransfer(
                            DragAndDropTransferData(
                                ClipData.newPlainText("item_id", item.id.toString())
                            )
                        )
                    }
                )
            }
            .alpha(if (isDragging) 0.5f else 1f)
    ) {
        Text(item.nombre)
    }
}

@Composable
fun ZonaDeposito(onDeposito: (String) -> Unit) {
    var isDragOver by remember { mutableStateOf(false) }

    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(200.dp)
            .background(
                if (isDragOver) MaterialTheme.colorScheme.primaryContainer
                else MaterialTheme.colorScheme.surfaceVariant
            )
            .dragAndDropTarget(
                shouldStartDragAndDrop = { true },
                target = object : DragAndDropTarget {
                    override fun onEntered(event: DragAndDropEvent) { isDragOver = true }
                    override fun onExited(event: DragAndDropEvent) { isDragOver = false }
                    override fun onDrop(event: DragAndDropEvent): Boolean {
                        val itemId = event.toAndroidDragEvent()
                            .clipData.getItemAt(0).text.toString()
                        onDeposito(itemId)
                        isDragOver = false
                        return true
                    }
                }
            )
    ) {
        Text("Soltar aquí", modifier = Modifier.align(Alignment.Center))
    }
}

Navegación con teclado — Tab y flechas

// La navegación con Tab entre elementos interactivos funciona automáticamente
// en componentes estándar de Compose (Button, TextField, Checkbox, etc.)

// Para controlar el orden de focus manualmente:
@Composable
fun Formulario() {
    val focusManager = LocalFocusManager.current
    val (nombreFocus, emailFocus, passwordFocus) = FocusRequester.createRefs()

    TextField(
        value = nombre, onValueChange = { nombre = it },
        label = { Text("Nombre") },
        keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
        keyboardActions = KeyboardActions(onNext = { emailFocus.requestFocus() }),
        modifier = Modifier.focusRequester(nombreFocus).focusOrder { next = emailFocus }
    )
    TextField(
        value = email, onValueChange = { email = it },
        label = { Text("Email") },
        keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
        keyboardActions = KeyboardActions(onNext = { passwordFocus.requestFocus() }),
        modifier = Modifier.focusRequester(emailFocus)
            .focusOrder { previous = nombreFocus; next = passwordFocus }
    )
    TextField(
        value = password, onValueChange = { password = it },
        label = { Text("Contraseña") },
        keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
        keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
        modifier = Modifier.focusRequester(passwordFocus)
            .focusOrder { previous = emailFocus }
    )
}

// Para capturar teclas específicas:
Box(
    modifier = Modifier.onKeyEvent { event ->
        if (event.key == Key.Escape && event.type == KeyEventType.KeyDown) {
            onCerrar()
            true  // consumido
        } else false  // no consumido
    }
) { /* contenido */ }

Atajos de teclado

// Registrar atajos de teclado globales en la Activity:
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
    return when {
        // Ctrl+S → guardar
        event.isCtrlPressed && keyCode == KeyEvent.KEYCODE_S -> {
            viewModel.guardar()
            true
        }
        // Ctrl+Z → deshacer
        event.isCtrlPressed && keyCode == KeyEvent.KEYCODE_Z -> {
            viewModel.deshacer()
            true
        }
        // Ctrl+Shift+Z → rehacer
        event.isCtrlPressed && event.isShiftPressed && keyCode == KeyEvent.KEYCODE_Z -> {
            viewModel.rehacer()
            true
        }
        else -> super.onKeyDown(keyCode, event)
    }
}

// En Compose — usando KeyboardShortcut para documentarlos:
// (API de Android 14+)
// Los atajos registrados aparecen en el panel de atajos del sistema
// cuando el usuario mantiene presionado Meta/Command

// Atajos que siempre deben funcionar en ChromeOS y tablets con teclado:
// Ctrl+C / Ctrl+X / Ctrl+V → copiar/cortar/pegar (el sistema los maneja automáticamente)
// Ctrl+A → seleccionar todo (implementar en campos de texto)
// Ctrl+Z / Ctrl+Y → deshacer/rehacer (implementar si la app tiene historial)
// Escape → cerrar diálogo / cancelar acción

Stylus en tablets

// Compose — capturar eventos de stylus con presión y posición:
@Composable
fun LienzoDibujo() {
    val trazos = remember { mutableStateListOf<Trazo>() }
    var trazoActual by remember { mutableStateOf<Trazo?>(null) }

    Canvas(
        modifier = Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                awaitPointerEventScope {
                    while (true) {
                        val evento = awaitPointerEvent()
                        evento.changes.forEach { cambio ->
                            // Detectar si viene de stylus:
                            val esStylusODedo = cambio.type == PointerType.Stylus
                                || cambio.type == PointerType.Touch

                            if (esStylusODedo) {
                                val posicion = cambio.position
                                // pressure: 0f (sin presión) a 1f (presión máxima)
                                val presion = cambio.pressure

                                when {
                                    cambio.pressed && trazoActual == null -> {
                                        // Inicio del trazo
                                        trazoActual = Trazo(
                                            puntos = mutableListOf(posicion),
                                            grosor = presion * 20f
                                        )
                                    }
                                    cambio.pressed -> {
                                        // Continuación del trazo
                                        trazoActual?.puntos?.add(posicion)
                                    }
                                    !cambio.pressed && trazoActual != null -> {
                                        // Fin del trazo
                                        trazos.add(trazoActual!!)
                                        trazoActual = null
                                    }
                                }
                                cambio.consume()
                            }
                        }
                    }
                }
            }
    ) {
        // Dibujar todos los trazos completados
        trazos.forEach { trazo -> dibujarTrazo(trazo) }
        // Dibujar el trazo en progreso
        trazoActual?.let { dibujarTrazo(it) }
    }
}