diff --git a/DeepDrftShared.Client/Components/RadialKnob.razor b/DeepDrftShared.Client/Components/RadialKnob.razor
new file mode 100644
index 0000000..809878a
--- /dev/null
+++ b/DeepDrftShared.Client/Components/RadialKnob.razor
@@ -0,0 +1,225 @@
+@using System.Globalization
+
+
+@if (_isDragging)
+{
+
+
+}
+
+
+
+
+
+
+
+
+
+@code {
+ [Parameter] public double Value { get; set; }
+ [Parameter] public EventCallback ValueChanged { get; set; }
+ [Parameter] public double Min { get; set; } = 0;
+ [Parameter] public double Max { get; set; } = 100;
+ [Parameter] public double Step { get; set; } = 1;
+ [Parameter] public string Label { get; set; } = "";
+ [Parameter] public int Size { get; set; } = 50;
+ [Parameter] public MudBlazor.Color Color { get; set; } = MudBlazor.Color.Primary;
+ [Parameter] public bool HoldValue { get; set; } = false;
+
+ private bool _isDragging = false;
+ private double _lastMouseY = 0;
+ private double _dragValue = 0;
+
+ private double GetNormalizedValue()
+ {
+ // Use drag value during dragging if HoldValue is enabled, otherwise use current Value
+ double currentValue = (_isDragging && HoldValue) ? _dragValue : Value;
+ return Math.Max(0, Math.Min(1, (currentValue - Min) / (Max - Min)));
+ }
+
+ private string GetDisplayLabel()
+ {
+ // Show drag value during dragging if HoldValue is enabled, otherwise use provided Label
+ if (_isDragging && HoldValue)
+ {
+ return _dragValue.ToString("F0");
+ }
+ return Label;
+ }
+
+ private string GetKnobColor()
+ {
+ return Color switch
+ {
+ MudBlazor.Color.Primary => "var(--mud-palette-primary)",
+ MudBlazor.Color.Secondary => "var(--mud-palette-secondary)",
+ MudBlazor.Color.Success => "var(--mud-palette-success)",
+ MudBlazor.Color.Warning => "var(--mud-palette-warning)",
+ MudBlazor.Color.Error => "var(--mud-palette-error)",
+ MudBlazor.Color.Info => "var(--mud-palette-info)",
+ _ => "var(--mud-palette-primary)"
+ };
+ }
+
+ private string GetBackgroundStrokeDashArray()
+ {
+ double circumference = 2 * Math.PI * 35; // radius = 35
+ double maxArc = circumference * 0.75; // 270 degrees
+ return $"{maxArc} {circumference}";
+ }
+
+ private string GetStrokeDashArray()
+ {
+ double circumference = 2 * Math.PI * 35; // radius = 35
+ double maxArc = circumference * 0.75; // 270 degrees
+ double valueArc = maxArc * GetNormalizedValue();
+ return $"{valueArc} {circumference}";
+ }
+
+ private string GetStrokeDashOffset()
+ {
+ // No offset needed - arc should start from the beginning and grow
+ return "0";
+ }
+
+ private string GetPointerX()
+ {
+ double angle = -225 + (270 * GetNormalizedValue()); // -225 to +45 degrees (centered on vertical)
+ double radians = angle * Math.PI / 180;
+ double x = 50 + (25 * Math.Cos(radians)); // radius = 25 for pointer
+ return x.ToString("F1", CultureInfo.InvariantCulture);
+ }
+
+ private string GetPointerY()
+ {
+ double angle = -225 + (270 * GetNormalizedValue());
+ double radians = angle * Math.PI / 180;
+ double y = 50 + (25 * Math.Sin(radians));
+ return y.ToString("F1", CultureInfo.InvariantCulture);
+ }
+
+ private async Task OnMouseDown(MouseEventArgs e)
+ {
+ _isDragging = true;
+ _lastMouseY = e.ClientY;
+ _dragValue = Value; // Initialize drag value with current value
+
+ // Add global mouse event handlers using Blazor's event handling
+ StateHasChanged();
+ }
+
+ private async Task OnGlobalMouseMove(MouseEventArgs e)
+ {
+ if (_isDragging)
+ {
+ await UpdateValueFromMouse(e);
+ }
+ }
+
+ private async Task OnGlobalMouseUp(MouseEventArgs e)
+ {
+ if (_isDragging && HoldValue)
+ {
+ // If HoldValue is enabled, emit the final value now
+ if (Math.Abs(_dragValue - Value) > 0.001)
+ {
+ await ValueChanged.InvokeAsync(_dragValue);
+ }
+ }
+
+ _isDragging = false;
+ StateHasChanged();
+ }
+
+ private async Task UpdateValueFromMouse(MouseEventArgs e)
+ {
+ // Calculate vertical delta from last mouse position
+ double deltaY = _lastMouseY - e.ClientY; // Inverted: up = positive, down = negative
+ _lastMouseY = e.ClientY;
+
+ // Sensitivity factor (pixels to move for full range)
+ double sensitivity = 100.0;
+
+ // Calculate change in normalized value based on vertical movement
+ double normalizedDelta = deltaY / sensitivity;
+ double currentNormalized = GetNormalizedValue();
+ double newNormalized = Math.Max(0, Math.Min(1, currentNormalized + normalizedDelta));
+
+ // Convert to actual value
+ double newValue = Min + (newNormalized * (Max - Min));
+
+ // Apply step
+ if (Step > 0)
+ {
+ newValue = Math.Round(newValue / Step) * Step;
+ }
+
+ if (Math.Abs(newValue - (HoldValue ? _dragValue : Value)) > 0.001) // Avoid unnecessary updates
+ {
+ if (HoldValue)
+ {
+ // Update drag value only, don't emit ValueChanged yet
+ _dragValue = newValue;
+ StateHasChanged(); // Update visual only
+ }
+ else
+ {
+ // Original behavior - emit ValueChanged immediately
+ Value = newValue;
+ await ValueChanged.InvokeAsync(Value);
+ }
+ }
+ }
+}
\ No newline at end of file