diff --git a/samples/BehaviorsTestApplication/Views/MainView.axaml b/samples/BehaviorsTestApplication/Views/MainView.axaml
index c782e78d2..e91c67ce3 100644
--- a/samples/BehaviorsTestApplication/Views/MainView.axaml
+++ b/samples/BehaviorsTestApplication/Views/MainView.axaml
@@ -109,6 +109,9 @@
+
+
+
diff --git a/samples/BehaviorsTestApplication/Views/Pages/MouseDragBehaviorView.axaml b/samples/BehaviorsTestApplication/Views/Pages/MouseDragBehaviorView.axaml
new file mode 100644
index 000000000..18b48cfe4
--- /dev/null
+++ b/samples/BehaviorsTestApplication/Views/Pages/MouseDragBehaviorView.axaml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
diff --git a/samples/BehaviorsTestApplication/Views/Pages/MouseDragBehaviorView.axaml.cs b/samples/BehaviorsTestApplication/Views/Pages/MouseDragBehaviorView.axaml.cs
new file mode 100644
index 000000000..eac8377ae
--- /dev/null
+++ b/samples/BehaviorsTestApplication/Views/Pages/MouseDragBehaviorView.axaml.cs
@@ -0,0 +1,36 @@
+using System.Linq;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.Xaml.Interactivity;
+using Avalonia.Xaml.Interactions.Draggable;
+
+namespace BehaviorsTestApplication.Views.Pages;
+
+public partial class MouseDragBehaviorView : UserControl
+{
+ public MouseDragBehaviorView()
+ {
+ InitializeComponent();
+
+ var rect1 = this.FindControl("MultiRect1");
+ var rect2 = this.FindControl("MultiRect2");
+ var rect3 = this.FindControl("MultiRect3");
+
+ if (rect1 is not null && rect2 is not null && rect3 is not null)
+ {
+ var behavior = Interaction.GetBehaviors(rect1)
+ .OfType()
+ .FirstOrDefault();
+ if (behavior is not null)
+ {
+ behavior.TargetControls.Add(rect2);
+ behavior.TargetControls.Add(rect3);
+ }
+ }
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
diff --git a/src/Avalonia.Xaml.Interactions.Draggable/MouseDragElementBehavior.cs b/src/Avalonia.Xaml.Interactions.Draggable/MouseDragElementBehavior.cs
new file mode 100644
index 000000000..7650e6f82
--- /dev/null
+++ b/src/Avalonia.Xaml.Interactions.Draggable/MouseDragElementBehavior.cs
@@ -0,0 +1,137 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using Avalonia.Xaml.Interactivity;
+
+namespace Avalonia.Xaml.Interactions.Draggable;
+
+///
+/// Enables dragging of a control with the mouse using a .
+///
+public class MouseDragElementBehavior : StyledElementBehavior
+{
+ ///
+ /// Identifies the avalonia property.
+ ///
+ public static readonly StyledProperty ConstrainToParentBoundsProperty =
+ AvaloniaProperty.Register(nameof(ConstrainToParentBounds));
+
+ private bool _captured;
+ private Point _start;
+ private Control? _parent;
+ private TranslateTransform? _transform;
+
+ ///
+ /// Gets or sets whether dragging should be constrained to the bounds of the parent control.
+ ///
+ public bool ConstrainToParentBounds
+ {
+ get => GetValue(ConstrainToParentBoundsProperty);
+ set => SetValue(ConstrainToParentBoundsProperty, value);
+ }
+
+ ///
+ protected override void OnAttachedToVisualTree()
+ {
+ if (AssociatedObject is not null)
+ {
+ AssociatedObject.AddHandler(InputElement.PointerPressedEvent, Pressed, RoutingStrategies.Tunnel);
+ AssociatedObject.AddHandler(InputElement.PointerReleasedEvent, Released, RoutingStrategies.Tunnel);
+ AssociatedObject.AddHandler(InputElement.PointerMovedEvent, Moved, RoutingStrategies.Tunnel);
+ AssociatedObject.AddHandler(InputElement.PointerCaptureLostEvent, CaptureLost, RoutingStrategies.Tunnel);
+ }
+ }
+
+ ///
+ protected override void OnDetachedFromVisualTree()
+ {
+ if (AssociatedObject is not null)
+ {
+ AssociatedObject.RemoveHandler(InputElement.PointerPressedEvent, Pressed);
+ AssociatedObject.RemoveHandler(InputElement.PointerReleasedEvent, Released);
+ AssociatedObject.RemoveHandler(InputElement.PointerMovedEvent, Moved);
+ AssociatedObject.RemoveHandler(InputElement.PointerCaptureLostEvent, CaptureLost);
+ }
+ }
+
+ private void Pressed(object? sender, PointerPressedEventArgs e)
+ {
+ var properties = e.GetCurrentPoint(AssociatedObject).Properties;
+ if (properties.IsLeftButtonPressed && AssociatedObject?.Parent is Control parent)
+ {
+ _parent = parent;
+ _start = e.GetPosition(_parent);
+
+ if (AssociatedObject.RenderTransform is TranslateTransform tr)
+ {
+ _transform = tr;
+ }
+ else
+ {
+ _transform = new TranslateTransform();
+ AssociatedObject.RenderTransform = _transform;
+ }
+
+ _captured = true;
+ }
+ }
+
+ private void Released(object? sender, PointerReleasedEventArgs e)
+ {
+ if (_captured && e.InitialPressMouseButton == MouseButton.Left)
+ {
+ EndDrag();
+ }
+ }
+
+ private void CaptureLost(object? sender, PointerCaptureLostEventArgs e)
+ {
+ if (_captured)
+ {
+ EndDrag();
+ }
+ }
+
+ private void Moved(object? sender, PointerEventArgs e)
+ {
+ var properties = e.GetCurrentPoint(AssociatedObject).Properties;
+ if (!_captured || !properties.IsLeftButtonPressed || _parent is null || _transform is null)
+ {
+ return;
+ }
+
+ var position = e.GetPosition(_parent);
+ var deltaX = position.X - _start.X;
+ var deltaY = position.Y - _start.Y;
+ _start = position;
+
+ var newX = _transform.X + deltaX;
+ var newY = _transform.Y + deltaY;
+
+ if (ConstrainToParentBounds && AssociatedObject is Control element)
+ {
+ var parentBounds = _parent.Bounds;
+ var elementBounds = element.Bounds;
+
+ var minX = -elementBounds.X;
+ var minY = -elementBounds.Y;
+ var maxX = parentBounds.Width - elementBounds.Width - elementBounds.X;
+ var maxY = parentBounds.Height - elementBounds.Height - elementBounds.Y;
+
+ newX = Math.Min(Math.Max(newX, minX), maxX);
+ newY = Math.Min(Math.Max(newY, minY), maxY);
+ }
+
+ _transform.X = newX;
+ _transform.Y = newY;
+ }
+
+ private void EndDrag()
+ {
+ _captured = false;
+ _parent = null;
+ _transform = null;
+ }
+}
diff --git a/src/Avalonia.Xaml.Interactions.Draggable/MultiMouseDragElementBehavior.cs b/src/Avalonia.Xaml.Interactions.Draggable/MultiMouseDragElementBehavior.cs
new file mode 100644
index 000000000..526d23b22
--- /dev/null
+++ b/src/Avalonia.Xaml.Interactions.Draggable/MultiMouseDragElementBehavior.cs
@@ -0,0 +1,175 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using Avalonia.Metadata;
+using Avalonia.Xaml.Interactivity;
+
+namespace Avalonia.Xaml.Interactions.Draggable;
+
+///
+/// Enables dragging of multiple controls with the mouse using .
+///
+public class MultiMouseDragElementBehavior : StyledElementBehavior
+{
+ private AvaloniaList? _targetControls;
+ private bool _captured;
+ private Point _start;
+ private readonly Dictionary _transforms = new();
+ private Control? _parent;
+
+ ///
+ /// Identifies the avalonia property.
+ ///
+ public static readonly DirectProperty> TargetControlsProperty =
+ AvaloniaProperty.RegisterDirect>(nameof(TargetControls), b => b.TargetControls);
+
+ ///
+ /// Identifies the avalonia property.
+ ///
+ public static readonly StyledProperty ConstrainToParentBoundsProperty =
+ AvaloniaProperty.Register(nameof(ConstrainToParentBounds));
+
+ ///
+ /// Gets the collection of controls that should be dragged together. This is an avalonia property.
+ ///
+ [Content]
+ public AvaloniaList TargetControls => _targetControls ??= [];
+
+ ///
+ /// Gets or sets whether dragging should be constrained to the bounds of the parent control.
+ ///
+ public bool ConstrainToParentBounds
+ {
+ get => GetValue(ConstrainToParentBoundsProperty);
+ set => SetValue(ConstrainToParentBoundsProperty, value);
+ }
+
+ ///
+ protected override void OnAttachedToVisualTree()
+ {
+ if (AssociatedObject is not null)
+ {
+ AssociatedObject.AddHandler(InputElement.PointerPressedEvent, Pressed, RoutingStrategies.Tunnel);
+ AssociatedObject.AddHandler(InputElement.PointerReleasedEvent, Released, RoutingStrategies.Tunnel);
+ AssociatedObject.AddHandler(InputElement.PointerMovedEvent, Moved, RoutingStrategies.Tunnel);
+ AssociatedObject.AddHandler(InputElement.PointerCaptureLostEvent, CaptureLost, RoutingStrategies.Tunnel);
+ }
+ }
+
+ ///
+ protected override void OnDetachedFromVisualTree()
+ {
+ if (AssociatedObject is not null)
+ {
+ AssociatedObject.RemoveHandler(InputElement.PointerPressedEvent, Pressed);
+ AssociatedObject.RemoveHandler(InputElement.PointerReleasedEvent, Released);
+ AssociatedObject.RemoveHandler(InputElement.PointerMovedEvent, Moved);
+ AssociatedObject.RemoveHandler(InputElement.PointerCaptureLostEvent, CaptureLost);
+ }
+ }
+
+ private void Pressed(object? sender, PointerPressedEventArgs e)
+ {
+ var properties = e.GetCurrentPoint(AssociatedObject).Properties;
+ if (properties.IsLeftButtonPressed && AssociatedObject?.Parent is Control parent)
+ {
+ _parent = parent;
+ _start = e.GetPosition(_parent);
+ _transforms.Clear();
+
+ AddTransform(AssociatedObject);
+ foreach (var control in TargetControls)
+ {
+ AddTransform(control);
+ }
+
+ _captured = true;
+ }
+ }
+
+ private void Released(object? sender, PointerReleasedEventArgs e)
+ {
+ if (_captured && e.InitialPressMouseButton == MouseButton.Left)
+ {
+ EndDrag();
+ }
+ }
+
+ private void CaptureLost(object? sender, PointerCaptureLostEventArgs e)
+ {
+ if (_captured)
+ {
+ EndDrag();
+ }
+ }
+
+ private void Moved(object? sender, PointerEventArgs e)
+ {
+ var properties = e.GetCurrentPoint(AssociatedObject).Properties;
+ if (!_captured || !properties.IsLeftButtonPressed || _parent is null)
+ {
+ return;
+ }
+
+ var position = e.GetPosition(_parent);
+ var dx = position.X - _start.X;
+ var dy = position.Y - _start.Y;
+ _start = position;
+
+ foreach (var kvp in _transforms)
+ {
+ var element = kvp.Key;
+ var transform = kvp.Value;
+
+ var newX = transform.X + dx;
+ var newY = transform.Y + dy;
+
+ if (ConstrainToParentBounds && element.Parent is Control p)
+ {
+ var parentBounds = p.Bounds;
+ var bounds = element.Bounds;
+
+ var minX = -bounds.X;
+ var minY = -bounds.Y;
+ var maxX = parentBounds.Width - bounds.Width - bounds.X;
+ var maxY = parentBounds.Height - bounds.Height - bounds.Y;
+
+ newX = Math.Min(Math.Max(newX, minX), maxX);
+ newY = Math.Min(Math.Max(newY, minY), maxY);
+ }
+
+ transform.X = newX;
+ transform.Y = newY;
+ }
+ }
+
+ private void AddTransform(Control? element)
+ {
+ if (element is null)
+ {
+ return;
+ }
+
+ if (element.RenderTransform is TranslateTransform tr)
+ {
+ _transforms[element] = tr;
+ }
+ else
+ {
+ var t = new TranslateTransform();
+ element.RenderTransform = t;
+ _transforms[element] = t;
+ }
+ }
+
+ private void EndDrag()
+ {
+ _captured = false;
+ _parent = null;
+ _transforms.Clear();
+ }
+}