# Popup System Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Build `com.jovian.popup-system`, a low-allocation popup/tooltip package with category-based isolation, fluent content builder, grow-only element cache, screen edge clamping, and extensible animations.

**Architecture:** Per-game-state `IPopupSystem` instances with registered categories. Each category lazily creates one `PopupReference` MonoBehaviour that reuses cached content elements. `PopupTrigger` MonoBehaviour handles hover detection. `PopupContentBuilder` struct operates directly on cached elements. Float-based timers in `Tick()` — no coroutines.

**Tech Stack:** Unity 6 / C# 9, TextMeshPro, Unity Input System, Newtonsoft.Json

---

### Task 1: Package scaffold

**Files:**
- Create: `Packages/com.jovian.popup-system/package.json`
- Create: `Packages/com.jovian.popup-system/Runtime/Jovian.PopupSystem.asmdef`
- Create: `Packages/com.jovian.popup-system/Editor/Jovian.PopupSystem.Editor.asmdef`
- Create: `Packages/com.jovian.popup-system/Samples~/README.md`

**Step 1: Create package.json**

```json
{
    "name": "com.jovian.popup-system",
    "version": "0.1.0",
    "displayName": "Jovian Popup System",
    "description": "A lightweight, low-allocation popup and tooltip system with category-based isolation, fluent content builder, and extensible animations.",
    "unity": "2022.3",
    "dependencies": {
        "com.unity.textmeshpro": "3.0.6",
        "com.unity.inputsystem": "1.18.0",
        "com.unity.nuget.newtonsoft-json": "3.2.1"
    },
    "keywords": [
        "popup",
        "tooltip",
        "ui"
    ],
    "author": {
        "name": "Jovian"
    }
}
```

**Step 2: Create Runtime asmdef**

```json
{
    "name": "Jovian.PopupSystem",
    "rootNamespace": "Jovian.PopupSystem",
    "references": [
        "Unity.TextMeshPro",
        "Unity.InputSystem"
    ],
    "includePlatforms": [],
    "excludePlatforms": [],
    "allowUnsafeCode": false,
    "overrideReferences": true,
    "precompiledReferences": [
        "Newtonsoft.Json.dll"
    ],
    "autoReferenced": true,
    "defineConstraints": [],
    "versionDefines": [],
    "noEngineReferences": false
}
```

**Step 3: Create Editor asmdef**

```json
{
    "name": "Jovian.PopupSystem.Editor",
    "rootNamespace": "Jovian.PopupSystem.Editor",
    "references": [
        "Jovian.PopupSystem"
    ],
    "includePlatforms": ["Editor"],
    "excludePlatforms": [],
    "allowUnsafeCode": false,
    "overrideReferences": false,
    "precompiledReferences": [],
    "autoReferenced": true,
    "defineConstraints": [],
    "versionDefines": [],
    "noEngineReferences": false
}
```

**Step 4: Create Samples~/README.md**

```markdown
# Samples

This folder is reserved for sample scenes and scripts demonstrating the Popup System.
```

**Step 5: Commit**

```bash
git add Packages/com.jovian.popup-system/
git commit -m "feat: scaffold com.jovian.popup-system package"
```

---

### Task 2: PopupCategory + JSON converter + enums

**Files:**
- Create: `Packages/com.jovian.popup-system/Runtime/PopupCategory.cs`
- Create: `Packages/com.jovian.popup-system/Runtime/PopupCategoryJsonConverter.cs`
- Create: `Packages/com.jovian.popup-system/Runtime/PopupEnums.cs`

**Step 1: Implement PopupCategory** (same pattern as LogChannel)

```csharp
using System;
using UnityEngine;

namespace Jovian.PopupSystem {
    [Serializable]
    public readonly struct PopupCategory : IEquatable<PopupCategory> {
        [SerializeField] readonly string id;

        public string Id => id;

        public PopupCategory(string id) {
            this.id = id;
        }

        public static readonly PopupCategory Character = new("Character");
        public static readonly PopupCategory Item = new("Item");
        public static readonly PopupCategory Skill = new("Skill");
        public static readonly PopupCategory General = new("General");

        public bool Equals(PopupCategory other) {
            return string.Equals(id, other.id, StringComparison.Ordinal);
        }

        public override bool Equals(object obj) {
            return obj is PopupCategory other && Equals(other);
        }

        public override int GetHashCode() {
            return id != null ? id.GetHashCode() : 0;
        }

        public override string ToString() {
            return id ?? string.Empty;
        }

        public static bool operator ==(PopupCategory left, PopupCategory right) {
            return left.Equals(right);
        }

        public static bool operator !=(PopupCategory left, PopupCategory right) {
            return !left.Equals(right);
        }
    }
}
```

