Conditional Drop¶
By default, any dragged item can be dropped on any drop target. Conditional drop lets you control which drop targets accept which items, enabling scenarios such as typed drop zones, capacity limits, or permission-based dropping.
Using canDrop¶
The canDrop parameter on the dropTarget modifier accepts a lambda that receives the DraggedItemState and returns a Boolean. If it returns false, the drop target will not respond to that dragged item.
Box(
modifier = Modifier
.dropTarget(
key = "numbers-only",
state = dragAndDropState,
canDrop = { state ->
// Only accept items whose data is a number
state.data is Int
},
onDrop = { state ->
println("Accepted: ${state.data}")
},
)
) {
Text("Numbers only")
}
Experimental API
The canDrop parameter is annotated with @ExperimentalDndApi. You must opt in with @OptIn(ExperimentalDndApi::class) to use it.
Usage Scenarios¶
Typed Drop Zones¶
Accept only specific data types in different zones:
@OptIn(ExperimentalDndApi::class)
@Composable
fun TypedDropZonesExample() {
val dragAndDropState = rememberDragAndDropState<Any>()
DragAndDropContainer(state = dragAndDropState) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxWidth(),
) {
// Only accepts strings
Box(
modifier = Modifier
.weight(1f)
.height(200.dp)
.dropTarget(
key = "text-zone",
state = dragAndDropState,
canDrop = { it.data is String },
onDrop = { /* handle text drop */ },
)
) {
Text("Text Zone")
}
// Only accepts integers
Box(
modifier = Modifier
.weight(1f)
.height(200.dp)
.dropTarget(
key = "number-zone",
state = dragAndDropState,
canDrop = { it.data is Int },
onDrop = { /* handle number drop */ },
)
) {
Text("Number Zone")
}
}
}
}
Capacity Limits¶
Prevent dropping when a zone is full:
@OptIn(ExperimentalDndApi::class)
val maxItems = 5
var droppedItems by remember { mutableStateOf(listOf<String>()) }
Box(
modifier = Modifier
.dropTarget(
key = "limited-zone",
state = dragAndDropState,
canDrop = { droppedItems.size < maxItems },
onDrop = { state ->
droppedItems = droppedItems + state.data
},
)
) {
Text("${droppedItems.size} / $maxItems")
}
Preventing Self-Drop¶
Prevent an item from being dropped back onto itself:
@OptIn(ExperimentalDndApi::class)
Box(
modifier = Modifier
.dropTarget(
key = item.id,
state = dragAndDropState,
canDrop = { state -> state.key != item.id },
onDrop = { state ->
// Handle drop from a different item
},
)
)
Interaction with dropTargets Parameter¶
The canDrop parameter on the drop target is different from the dropTargets parameter on DraggableItem:
| Feature | dropTargets (on DraggableItem) | canDrop (on dropTarget) |
|---|---|---|
| Defined on | The dragged item | The drop target |
| Filters by | Drop target keys (allowlist) | Custom logic based on dragged item state |
| Use case | "This item can only go to zones A, B" | "This zone only accepts certain items" |
Both can be combined. The dragged item first checks its dropTargets list, and then the drop target checks its canDrop lambda.
Visual Feedback¶
Combine canDrop with hoveredDropTargetKey to provide visual feedback:
@OptIn(ExperimentalDndApi::class)
@Composable
fun ConditionalDropWithFeedback() {
val dragAndDropState = rememberDragAndDropState<String>()
val isHovered = dragAndDropState.hoveredDropTargetKey == "target-1"
Box(
modifier = Modifier
.border(
width = 2.dp,
color = if (isHovered) Color.Green else Color.Gray,
)
.dropTarget(
key = "target-1",
state = dragAndDropState,
canDrop = { state ->
state.data.length <= 10 // Only accept short strings
},
onDrop = { state ->
println("Dropped: ${state.data}")
},
)
) {
Text("Short strings only")
}
}
Note
When canDrop returns false, the drop target will not trigger onDragEnter, onDragExit, or onDrop for that item. The hoveredDropTargetKey will not be set to this target's key, so visual hover feedback will not activate.