diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs
--- a/crates/ui-egui/src/canvas.rs
+++ b/crates/ui-egui/src/canvas.rs
@@ -805,6 +805,8 @@ impl DocView {
         self.wheel.clear_motion();
         self.wheel_boost_x.reset();
         self.wheel_boost_y.reset();
+        self.fling.stop();
+        self.fling.reset_gesture();
     }
 }
 
@@ -1104,14 +1106,17 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
     // Touchpad momentum (#531): a swipe over the page lifted at speed glides on and slows to a
     // stop, through the same un-eased path. Never while the pager owns the wheel, a gesture or
     // dialog has the input, or animations are off; any click, key, zoom or other scrolling ends it.
+    // Discrete mouse wheel scrolling stops any active fling and never glides (#4917).
     // Asked of the preference, not of egui's animation time, which test harnesses zero.
+    let has_discrete_wheel = line_base != Vec2::ZERO || boost_extra != Vec2::ZERO || input.line != Vec2::ZERO || has_page;
     let animates = app.animation_effects.animates(app.motion.reduced);
-    let may_glide = !paging_owns_vertical && !middle_gesture && navigation_enabled && unobstructed && animates;
+    let may_glide = !paging_owns_vertical && !middle_gesture && navigation_enabled && unobstructed && animates && !has_discrete_wheel;
     let over_page = ui.rect_contains_pointer(avail);
     ui.input(|i| view.fling.read(&i.events, now, may_glide && over_page));
     let interrupted = glide_interrupted(ui);