**Step 2: Implement JSON converter**

```csharp
using System;
using Newtonsoft.Json;

namespace Jovian.PopupSystem {
    public sealed class PopupCategoryJsonConverter : JsonConverter<PopupCategory> {
        public override void WriteJson(JsonWriter writer, PopupCategory value, JsonSerializer serializer) {
            writer.WriteValue(value.Id);
        }

        public override PopupCategory ReadJson(JsonReader reader, Type objectType, PopupCategory existingValue, bool hasExistingValue, JsonSerializer serializer) {
            var id = reader.Value as string;
            return new PopupCategory(id);
        }
    }
}
```

**Step 3: Implement enums**

```csharp
namespace Jovian.PopupSystem {
    public enum AnchorSide {
        Below,
        Above,
        Left,
        Right
    }

    public enum PopupPositionMode {
        AnchorToElement,
        FollowMouse
    }
}
```

**Step 4: Commit**

```bash
git add Packages/com.jovian.popup-system/Runtime/
git commit -m "feat: add PopupCategory, enums, and JSON converter"
```

---

### Task 3: PopupSettings ScriptableObject

**Files:**
- Create: `Packages/com.jovian.popup-system/Runtime/PopupSettings.cs`

**Step 1: Implement settings**

```csharp
using System;
using System.Collections.Generic;
using UnityEngine;

namespace Jovian.PopupSystem {
    [CreateAssetMenu(fileName = "PopupSettings", menuName = "Jovian/Popup System/Popup Settings")]
    public class PopupSettings : ScriptableObject {
        [Header("General")]
        public float popupDelay = 0.4f;
        public float fadeDuration = 0.2f;
        public AnchorSide defaultAnchorSide = AnchorSide.Below;
        public float screenEdgePadding = 10f;
        public float maxPopupWidth = 400f;
        public int sortingOrder = 100;

        [Header("Follow Mouse")]
        public Vector2 followMouseOffset = new(15f, -15f);

        [Header("Input")]
        public float touchHoldDuration = 0.6f;
        public bool gamepadFocusTrigger = true;

        [Header("Priority")]
        public List<CategoryPriority> categoryPriorities = new();

        [Header("Per-Category Overrides")]
        public List<CategoryDelay> categoryDelayOverrides = new();

        public int GetPriority(PopupCategory category) {
            foreach(var cp in categoryPriorities) {
                if(cp.category == category) {
                    return cp.priority;
                }
            }
            return 0;
        }

        public float GetDelay(PopupCategory category) {
            foreach(var cd in categoryDelayOverrides) {
                if(cd.category == category) {
                    return cd.delay;
                }
            }
            return popupDelay;
        }
    }

    [Serializable]
    public sealed class CategoryPriority {
        public PopupCategory category;
        public int priority;
    }

    [Serializable]
    public sealed class CategoryDelay {
        public PopupCategory category;
        public float delay;
    }
}
```

**Step 2: Commit**

```bash
git add Packages/com.jovian.popup-system/Runtime/PopupSettings.cs
git commit -m "feat: add PopupSettings ScriptableObject with full configuration"
```

---

### Task 4: IPopupAnimator + FadePopupAnimator

**Files:**
- Create: `Packages/com.jovian.popup-system/Runtime/IPopupAnimator.cs`
- Create: `Packages/com.jovian.popup-system/Runtime/FadePopupAnimator.cs`

**Step 1: Implement interface**

```csharp
using System;
using UnityEngine;

namespace Jovian.PopupSystem {
    public interface IPopupAnimator {
        void Show(CanvasGroup canvasGroup, float duration, Action onComplete);
        void Hide(CanvasGroup canvasGroup, float duration, Action onComplete);
        void Tick(float deltaTime);
        bool IsAnimating { get; }
    }
}
```

**Step 2: Implement FadePopupAnimator**

