diff --git a/src/AcDream.App/UI/UiMeter.cs b/src/AcDream.App/UI/UiMeter.cs
new file mode 100644
index 00000000..9fcdd5d6
--- /dev/null
+++ b/src/AcDream.App/UI/UiMeter.cs
@@ -0,0 +1,40 @@
+using System.Numerics;
+
+namespace AcDream.App.UI;
+
+///
+/// A horizontal vital bar: an empty background rect with a partial-width
+/// fill. returns 0..1 (or null = no data → empty bar).
+/// Solid-color for Spec 1; the retail orb sprite + scissor crop is a later
+/// sub-phase.
+///
+public sealed class UiMeter : UiElement
+{
+ /// Fill fraction provider; a null result draws an empty bar.
+ public Func Fill { get; set; } = () => 0f;
+ public Vector4 BarColor { get; set; } = new(1f, 0f, 0f, 1f);
+ public Vector4 BgColor { get; set; } = new(0f, 0f, 0f, 0.5f);
+
+ public UiMeter() { ClickThrough = true; }
+
+ /// Clamp to [0,1] and return the fill rect
+ /// (local px) for a bar of x .
+ public static (float x, float y, float w, float h) ComputeFillRect(
+ float pct, float w, float h)
+ {
+ if (pct < 0f) pct = 0f;
+ if (pct > 1f) pct = 1f;
+ return (0f, 0f, w * pct, h);
+ }
+
+ protected override void OnDraw(UiRenderContext ctx)
+ {
+ ctx.DrawRect(0, 0, Width, Height, BgColor);
+ float? pct = Fill();
+ if (pct is float p)
+ {
+ var (fx, fy, fw, fh) = ComputeFillRect(p, Width, Height);
+ if (fw > 0f) ctx.DrawRect(fx, fy, fw, fh, BarColor);
+ }
+ }
+}
diff --git a/tests/AcDream.App.Tests/UI/UiMeterTests.cs b/tests/AcDream.App.Tests/UI/UiMeterTests.cs
new file mode 100644
index 00000000..9e7637e9
--- /dev/null
+++ b/tests/AcDream.App.Tests/UI/UiMeterTests.cs
@@ -0,0 +1,25 @@
+using AcDream.App.UI;
+
+namespace AcDream.App.Tests.UI;
+
+public class UiMeterTests
+{
+ [Fact]
+ public void ComputeFillRect_HalfFillIsHalfWidth()
+ {
+ var (x, y, w, h) = UiMeter.ComputeFillRect(0.5f, 200f, 12f);
+ Assert.Equal(0f, x); Assert.Equal(0f, y);
+ Assert.Equal(100f, w); Assert.Equal(12f, h);
+ }
+
+ [Theory]
+ [InlineData(-1f, 0f)] // clamps below 0
+ [InlineData(2f, 200f)] // clamps above 1
+ [InlineData(0f, 0f)]
+ [InlineData(1f, 200f)]
+ public void ComputeFillRect_ClampsFraction(float pct, float expectedW)
+ {
+ var (_, _, w, _) = UiMeter.ComputeFillRect(pct, 200f, 12f);
+ Assert.Equal(expectedW, w);
+ }
+}