-    if !may_glide || interrupted || key_scroll != 0.0 || auto_delta != Vec2::ZERO || zoom_delta != 1.0 {
+    if !may_glide || interrupted || key_scroll != 0.0 || auto_delta != Vec2::ZERO || zoom_delta != 1.0 || has_discrete_wheel {
         view.fling.stop();
+        view.fling.reset_gesture();
     }
     let glide = view.fling.step(now);
     if view.fling.gliding() {
diff --git a/crates/ui-egui/src/canvas/fling.rs b/crates/ui-egui/src/canvas/fling.rs
--- a/crates/ui-egui/src/canvas/fling.rs
+++ b/crates/ui-egui/src/canvas/fling.rs
@@ -29,6 +29,8 @@ const STALE: f64 = 0.1;
 pub(crate) struct Fling {
     /// Whether a lift glides on this platform (see the module docs).
     pub(crate) enabled: bool,
+    /// Whether an active touchpad gesture is currently in progress (#4917).
+    touch_active: bool,
     /// The gesture in progress: when each pass's motion arrived, and how far it went.
     samples: Vec<(f64, Vec2)>,
     /// The glide's speed in points per second (positive moves the content down), or zero.
@@ -46,19 +48,28 @@ impl Default for Fling {
 }
 
 impl Fling {
+    /// Creates a new fling momentum tracker, enabled where supported by the platform.
     pub(crate) fn new(enabled: bool) -> Self {
-        Self { enabled, samples: Vec::new(), velocity: Vec2::ZERO, at: 0.0, stalled: 0 }
+        Self { enabled, touch_active: false, samples: Vec::new(), velocity: Vec2::ZERO, at: 0.0, stalled: 0 }
     }
 
+    /// Whether the view is currently gliding on fling momentum.
     pub(crate) fn gliding(&self) -> bool {
         self.velocity != Vec2::ZERO
     }
 
+    /// Stops any ongoing glide and resets stalled counter.
     pub(crate) fn stop(&mut self) {
         self.velocity = Vec2::ZERO;
         self.stalled = 0;
     }
 
+    /// Resets active touchpad gesture tracking and clears accumulated samples (#4917).
+    pub(crate) fn reset_gesture(&mut self) {
+        self.touch_active = false;
+        self.samples.clear();
+    }
+
     /// Reads one pass's wheel events, at time `now`. Any wheel input stops a glide; a touchpad
     /// gesture's `End` may start one. `glide` is false where the view must not glide this pass
     /// (animations off, single-page paging, a zoom, a dialog).
@@ -68,27 +79,33 @@ impl Fling {
             self.stop();
             // Zooming, a mouse wheel and sideways Shift scrolling never glide.
             if *unit != MouseWheelUnit::Point || modifiers.command || modifiers.ctrl || modifiers.shift {
-                self.samples.clear();
+                self.reset_gesture();
                 continue;
             }
             match phase {
                 TouchPhase::Start => {
+                    self.touch_active = true;
                     self.samples.clear();
                     self.push(now, *delta);
                 }
-                TouchPhase::Move => self.push(now, *delta),
+                TouchPhase::Move => {
+                    if self.touch_active {
+                        self.push(now, *delta);
+                    }
+                }
                 TouchPhase::End => {
-                    if glide && self.enabled {
+                    if self.touch_active && glide && self.enabled {
                         self.velocity = self.lift_velocity(now);
                         self.at = now;
                     }
-                    self.samples.clear();
+                    self.reset_gesture();
                 }
-                TouchPhase::Cancel => self.samples.clear(),
+                TouchPhase::Cancel => self.reset_gesture(),
             }
         }
     }
 
+    /// Records one sample of gesture delta at time `now`.
     fn push(&mut self, now: f64, delta: Vec2) {
         if !(delta.x.is_finite() && delta.y.is_finite()) {
             return;
@@ -159,6 +176,7 @@ mod tests {
     use super::*;
     use egui::{Modifiers, vec2};
 
+    /// Helper creating a wheel point event with the given phase.
     fn wheel(dy: f32, phase: TouchPhase) -> Event {
         Event::MouseWheel {
             unit: MouseWheelUnit::Point,
@@ -293,4 +311,48 @@ mod tests {
         swipe(&mut fling, f32::NAN, 6);
         assert!(!fling.gliding(), "a speed that isn't a number");
     }
+
+    /// Discrete wheel events (Line deltas) must never trigger a kinetic glide (#4917).
+    #[test]
+    fn discrete_wheel_events_never_trigger_glide() {
+        let mut fling = Fling::new(true);
+        let dt = 1.0 / 60.0;
+        let mut now = 0.0;
+        for _ in 0..10 {
+            now += dt;
+            let notch = Event::MouseWheel {
+                unit: MouseWheelUnit::Line,
+                delta: vec2(0.0, -1.0),
+                phase: TouchPhase::Move,
+                modifiers: Modifiers::NONE,
+                source: egui::MouseWheelSource::Unknown,
+            };
+            fling.read(&[notch], now, true);
+        }
+        now += dt;
+        let end_notch = Event::MouseWheel {
+            unit: MouseWheelUnit::Line,
+            delta: vec2(0.0, 0.0),
+            phase: TouchPhase::End,
+            modifiers: Modifiers::NONE,
+            source: egui::MouseWheelSource::Unknown,
+        };
+        fling.read(&[end_notch], now, true);
+        assert!(!fling.gliding(), "discrete wheel steps never trigger glide");
+    }
+
+    /// Movement without a preceding TouchPhase::Start must never trigger a kinetic glide (#4917).
+    #[test]
+    fn move_without_touch_start_never_triggers_glide() {
+        let mut fling = Fling::new(true);
+        let dt = 1.0 / 60.0;
+        let mut now = 0.0;
+        for _ in 0..6 {
+            now += dt;
+            fling.read(&[wheel(-25.0, TouchPhase::Move)], now, true);
+        }
+        now += dt;
+        fling.read(&[wheel(0.0, TouchPhase::End)], now, true);
+        assert!(!fling.gliding(), "no TouchPhase::Start means not a touchpad gesture");
+    }
 }
diff --git a/crates/ui-egui/tests/it/canvas.rs b/crates/ui-egui/tests/it/canvas.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/tests/it/canvas.rs
@@ -0,0 +1,137 @@
+//! Canvas scrolling tests, verifying that mouse wheel events do not trigger kinetic fling jumps (#4917).
+
+use egui::{Modifiers, MouseWheelUnit, TouchPhase, vec2};
+use egui_kittest::Harness;
+use pdfcraft_ui_egui::PdfCraftApp;
+
+/// Three 300x400 pt pages in a continuous scroll view.
+const PAGES: &[u8] = b"%PDF-1.7
+1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
+2 0 obj << /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 /MediaBox [0 0 300 400] >> endobj
+3 0 obj << /Type /Page /Parent 2 0 R >> endobj
+4 0 obj << /Type /Page /Parent 2 0 R >> endobj
+5 0 obj << /Type /Page /Parent 2 0 R >> endobj
+trailer << /Root 1 0 R >>
+%%EOF";
+
+/// Creates a test harness with stepped frame time and standard continuous layout.
+fn harness(step_dt: f32) -> Harness<'static, PdfCraftApp> {
+    let mut h = Harness::builder().with_size(vec2(1400.0, 900.0)).with_step_dt(step_dt).build_eframe(move |_cc| {
+        let mut app = PdfCraftApp::new();
+        app.set_option("language", "en").unwrap();
+        app.open_bytes("pages.pdf", None, PAGES.to_vec()).expect("opens");
+        app.set_option("left", "closed").unwrap();
+        app.set_option("panel", "none").unwrap();
+        app.set_option("layout", "continuous").unwrap();
+        app.set_option("zoom", "300").unwrap();
+        app
+    });
+    h.run_steps(8);
+    h
+}
+
+/// Helper getting the screen rect of a page.
+fn rect(h: &Harness<'static, PdfCraftApp>, page: usize) -> Option<egui::Rect> {
+    h.state().views[0].page_screen_rect(page)
+}
+
+/// Measures total vertical scroll movement over `frames` steps.
+fn scroll_over(h: &mut Harness<'static, PdfCraftApp>, frames: usize) -> f32 {
+    let tops = |h: &Harness<'static, PdfCraftApp>| (0..3).map(|p| rect(h, p).map(|r| r.min.y)).collect::<Vec<_>>();
+    let mut moved = 0.0;
+    for _ in 0..frames {
+        let before = tops(h);
+        h.step();
+        let after = tops(h);
+        let step = before.iter().zip(&after).find_map(|(b, a)| Some(b.as_ref()? - a.as_ref()?));
+        moved += step.expect("a page stays on screen from one frame to the next");
+    }
+    moved
+}
+
+/// Rapid discrete mouse wheel steps must never trigger kinetic momentum (#4917).
+#[test]
+fn fast_mouse_wheel_scrolling_does_not_trigger_kinetic_fling_jumps() {
+    let mut h = harness(1.0 / 60.0);
+    let at = h.state().views[0].viewport_rect().center();
+    h.hover_at(at);
+    h.run_steps(2);
+
+    // Send rapid discrete mouse wheel notches (line delta).
+    for _ in 0..10 {
+        h.event(egui::Event::MouseWheel {
+            unit: MouseWheelUnit::Line,
+            delta: vec2(0.0, -1.0),
+            phase: TouchPhase::Move,
+            modifiers: Modifiers::NONE,
+            source: egui::MouseWheelSource::Unknown,
+        });
+        h.step();
+    }
+
+    // Step 20 frames to allow egui's internal ~0.25s notch easing to finish settling.
+    h.run_steps(20);
+
+    // After mouse wheel scrolling ceases, the view must not exhibit any fling glide or drift.
+    let drift = scroll_over(&mut h, 20);
+    assert!(drift.abs() < 0.5, "mouse wheel must not trigger kinetic fling drift: drifted {drift}");
+}
+
+/// A mouse wheel notch immediately stops an active touchpad fling glide (#4917).
+#[cfg(not(target_os = "macos"))]
+#[test]
+fn mouse_wheel_notch_interrupts_active_touchpad_glide() {
+    let mut h = harness(1.0 / 60.0);
+    let at = h.state().views[0].viewport_rect().center();
+    h.hover_at(at);
+    h.run_steps(2);
+
+    // Perform a touchpad swipe to enter a gliding state.
+    h.event(egui::Event::MouseWheel {
+        unit: MouseWheelUnit::Point,
+        delta: vec2(0.0, -25.0),
+        phase: TouchPhase::Start,
+        modifiers: Modifiers::NONE,
+        source: egui::MouseWheelSource::Unknown,
+    });
+    h.step();
+
+    for _ in 0..6 {
+        h.event(egui::Event::MouseWheel {
+            unit: MouseWheelUnit::Point,
+            delta: vec2(0.0, -25.0),
+            phase: TouchPhase::Move,
+            modifiers: Modifiers::NONE,
+            source: egui::MouseWheelSource::Unknown,
+        });
+        h.step();
+    }
+
+    h.event(egui::Event::MouseWheel {
+        unit: MouseWheelUnit::Point,
+        delta: vec2(0.0, 0.0),
+        phase: TouchPhase::End,
+        modifiers: Modifiers::NONE,
+        source: egui::MouseWheelSource::Unknown,
+    });
+    h.step();
+
+    // Verify the page is gliding forward.
+    let gliding_delta = scroll_over(&mut h, 3);
+    assert!(gliding_delta > 10.0, "touchpad swipe must glide after lift: moved {gliding_delta}");
+
+    // Deliver a discrete wheel notch.
+    h.event(egui::Event::MouseWheel {
+        unit: MouseWheelUnit::Line,
+        delta: vec2(0.0, -1.0),
+        phase: TouchPhase::Move,
+        modifiers: Modifiers::NONE,
+        source: egui::MouseWheelSource::Unknown,
+    });
+    h.step();
+    h.run_steps(2);
+
+    // After the wheel notch, the prior fling glide must be completely cancelled.
+    let drift = scroll_over(&mut h, 20);
+    assert!(drift.abs() < 0.5, "mouse wheel notch must stop kinetic glide: drifted {drift}");
+}
diff --git a/crates/ui-egui/tests/it/main.rs b/crates/ui-egui/tests/it/main.rs
--- a/crates/ui-egui/tests/it/main.rs
+++ b/crates/ui-egui/tests/it/main.rs
@@ -27,6 +27,7 @@ mod bookmarks;
 mod bulk_fields;
 mod buttons;
 mod camera;
+mod canvas;
 mod column_select;
 mod combine_grid;
 mod commands;