```csharp
using System;
using UnityEngine;

namespace Jovian.PopupSystem {
    public sealed class FadePopupAnimator : IPopupAnimator {
        private CanvasGroup target;
        private float duration;
        private float elapsed;
        private float startAlpha;
        private float endAlpha;
        private Action onComplete;

        public bool IsAnimating => target != null;

        public void Show(CanvasGroup canvasGroup, float duration, Action onComplete) {
            target = canvasGroup;
            this.duration = Mathf.Max(duration, 0.001f);
            elapsed = 0f;
            startAlpha = 0f;
            endAlpha = 1f;
            this.onComplete = onComplete;
            canvasGroup.alpha = 0f;
        }

        public void Hide(CanvasGroup canvasGroup, float duration, Action onComplete) {
            target = canvasGroup;
            this.duration = Mathf.Max(duration, 0.001f);
            elapsed = 0f;
            startAlpha = canvasGroup.alpha;
            endAlpha = 0f;
            this.onComplete = onComplete;
        }

        public void Tick(float deltaTime) {
            if(target == null) {
                return;
            }

            elapsed += deltaTime;
            var t = Mathf.Clamp01(elapsed / duration);
            target.alpha = Mathf.Lerp(startAlpha, endAlpha, t);

            if(t >= 1f) {
                var callback = onComplete;
                target = null;
                onComplete = null;
                callback?.Invoke();
            }
        }
    }
}
```

**Step 3: Commit**

```bash
git add Packages/com.jovian.popup-system/Runtime/IPopupAnimator.cs Packages/com.jovian.popup-system/Runtime/FadePopupAnimator.cs
git commit -m "feat: add IPopupAnimator interface and FadePopupAnimator"
```

---

### Task 5: PopupReference MonoBehaviour with element cache

**Files:**
- Create: `Packages/com.jovian.popup-system/Runtime/UI/PopupReference.cs`

**Step 1: Implement PopupReference with grow-only element cache, positioning, and screen clamping**

