diff --git a/src/AcDream.App/UI/UiItemSlot.cs b/src/AcDream.App/UI/UiItemSlot.cs
new file mode 100644
index 00000000..ebd1f33e
--- /dev/null
+++ b/src/AcDream.App/UI/UiItemSlot.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Numerics;
+
+namespace AcDream.App.UI;
+
+///
+/// One item-in-a-slot cell (port of retail UIElement_UIItem, class 0x10000032).
+/// A behavioral LEAF: it draws the empty-slot sprite when unbound, else a
+/// pre-composited icon texture (set by the controller). Holds the bound weenie
+/// guid (retail UIElement_UIItem::itemID, +0x5FC).
+///
+public sealed class UiItemSlot : UiElement
+{
+ public UiItemSlot() { ClickThrough = false; }
+
+ public override bool ConsumesDatChildren => true;
+
+ /// Bound weenie guid (0 = empty). Retail UIElement_UIItem::itemID.
+ public uint ItemId { get; private set; }
+
+ /// Pre-composited icon GL texture for the bound item (0 = none).
+ public uint IconTexture { get; private set; }
+
+ /// Empty-slot sprite. Default = the generic toolbar empty-slot border
+ /// 0x060074CF (uiitem template 0x21000037, state ItemSlot_Empty). Configurable so
+ /// paperdoll equip slots can use their per-slot silhouettes later.
+ public uint EmptySprite { get; set; } = 0x060074CFu;
+
+ /// RenderSurface id -> (GL texture, w, h). Set by the factory/controller.
+ public Func? SpriteResolve { get; set; }
+
+ public void SetItem(uint itemId, uint iconTexture)
+ {
+ ItemId = itemId;
+ IconTexture = iconTexture;
+ }
+
+ public void Clear() { ItemId = 0; IconTexture = 0; }
+
+ protected override void OnDraw(UiRenderContext ctx)
+ {
+ if (ItemId != 0 && IconTexture != 0)
+ {
+ ctx.DrawSprite(IconTexture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
+ return;
+ }
+ if (SpriteResolve is not null && EmptySprite != 0)
+ {
+ var (tex, _, _) = SpriteResolve(EmptySprite);
+ if (tex != 0)
+ ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
+ }
+ }
+}
diff --git a/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs b/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs
new file mode 100644
index 00000000..381c281c
--- /dev/null
+++ b/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs
@@ -0,0 +1,31 @@
+using AcDream.App.UI;
+
+namespace AcDream.App.Tests.UI;
+
+public class UiItemSlotTests
+{
+ [Fact]
+ public void IsLeafWidget()
+ => Assert.True(new UiItemSlot().ConsumesDatChildren);
+
+ [Fact]
+ public void DefaultEmptySprite_isToolbarBorder()
+ => Assert.Equal(0x060074CFu, new UiItemSlot().EmptySprite);
+
+ [Fact]
+ public void Empty_whenNoItem()
+ {
+ var s = new UiItemSlot();
+ Assert.Equal(0u, s.ItemId);
+ Assert.Equal(0u, s.IconTexture);
+ }
+
+ [Fact]
+ public void SetItem_setsIdAndTexture()
+ {
+ var s = new UiItemSlot();
+ s.SetItem(0x5001u, 0x99u);
+ Assert.Equal(0x5001u, s.ItemId);
+ Assert.Equal(0x99u, s.IconTexture);
+ }
+}