diff --git a/Dashboard/Helpers/ScrollPanBehavior.cs b/Dashboard/Helpers/ScrollPanBehavior.cs
new file mode 100644
index 00000000..485483b9
--- /dev/null
+++ b/Dashboard/Helpers/ScrollPanBehavior.cs
@@ -0,0 +1,271 @@
+/*
+ * Copyright (c) 2026 Erik Darling, Darling Data LLC
+ *
+ * This file is part of the SQL Server Performance Monitor.
+ *
+ * Licensed under the MIT License. See LICENSE file in the project root for full license information.
+ */
+
+using System;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Input;
+using System.Windows.Media;
+
+namespace PerformanceMonitorDashboard.Helpers;
+
+///
+/// Enables middle-mouse drag panning for scrollable controls such as DataGrid and ListView.
+///
+public static class ScrollPanBehavior
+{
+ public static readonly DependencyProperty EnableMiddleClickPanningProperty =
+ DependencyProperty.RegisterAttached(
+ "EnableMiddleClickPanning",
+ typeof(bool),
+ typeof(ScrollPanBehavior),
+ new PropertyMetadata(false, OnEnableMiddleClickPanningChanged));
+
+ private static readonly DependencyProperty PanStateProperty =
+ DependencyProperty.RegisterAttached(
+ "PanState",
+ typeof(PanState),
+ typeof(ScrollPanBehavior),
+ new PropertyMetadata(null));
+
+ public static bool GetEnableMiddleClickPanning(DependencyObject obj) => (bool)obj.GetValue(EnableMiddleClickPanningProperty);
+ public static void SetEnableMiddleClickPanning(DependencyObject obj, bool value) => obj.SetValue(EnableMiddleClickPanningProperty, value);
+
+ private static PanState GetOrCreatePanState(FrameworkElement element)
+ {
+ if (element.GetValue(PanStateProperty) is not PanState state)
+ {
+ state = new PanState();
+ element.SetValue(PanStateProperty, state);
+ }
+
+ return state;
+ }
+
+ private static void OnEnableMiddleClickPanningChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is not FrameworkElement element)
+ {
+ return;
+ }
+
+ var isEnabled = (bool)e.NewValue;
+
+ if (isEnabled)
+ {
+ element.Loaded += OnLoaded;
+ element.Unloaded += OnUnloaded;
+ element.PreviewMouseDown += OnPreviewMouseDown;
+ element.PreviewMouseMove += OnPreviewMouseMove;
+ element.PreviewMouseUp += OnPreviewMouseUp;
+ element.LostMouseCapture += OnLostMouseCapture;
+ }
+ else
+ {
+ element.Loaded -= OnLoaded;
+ element.Unloaded -= OnUnloaded;
+ element.PreviewMouseDown -= OnPreviewMouseDown;
+ element.PreviewMouseMove -= OnPreviewMouseMove;
+ element.PreviewMouseUp -= OnPreviewMouseUp;
+ element.LostMouseCapture -= OnLostMouseCapture;
+ StopPanning(element, restoreCursor: true);
+ }
+ }
+
+ private static void OnLoaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is FrameworkElement element)
+ {
+ GetOrCreatePanState(element).ScrollViewer = FindVisualChild(element);
+ }
+ }
+
+ private static void OnUnloaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is FrameworkElement element)
+ {
+ StopPanning(element, restoreCursor: false);
+ GetOrCreatePanState(element).ScrollViewer = null;
+ }
+ }
+
+ private static void OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
+ {
+ if (e.ChangedButton != MouseButton.Middle
+ || sender is not FrameworkElement element
+ || !CanStartPanning(e.OriginalSource as DependencyObject))
+ {
+ return;
+ }
+
+ var state = GetOrCreatePanState(element);
+ state.ScrollViewer ??= FindVisualChild(element);
+
+ if (state.ScrollViewer is null)
+ {
+ return;
+ }
+
+ if (state.ScrollViewer.ScrollableWidth <= 0 && state.ScrollViewer.ScrollableHeight <= 0)
+ {
+ return;
+ }
+
+ state.IsPanning = true;
+ state.StartPoint = e.GetPosition(state.ScrollViewer);
+ state.StartHorizontalOffset = state.ScrollViewer.HorizontalOffset;
+ state.StartVerticalOffset = state.ScrollViewer.VerticalOffset;
+ state.OriginalCursor = element.Cursor;
+
+ element.Cursor = Cursors.ScrollAll;
+ element.CaptureMouse();
+ e.Handled = true;
+ }
+
+ private static void OnPreviewMouseMove(object sender, MouseEventArgs e)
+ {
+ if (sender is not FrameworkElement element)
+ {
+ return;
+ }
+
+ var state = GetOrCreatePanState(element);
+ if (!state.IsPanning || state.ScrollViewer is null)
+ {
+ return;
+ }
+
+ var currentPoint = e.GetPosition(state.ScrollViewer);
+ var deltaX = currentPoint.X - state.StartPoint.X;
+ var deltaY = currentPoint.Y - state.StartPoint.Y;
+
+ state.ScrollViewer.ScrollToHorizontalOffset(ClampOffset(state.StartHorizontalOffset - deltaX, state.ScrollViewer.ScrollableWidth));
+ state.ScrollViewer.ScrollToVerticalOffset(ClampOffset(state.StartVerticalOffset - deltaY, state.ScrollViewer.ScrollableHeight));
+ e.Handled = true;
+ }
+
+ private static void OnPreviewMouseUp(object sender, MouseButtonEventArgs e)
+ {
+ if (e.ChangedButton != MouseButton.Middle || sender is not FrameworkElement element)
+ {
+ return;
+ }
+
+ if (!GetOrCreatePanState(element).IsPanning)
+ {
+ return;
+ }
+
+ StopPanning(element, restoreCursor: true);
+ e.Handled = true;
+ }
+
+ private static void OnLostMouseCapture(object sender, MouseEventArgs e)
+ {
+ if (sender is FrameworkElement element)
+ {
+ StopPanning(element, restoreCursor: true);
+ }
+ }
+
+ private static void StopPanning(FrameworkElement element, bool restoreCursor)
+ {
+ var state = GetOrCreatePanState(element);
+ if (!state.IsPanning)
+ {
+ if (restoreCursor)
+ {
+ element.ClearValue(FrameworkElement.CursorProperty);
+ }
+
+ return;
+ }
+
+ state.IsPanning = false;
+
+ if (restoreCursor)
+ {
+ if (state.OriginalCursor is null)
+ {
+ element.ClearValue(FrameworkElement.CursorProperty);
+ }
+ else
+ {
+ element.Cursor = state.OriginalCursor;
+ }
+ }
+
+ state.OriginalCursor = null;
+
+ if (Mouse.Captured == element)
+ {
+ element.ReleaseMouseCapture();
+ }
+ }
+
+ private static bool CanStartPanning(DependencyObject? source)
+ {
+ while (source is not null)
+ {
+ if (source is ScrollBar
+ || source is Thumb
+ || source is DataGridColumnHeader
+ || source is GridViewColumnHeader
+ || source is TextBoxBase
+ || source is PasswordBox
+ || source is ComboBox
+ || source is ComboBoxItem
+ || source is ButtonBase)
+ {
+ return false;
+ }
+
+ source = VisualTreeHelper.GetParent(source);
+ }
+
+ return true;
+ }
+
+ private static double ClampOffset(double value, double maxValue) => Math.Max(0, Math.Min(maxValue, value));
+
+ private static T? FindVisualChild(DependencyObject? parent) where T : DependencyObject
+ {
+ if (parent is null)
+ {
+ return null;
+ }
+
+ for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
+ {
+ var child = VisualTreeHelper.GetChild(parent, i);
+ if (child is T target)
+ {
+ return target;
+ }
+
+ var nested = FindVisualChild(child);
+ if (nested is not null)
+ {
+ return nested;
+ }
+ }
+
+ return null;
+ }
+
+ private sealed class PanState
+ {
+ public bool IsPanning { get; set; }
+ public Point StartPoint { get; set; }
+ public double StartHorizontalOffset { get; set; }
+ public double StartVerticalOffset { get; set; }
+ public Cursor? OriginalCursor { get; set; }
+ public ScrollViewer? ScrollViewer { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Dashboard/Themes/CoolBreezeTheme.xaml b/Dashboard/Themes/CoolBreezeTheme.xaml
index 4eb70025..933d65e4 100644
--- a/Dashboard/Themes/CoolBreezeTheme.xaml
+++ b/Dashboard/Themes/CoolBreezeTheme.xaml
@@ -649,6 +649,7 @@