```csharp
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;

namespace Jovian.PopupSystem.UI {
    public class PopupReference : MonoBehaviour {
        [SerializeField] RectTransform content;
        [SerializeField] CanvasGroup canvasGroup;
        [SerializeField] RectTransform background;

        [Header("Element Prefabs")]
        [SerializeField] TMP_Text headerPrefab;
        [SerializeField] TMP_Text textPrefab;
        [SerializeField] RectTransform statPrefab; // contains label + value TMP_Text
        [SerializeField] Image imagePrefab;
        [SerializeField] Image separatorPrefab;

        // Element caches (grow-only)
        readonly List<TMP_Text> headerCache = new();
        readonly List<TMP_Text> textCache = new();
        readonly List<RectTransform> statCache = new();
        readonly List<Image> imageCache = new();
        readonly List<Image> separatorCache = new();

        int headerIndex;
        int textIndex;
        int statIndex;
        int imageIndex;
        int separatorIndex;

        // State
        PopupPositionMode positionMode;
        RectTransform anchorTarget;
        AnchorSide anchorSide;
        Vector2 followOffset;
        float screenEdgePadding;
        bool isVisible;

        public CanvasGroup CanvasGroup => canvasGroup;
        public RectTransform Content => content;
        public bool IsVisible => isVisible;

        public void SetVisible(bool visible) {
            isVisible = visible;
            gameObject.SetActive(visible);
            if(!visible) {
                canvasGroup.alpha = 0f;
            }
        }

        public void ClearContent() {
            for(int i = 0; i < headerIndex; i++) {
                headerCache[i].gameObject.SetActive(false);
            }
            for(int i = 0; i < textIndex; i++) {
                textCache[i].gameObject.SetActive(false);
            }
            for(int i = 0; i < statIndex; i++) {
                statCache[i].gameObject.SetActive(false);
            }
            for(int i = 0; i < imageIndex; i++) {
                imageCache[i].gameObject.SetActive(false);
            }
            for(int i = 0; i < separatorIndex; i++) {
                separatorCache[i].gameObject.SetActive(false);
            }
            headerIndex = 0;
            textIndex = 0;
            statIndex = 0;
            imageIndex = 0;
            separatorIndex = 0;
        }

        // --- Element access (grow-only) ---

        public TMP_Text GetHeader() {
            return GetOrCreate(headerCache, headerPrefab, ref headerIndex);
        }

        public TMP_Text GetText() {
            return GetOrCreate(textCache, textPrefab, ref textIndex);
        }

        public (TMP_Text label, TMP_Text value) GetStat() {
            var rt = GetOrCreate(statCache, statPrefab, ref statIndex);
            var children = rt.GetComponentsInChildren<TMP_Text>(true);
            return (children[0], children[1]);
        }

        public Image GetImage() {
            return GetOrCreate(imageCache, imagePrefab, ref imageIndex);
        }

        public Image GetSeparator() {
            return GetOrCreate(separatorCache, separatorPrefab, ref separatorIndex);
        }

        private T GetOrCreate<T>(List<T> cache, T prefab, ref int index) where T : Component {
            if(index < cache.Count) {
                var existing = cache[index];
                existing.gameObject.SetActive(true);
                existing.transform.SetAsLastSibling();
                index++;
                return existing;
            }

            var created = Instantiate(prefab, content);
            created.gameObject.SetActive(true);
            created.transform.SetAsLastSibling();
            cache.Add(created);
            index++;
            return created;
        }

        // --- Positioning ---

        public void SetAnchorMode(RectTransform target, AnchorSide side, float edgePadding) {
            positionMode = PopupPositionMode.AnchorToElement;
            anchorTarget = target;
            anchorSide = side;
            screenEdgePadding = edgePadding;
        }

        public void SetFollowMouseMode(Vector2 offset, float edgePadding) {
            positionMode = PopupPositionMode.FollowMouse;
            followOffset = offset;
            screenEdgePadding = edgePadding;
        }

        public void UpdatePosition() {
            if(positionMode == PopupPositionMode.FollowMouse) {
                PositionAtScreenPoint(Mouse.current.position.ReadValue() + followOffset);
            }
            else if(anchorTarget != null) {
                PositionAnchoredTo(anchorTarget, anchorSide);
            }
        }

        private void PositionAnchoredTo(RectTransform target, AnchorSide side) {
            var targetRect = GetScreenRect(target);
            var popupRect = GetScreenRect((RectTransform)transform);
            var popupSize = popupRect.size;

            var pos = side switch {
                AnchorSide.Below => new Vector2(targetRect.center.x - popupSize.x * 0.5f, targetRect.yMin - popupSize.y),
                AnchorSide.Above => new Vector2(targetRect.center.x - popupSize.x * 0.5f, targetRect.yMax),
                AnchorSide.Left => new Vector2(targetRect.xMin - popupSize.x, targetRect.center.y - popupSize.y * 0.5f),
                AnchorSide.Right => new Vector2(targetRect.xMax, targetRect.center.y - popupSize.y * 0.5f),
                _ => targetRect.center
            };

            PositionAtScreenPoint(pos);
        }

        private void PositionAtScreenPoint(Vector2 screenPos) {
            var rt = (RectTransform)transform;
            LayoutRebuilder.ForceRebuildLayoutImmediate(content);
            var popupSize = rt.rect.size;

            // Clamp to screen
            screenPos.x = Mathf.Clamp(screenPos.x, screenEdgePadding, Screen.width - popupSize.x - screenEdgePadding);
            screenPos.y = Mathf.Clamp(screenPos.y, screenEdgePadding, Screen.height - popupSize.y - screenEdgePadding);

            rt.position = screenPos;
        }

        private static Rect GetScreenRect(RectTransform rt) {
            var corners = new Vector3[4];
            rt.GetWorldCorners(corners);
            var min = new Vector2(corners[0].x, corners[0].y);
            var max = new Vector2(corners[2].x, corners[2].y);
            return new Rect(min, max - min);
        }
    }
}
```

**Step 2: Commit**

```bash
git add Packages/com.jovian.popup-system/Runtime/UI/PopupReference.cs
git commit -m "feat: add PopupReference with grow-only element cache and positioning"
```

---

### Task 6: PopupContentBuilder struct

**Files:**
- Create: `Packages/com.jovian.popup-system/Runtime/PopupContentBuilder.cs`

**Step 1: Implement fluent builder**

