diff --git a/crates/ui/src/docking/surface.rs b/crates/ui/src/docking/surface.rs
--- a/crates/ui/src/docking/surface.rs
+++ b/crates/ui/src/docking/surface.rs
@@ -829,8 +829,12 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
 
     fn finish_drag(&mut self, ctx: &Context) {
         let Some(drag) = ctx.data(|d| d.get_temp::<Drag<P>>(self.id.with("drag"))) else { return };
-        let (pointer, down, released, focused) =
-            ctx.input(|i| (i.pointer.interact_pos(), i.pointer.primary_down(), i.pointer.primary_released(), i.focused));
+        let (pointer, down, released, focused, gone) =
+            ctx.input(|i| (i.pointer.interact_pos(), i.pointer.primary_down(), i.pointer.primary_released(), i.focused, !i.pointer.has_pointer()));
+        // Leaving the window is the drop: on X11 and Windows the release outside never reaches
+        // egui, so the drag would otherwise stay on the pointer. `interact_pos` still holds the
+        // last position this frame, and the panel floats there rather than docking at the edge.
+        let released = released || gone;
         let escape = ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, Key::Escape));
         if escape || !focused || !self.layout.contains(&drag.panel) || (!down && !released) {
             cancel_drag::<P>(ctx, self.id);
@@ -854,10 +858,10 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
             if pointer.distance(drag.origin) < 2.0 {
                 return;
             }
-            let action = if let Some(proposal) = proposal {
+            let action = if let Some(proposal) = proposal.filter(|_| !gone) {
                 Some(Action::Move { panel: drag.panel.clone(), anchor: proposal.anchor, placement: proposal.placement })
             } else if (self.permissions)(&drag.panel).floatable
-                && (!drag.source.contains(pointer) || self.layout.floating.iter().any(|g| g.panels.contains(&drag.panel)))
+                && (gone || !drag.source.contains(pointer) || self.layout.floating.iter().any(|g| g.panels.contains(&drag.panel)))
             {
                 let rect = clamp_float(Rect::from_min_size(pointer - drag.grab, drag.source.size()), ctx.content_rect());
                 Some(Action::Float { panel: drag.panel.clone(), rect: [rect.left(), rect.top(), rect.width(), rect.height()] })
diff --git a/crates/ui/src/docking/tests.rs b/crates/ui/src/docking/tests.rs
--- a/crates/ui/src/docking/tests.rs
+++ b/crates/ui/src/docking/tests.rs
@@ -395,6 +395,30 @@ fn actual_pointer_drag_docks_a_floating_panel_into_the_main_tab_strip() {
     assert_eq!(panels.get(*active), Some(&5));
 }
 
+/// On X11 and Windows a release outside the window never reaches egui (egui-winit drops a
+/// button event once the pointer has left), so a tab dragged out of the window used to stay on
+/// the pointer for good. Leaving the window detaches the panel where it left, even over a dock
+/// target, and ends the drag.
+#[test]
+fn tab_dragged_out_of_the_window_floats_where_it_left() {
+    let ctx = Context::default();
+    let mut layout = sample();
+    for _ in 0..3 {
+        step(&ctx, &mut layout, Vec::new());
+    }
+    let tab = ctx.read_response(Id::new("test-area").with(("tab", &3u32))).unwrap().rect.center();
+    let exit = pos2(4.0, 300.0);
+    step(&ctx, &mut layout, vec![Event::PointerMoved(tab), pointer_button(tab, true)]);
+    step(&ctx, &mut layout, vec![Event::PointerMoved(tab + vec2(-8.0, 0.0))]);
+    step(&ctx, &mut layout, vec![Event::PointerMoved(exit)]);
+    let actions = step(&ctx, &mut layout, vec![Event::PointerGone]);
+    assert!(actions.iter().any(|a| matches!(a, Action::Float { panel: 3, .. })), "{actions:?}");
+    assert!(layout.floating.iter().any(|g| g.panels == [3]), "{layout:?}");
+    let back = pos2(400.0, 300.0);
+    assert!(step(&ctx, &mut layout, vec![Event::PointerMoved(back)]).is_empty());
+    assert!(step(&ctx, &mut layout, vec![pointer_button(back, false)]).is_empty(), "the drag ended when the pointer left");
+}
+
 #[test]
 fn panel_size_limits_clamp_geometry_without_overwriting_preferred_widths() {
     let layout = Layout {
