# One drag gesture: every row drag is a native OS drag

> Why a hidden Alt+drag export became one native drag serving the other pane, another window and Explorer, how cwdio spots its own drop, and why it only copies.

- Canonical: https://cwdio.com/blog/one-drag-gesture/
- Updated: 2026-08-15
- Product: Cwdio File Manager (cwdio) — https://cwdio.com

Dragging a selection out of cwdio into Explorer used to be nominally possible and effectively absent. It
was <kbd>Alt</kbd>+drag, it appeared in no menu or help text, it meant something else on Windows, it
crashed the main process from a remote panel, and a plain drag out showed the no-drop cursor — which reads
as "this app can't do that". ADR 0030 replaced it with one gesture. This is why the obvious fixes did not
work and what did.

## The shape of the problem

In a file manager the destination of a drag is **not known at grab time**. You pick up files and *then*
decide — the other pane, a folder row, another window, Explorer. Any design that forces the choice at
`dragstart` fights the user's model. A modifier held at grab time is exactly such a design.

But on Windows `dragstart` is precisely where the choice must be made: an in-flight HTML5 drag cannot be
upgraded into a native one, because Chromium is already inside its own modal drag loop. So the gesture has
to commit to native or HTML5 the instant it starts, without knowing where it will end.

## What was ruled out

- **A native addon calling `DoDragDrop`** — the classic answer, dead since Electron 28: an addon cannot
  construct the nested-loop permission Chromium requires, so its modal loop starves mouse events.
- **`drag-rs`**, the best current implementation, has no Node bindings and would hit the same wall
  in-process.
- **Chromium's `DownloadURL` promise drag** needs no `preventDefault()`, so it would leave the HTML5 drag
  intact — but it is single-file, and on Windows setting it clobbers the other formats, breaking the very
  in-app drag it was meant to preserve.
- **A pointer-driven internal drag that hands off at the window edge** — the higher-fidelity end state, and
  still the follow-up — is a large rewrite that was not needed to make the gesture work.

## The decision

**There is exactly one row-drag gesture, and for anything with a real path it is a native OS drag.** The
controller calls `webContents.startDrag` at `dragstart` and the panel hands the gesture over. Released on
the other pane, a folder row, another cwdio window, Explorer or any app that takes files, the files go there
— nothing decided at grab time.

The part that makes it viable: **cwdio recognises its own drag.** The controller captures the dragged set at
`dragstart` and holds it for the drag's duration. While that set is armed, a `Files` drop landing on one of
its own panels is routed through the internal drop path, so an in-app release keeps the full internal
semantics — the guards against dropping a folder into itself or its own subtree, <kbd>Shift</kbd> as move,
conflict prompts, undo. Only when the controller has no armed set is a drop treated as a genuine import from
outside.

That needed `startDrag` to become a call that **reports**: the access policy is still enforced in the main
process and a refusal has to surface, and — because `startDrag` does not return until the drop completes —
its return is what disarms the captured set. Without it, a drag released outside the window would leave the
set armed and the next real Explorer drop would be mistaken for our own.

Rows with no local path — remote, in-archive — keep Chromium's ordinary HTML5 drag. Going native and then
finding nothing to hand the OS would cancel the drag and strand the captured set; both were real defects
caught in review of the first cut.

## Measured, not assumed

Four platform assumptions were tested against Electron 42 with a throwaway probe: an app **can** receive its
own `startDrag` back (the 2016 blocker is gone); multi-file works; cross-window works — window #1 to window
#2 lands as a real file drop; and `startDrag` does not freeze the app: it holds the *call* for the drag's
duration, roughly half a second to a second in the measurements, but `dragenter`/`dragover`/`drop` all fire
inside that window. A nested loop that keeps pumping, not a freeze.

The re-entry result is why changing your mind mid-drag recovers fully — and why cross-window drag, silently
broken before because the dragged set lived per renderer, now works.

## What was given up, knowingly

- **The cursor cannot show move.** A native drag's allowed effect is copy-only, and asking for more makes
  Chromium refuse the drop. <kbd>Shift</kbd> still *performs* a move — read at drop time — it just cannot
  be advertised.
- **Drag-out is copy-only, and stays that way.** An accidental drag that *moved* files out of a file
  manager is the worst failure the product has; Explorer itself copies across volumes. The deliberate,
  keyboard-first way to move files out is <kbd>Ctrl+X</kbd> and paste — which is what [ADR
  0031](/blog/the-os-clipboard-is-the-file-clipboard/) then built.
- **The drop highlight expires rather than waiting to be told.** Every "the drag ended" signal proved
  unreliable — Chromium skips the final `dragleave` on a crossed panel, `dragend` never fires for a native
  drag, a drop outside the window sends nothing. Two attempts to find a terminal signal both stranded the
  outline. It now lapses when `dragover` stops repeating, which also covers foreign drags from Explorer.
- **The gesture cannot be end-to-end tested.** A native drag cannot be driven by Playwright by any means:
  `dragTo` leaves a real drag in flight, and synthetic events still reach `startDrag`, which with no real
  mouse gesture sometimes returns at once and sometimes blocks the main process forever — hanging the app
  and the whole test worker. Three separate attempts each hung the suite. The pure decisions and the
  transfer engine are covered; the renderer wiring between them is not, and the ADR says so plainly.

Follow-ups named in the record: staging remote and archive rows to a temp folder so they too can leave the
app, and the pointer-driven internal drag that would restore the move cursor and make the gesture testable.
The user-facing summary is on the [Windows integration page](/features/windows-integration/).

## Frequently asked questions

**Can I drag files from cwdio into Explorer or another app?** Yes — a plain drag, no modifier. Anything with a real local path leaves as a native OS drag, single or multi-file, and lands in Explorer, another cwdio window, a mail client or any app that takes files. Remote and in-archive rows keep the ordinary in-app drag and cannot leave the app.

**Why does the cursor not show 'move' when I hold Shift?** A native drag in Electron is copy-only in what it advertises, and requesting a different effect makes Chromium refuse the drop. Shift still performs a move inside cwdio — it is read at drop time — the cursor just cannot say so.

**Can I move files out of cwdio by dragging?** No, and deliberately: drag-out is copy-only, because an accidental drag that moved files out of a file manager is the worst failure the product could have. To move files to another application, cut with Ctrl+X and paste there — the clipboard carries a move.