```csharp
using Jovian.PopupSystem.UI;
using UnityEngine;

namespace Jovian.PopupSystem {
    public struct PopupContentBuilder {
        readonly PopupReference view;

        public PopupContentBuilder(PopupReference view) {
            this.view = view;
        }

        public PopupContentBuilder AddHeader(string text) {
            var header = view.GetHeader();
            header.text = text;
            return this;
        }

        public PopupContentBuilder AddText(string text) {
            var label = view.GetText();
            label.text = text;
            return this;
        }

        public PopupContentBuilder AddText(string text, string hexColor) {
            var label = view.GetText();
            var prefix = text.Length > 0 && hexColor[0] == '#' ? "" : "#";
            label.text = $"<color={prefix}{hexColor}>{text}</color>";
            return this;
        }

        public PopupContentBuilder AddStat(string label, int value) {
            var (labelText, valueText) = view.GetStat();
            labelText.text = label;
            valueText.text = value.ToString();
            return this;
        }

        public PopupContentBuilder AddStat(string label, string value) {
            var (labelText, valueText) = view.GetStat();
            labelText.text = label;
            valueText.text = value;
            return this;
        }

        public PopupContentBuilder AddImage(Sprite sprite, float height = 64f) {
            var image = view.GetImage();
            image.sprite = sprite;
            var rt = (RectTransform)image.transform;
            rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, height);
            return this;
        }

        public PopupContentBuilder AddSeparator() {
            view.GetSeparator();
            return this;
        }
    }
}
```

**Step 2: Commit**

```bash
git add Packages/com.jovian.popup-system/Runtime/PopupContentBuilder.cs
git commit -m "feat: add PopupContentBuilder fluent API"
```

---

### Task 7: IPopupSystem + PopupSystem implementation

**Files:**
- Create: `Packages/com.jovian.popup-system/Runtime/IPopupSystem.cs`
- Create: `Packages/com.jovian.popup-system/Runtime/PopupSystem.cs`

**Step 1: Implement interface**

```csharp
using System;
using UnityEngine;

namespace Jovian.PopupSystem {
    public interface IPopupSystem {
        void RegisterCategory(PopupCategory category, int priority = 0);
        void Show(PopupCategory category, Action<PopupContentBuilder> buildContent,
                  RectTransform anchor = null, AnchorSide? anchorSide = null);
        void ShowAtPosition(PopupCategory category, Action<PopupContentBuilder> buildContent,
                            Vector2 screenPosition);
        void Hide(PopupCategory category);
        void HideAll();
        void Tick(float deltaTime);
        void Dispose();
    }
}
```

**Step 2: Implement PopupSystem**

