diff --git a/dotfiles/config/hypr/hyprland.conf b/dotfiles/config/hypr/hyprland.conf
index bb865cb8..cb3f7274 100644
--- a/dotfiles/config/hypr/hyprland.conf
+++ b/dotfiles/config/hypr/hyprland.conf
@@ -315,10 +315,10 @@ bind = $mainMod, A, hy3:movefocus, l
bind = $mainMod, D, hy3:movefocus, r
# Move windows (Mod + Shift + WASD) - hy3:movewindow with once=true for swapping
-bind = $mainMod SHIFT, W, hy3:movewindow, u, once
-bind = $mainMod SHIFT, S, hy3:movewindow, d, once
-bind = $mainMod SHIFT, A, hy3:movewindow, l, once
-bind = $mainMod SHIFT, D, hy3:movewindow, r, once
+bind = $mainMod SHIFT, W, exec, ~/.config/hypr/scripts/movewindow-follow-cursor.sh u once
+bind = $mainMod SHIFT, S, exec, ~/.config/hypr/scripts/movewindow-follow-cursor.sh d once
+bind = $mainMod SHIFT, A, exec, ~/.config/hypr/scripts/movewindow-follow-cursor.sh l once
+bind = $mainMod SHIFT, D, exec, ~/.config/hypr/scripts/movewindow-follow-cursor.sh r once
# Resize windows (Mod + Ctrl + WASD)
binde = $mainMod CTRL, W, resizeactive, 0 -50
diff --git a/dotfiles/config/hypr/scripts/movewindow-follow-cursor.sh b/dotfiles/config/hypr/scripts/movewindow-follow-cursor.sh
new file mode 100755
index 00000000..0256e733
--- /dev/null
+++ b/dotfiles/config/hypr/scripts/movewindow-follow-cursor.sh
@@ -0,0 +1,83 @@
+#!/usr/bin/env bash
+# Move the active window in a direction and warp the cursor to keep its
+# relative position inside the moved window.
+
+set -euo pipefail
+
+export PATH="/run/current-system/sw/bin:${PATH}"
+
+if [[ $# -lt 1 ]]; then
+ echo "usage: $0
[mode]" >&2
+ exit 1
+fi
+
+dir="$1"
+mode="${2:-}"
+
+if ! command -v hyprctl >/dev/null; then
+ exit 0
+fi
+
+move_window() {
+ if [[ -n "$mode" ]]; then
+ hyprctl dispatch hy3:movewindow "$dir, $mode" >/dev/null 2>&1 || true
+ else
+ hyprctl dispatch hy3:movewindow "$dir" >/dev/null 2>&1 || true
+ fi
+}
+
+win_json="$(hyprctl -j activewindow 2>/dev/null || true)"
+cur_json="$(hyprctl -j cursorpos 2>/dev/null || true)"
+
+if [[ -z "$win_json" || "$win_json" == "null" || -z "$cur_json" || "$cur_json" == "null" ]]; then
+ move_window
+ exit 0
+fi
+
+win_x="$(jq -er '.at[0]' <<<"$win_json" 2>/dev/null || true)"
+win_y="$(jq -er '.at[1]' <<<"$win_json" 2>/dev/null || true)"
+win_w="$(jq -er '.size[0]' <<<"$win_json" 2>/dev/null || true)"
+win_h="$(jq -er '.size[1]' <<<"$win_json" 2>/dev/null || true)"
+cur_x="$(jq -er '.x' <<<"$cur_json" 2>/dev/null || true)"
+cur_y="$(jq -er '.y' <<<"$cur_json" 2>/dev/null || true)"
+
+if [[ ! "$win_x" =~ ^-?[0-9]+$ || ! "$win_y" =~ ^-?[0-9]+$ || ! "$win_w" =~ ^-?[0-9]+$ || ! "$win_h" =~ ^-?[0-9]+$ || ! "$cur_x" =~ ^-?[0-9]+$ || ! "$cur_y" =~ ^-?[0-9]+$ ]]; then
+ move_window
+ exit 0
+fi
+
+rel_x=$((cur_x - win_x))
+rel_y=$((cur_y - win_y))
+
+move_window
+
+win_json="$(hyprctl -j activewindow 2>/dev/null || true)"
+if [[ -z "$win_json" || "$win_json" == "null" ]]; then
+ exit 0
+fi
+
+win_x="$(jq -er '.at[0]' <<<"$win_json" 2>/dev/null || true)"
+win_y="$(jq -er '.at[1]' <<<"$win_json" 2>/dev/null || true)"
+win_w="$(jq -er '.size[0]' <<<"$win_json" 2>/dev/null || true)"
+win_h="$(jq -er '.size[1]' <<<"$win_json" 2>/dev/null || true)"
+
+if [[ ! "$win_x" =~ ^-?[0-9]+$ || ! "$win_y" =~ ^-?[0-9]+$ || ! "$win_w" =~ ^-?[0-9]+$ || ! "$win_h" =~ ^-?[0-9]+$ ]]; then
+ exit 0
+fi
+
+if ((rel_x < 0)); then
+ rel_x=0
+elif ((rel_x > win_w)); then
+ rel_x=$win_w
+fi
+
+if ((rel_y < 0)); then
+ rel_y=0
+elif ((rel_y > win_h)); then
+ rel_y=$win_h
+fi
+
+new_x=$((win_x + rel_x))
+new_y=$((win_y + rel_y))
+
+hyprctl dispatch movecursor "$new_x" "$new_y" >/dev/null 2>&1 || true