```csharp
using System;
using System.Collections.Generic;
using Jovian.PopupSystem.UI;
using UnityEngine;
using Object = UnityEngine.Object;

namespace Jovian.PopupSystem {
    public sealed class PopupSystem : IPopupSystem {
        readonly PopupSettings settings;
        readonly PopupReference viewPrefab;
        readonly IPopupAnimator animator;
        readonly Dictionary<PopupCategory, ViewState> categories = new();

        public PopupSystem(PopupSettings settings, PopupReference viewPrefab, IPopupAnimator animator = null) {
            this.settings = settings;
            this.viewPrefab = viewPrefab;
            this.animator = animator ?? new FadePopupAnimator();
        }

        public void RegisterCategory(PopupCategory category, int priority = 0) {
            if(categories.ContainsKey(category)) {
                return;
            }
            var effectivePriority = settings.GetPriority(category);
            if(effectivePriority == 0) {
                effectivePriority = priority;
            }
            categories[category] = new ViewState {
                priority = effectivePriority,
                delay = settings.GetDelay(category)
            };
        }

        public void Show(PopupCategory category, Action<PopupContentBuilder> buildContent,
                         RectTransform anchor = null, AnchorSide? anchorSide = null) {
            if(!categories.TryGetValue(category, out var state)) {
                return;
            }

            // Dismiss lower-priority visible popups
            DismissLowerPriority(state.priority);

            state.pendingBuild = buildContent;
            state.pendingAnchor = anchor;
            state.pendingAnchorSide = anchorSide ?? settings.defaultAnchorSide;
            state.pendingScreenPos = null;
            state.delayTimer = state.delay;
            state.isPending = true;
        }

        public void ShowAtPosition(PopupCategory category, Action<PopupContentBuilder> buildContent,
                                   Vector2 screenPosition) {
            if(!categories.TryGetValue(category, out var state)) {
                return;
            }

            DismissLowerPriority(state.priority);

            state.pendingBuild = buildContent;
            state.pendingAnchor = null;
            state.pendingScreenPos = screenPosition;
            state.delayTimer = state.delay;
            state.isPending = true;
        }

        public void Hide(PopupCategory category) {
            if(!categories.TryGetValue(category, out var state)) {
                return;
            }

            state.isPending = false;
            if(state.view != null && state.view.IsVisible) {
                animator.Hide(state.view.CanvasGroup, settings.fadeDuration, () => {
                    state.view.SetVisible(false);
                });
            }
        }

        public void HideAll() {
            foreach(var kvp in categories) {
                Hide(kvp.Key);
            }
        }

        public void Tick(float deltaTime) {
            animator.Tick(deltaTime);

            foreach(var kvp in categories) {
                var state = kvp.Value;
                if(!state.isPending) {
                    continue;
                }

                state.delayTimer -= deltaTime;
                if(state.delayTimer > 0f) {
                    continue;
                }

                state.isPending = false;
                ShowImmediate(state);
            }

            // Update follow-mouse positions
            foreach(var kvp in categories) {
                var state = kvp.Value;
                if(state.view != null && state.view.IsVisible && state.isFollowMouse) {
                    state.view.UpdatePosition();
                }
            }
        }

        public void Dispose() {
            foreach(var kvp in categories) {
                if(kvp.Value.view != null) {
                    Object.Destroy(kvp.Value.view.gameObject);
                }
            }
            categories.Clear();
        }

        private void ShowImmediate(ViewState state) {
            EnsureView(state);
            state.view.ClearContent();

            var builder = new PopupContentBuilder(state.view);
            state.pendingBuild?.Invoke(builder);

            if(state.pendingScreenPos.HasValue) {
                state.view.SetFollowMouseMode(settings.followMouseOffset, settings.screenEdgePadding);
                state.isFollowMouse = false; // positioned at fixed screen point
            }
            else if(state.pendingAnchor != null) {
                state.view.SetAnchorMode(state.pendingAnchor, state.pendingAnchorSide, settings.screenEdgePadding);
                state.isFollowMouse = false;
            }
            else {
                state.view.SetFollowMouseMode(settings.followMouseOffset, settings.screenEdgePadding);
                state.isFollowMouse = true;
            }

            state.view.SetVisible(true);
            state.view.UpdatePosition();
            animator.Show(state.view.CanvasGroup, settings.fadeDuration, null);
        }

        private void EnsureView(ViewState state) {
            if(state.view != null) {
                return;
            }
            state.view = Object.Instantiate(viewPrefab);
            state.view.SetVisible(false);

            var canvas = state.view.GetComponent<Canvas>();
            if(canvas != null) {
                canvas.sortingOrder = settings.sortingOrder;
            }
        }

        private void DismissLowerPriority(int showingPriority) {
            foreach(var kvp in categories) {
                var state = kvp.Value;
                if(state.priority < showingPriority && state.view != null && state.view.IsVisible) {
                    state.isPending = false;
                    animator.Hide(state.view.CanvasGroup, settings.fadeDuration, () => {
                        state.view.SetVisible(false);
                    });
                }
            }
        }

        private sealed class ViewState {
            public PopupReference view;
            public int priority;
            public float delay;
            public float delayTimer;
            public bool isPending;
            public bool isFollowMouse;
            public Action<PopupContentBuilder> pendingBuild;
            public RectTransform pendingAnchor;
            public AnchorSide pendingAnchorSide;
            public Vector2? pendingScreenPos;
        }
    }
}
```

**Step 3: Commit**

```bash
git add Packages/com.jovian.popup-system/Runtime/IPopupSystem.cs Packages/com.jovian.popup-system/Runtime/PopupSystem.cs
git commit -m "feat: add IPopupSystem and PopupSystem with priority and delay"
```

---

### Task 8: PopupTrigger MonoBehaviour

**Files:**
- Create: `Packages/com.jovian.popup-system/Runtime/UI/PopupTrigger.cs`

**Step 1: Implement trigger**

```csharp
using System;
using UnityEngine;
using UnityEngine.EventSystems;

namespace Jovian.PopupSystem.UI {
    public class PopupTrigger : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler {
        [SerializeField] PopupCategory category;
        [SerializeField] AnchorSide anchorSide = AnchorSide.Below;
        [SerializeField] PopupPositionMode positionMode = PopupPositionMode.AnchorToElement;

        IPopupSystem popupSystem;
        Action<PopupContentBuilder> contentCallback;
        bool initialized;

        public PopupCategory Category => category;

        public void Initialize(IPopupSystem popupSystem, Action<PopupContentBuilder> contentCallback) {
            this.popupSystem = popupSystem;
            this.contentCallback = contentCallback;
            initialized = true;
        }

        public void Initialize(IPopupSystem popupSystem, PopupCategory category, Action<PopupContentBuilder> contentCallback) {
            this.popupSystem = popupSystem;
            this.category = category;
            this.contentCallback = contentCallback;
            initialized = true;
        }

        public void UpdateContent(Action<PopupContentBuilder> contentCallback) {
            this.contentCallback = contentCallback;
        }

        public void OnPointerEnter(PointerEventData eventData) {
            if(!initialized || popupSystem == null || contentCallback == null) {
                return;
            }

            if(positionMode == PopupPositionMode.AnchorToElement) {
                popupSystem.Show(category, contentCallback, (RectTransform)transform, anchorSide);
            }
            else {
                popupSystem.Show(category, contentCallback);
            }
        }

        public void OnPointerExit(PointerEventData eventData) {
            if(!initialized || popupSystem == null) {
                return;
            }
            popupSystem.Hide(category);
        }
    }
}
```

**Step 2: Commit**

```bash
git add Packages/com.jovian.popup-system/Runtime/UI/PopupTrigger.cs
git commit -m "feat: add PopupTrigger MonoBehaviour for hover detection"
```

---

### Task 9: README.md

**Files:**
- Create: `Packages/com.jovian.popup-system/README.md`

**Step 1: Write README** covering: quick start, PopupCategory, PopupSettings, content builder API, PopupTrigger setup, code-only triggers, prefab setup, IPopupAnimator extensibility.

**Step 2: Commit**

```bash
git add Packages/com.jovian.popup-system/README.md
git commit -m "feat: add README for popup system package"
```

---

### Task 10: Editor — PopupSettingsProvider

**Files:**
- Create: `Packages/com.jovian.popup-system/Editor/PopupSettingsProvider.cs`

**Step 1: Implement settings provider** for Project Settings > Jovian > Popup System (same pattern as SaveSystemSettingsProvider).

**Step 2: Commit**

```bash
git add Packages/com.jovian.popup-system/Editor/PopupSettingsProvider.cs
git commit -m "feat: add PopupSettingsProvider for Project Settings"
```

---

## File Summary

| File | Type | Purpose |
|------|------|---------|
| `Packages/com.jovian.popup-system/package.json` | Create | Package manifest |
| `Packages/com.jovian.popup-system/Runtime/Jovian.PopupSystem.asmdef` | Create | Runtime assembly |
| `Packages/com.jovian.popup-system/Editor/Jovian.PopupSystem.Editor.asmdef` | Create | Editor assembly |
| `Packages/com.jovian.popup-system/Samples~/README.md` | Create | Samples placeholder |
| `Packages/com.jovian.popup-system/Runtime/PopupCategory.cs` | Create | Category readonly struct |
| `Packages/com.jovian.popup-system/Runtime/PopupCategoryJsonConverter.cs` | Create | Newtonsoft converter |
| `Packages/com.jovian.popup-system/Runtime/PopupEnums.cs` | Create | AnchorSide, PopupPositionMode |
| `Packages/com.jovian.popup-system/Runtime/PopupSettings.cs` | Create | ScriptableObject config |
| `Packages/com.jovian.popup-system/Runtime/IPopupAnimator.cs` | Create | Animation interface |
| `Packages/com.jovian.popup-system/Runtime/FadePopupAnimator.cs` | Create | Default fade animation |
| `Packages/com.jovian.popup-system/Runtime/PopupContentBuilder.cs` | Create | Fluent builder struct |
| `Packages/com.jovian.popup-system/Runtime/IPopupSystem.cs` | Create | System interface |
| `Packages/com.jovian.popup-system/Runtime/PopupSystem.cs` | Create | System implementation |
| `Packages/com.jovian.popup-system/Runtime/UI/PopupReference.cs` | Create | MonoBehaviour with element cache |
| `Packages/com.jovian.popup-system/Runtime/UI/PopupTrigger.cs` | Create | Hover trigger MonoBehaviour |
| `Packages/com.jovian.popup-system/README.md` | Create | Package documentation |
| `Packages/com.jovian.popup-system/Editor/PopupSettingsProvider.cs` | Create | Project Settings UI |
