// ui_widgets.cpp - Immediate-mode widget implementations on top of Clay.
//
// IMPORTANT: Clay_Hovered() only works inside CLAY() macro declaration args
// (where the element is the "open" element on the stack). For hover checks
// AFTER a CLAY() block, use Clay_PointerOver(elementId).

#include "ui/ui_widgets.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

UI_WidgetState g_wstate = {};

// Icon per-frame pool (forward declaration for begin_frame)
#define UI_MAX_ICONS_PER_FRAME 32
static CustomIconData g_icon_pool[UI_MAX_ICONS_PER_FRAME];
static S32 g_icon_pool_count = 0;

// Gradient per-frame pool (for button/title bar/dropdown/input gradients)
#define UI_MAX_GRADIENTS_PER_FRAME 64
static CustomGradientData g_grad_pool[UI_MAX_GRADIENTS_PER_FRAME];
static S32 g_grad_pool_count = 0;

// Rotated icon per-frame pool (for knobs)
#define UI_MAX_ROTATED_ICONS_PER_FRAME 16
static CustomRotatedIconData g_rotated_icon_pool[UI_MAX_ROTATED_ICONS_PER_FRAME];
static S32 g_rotated_icon_pool_count = 0;

// Static buffer pool for knob value text
#define UI_MAX_KNOB_TEXT_BUFS 16
static char g_knob_text_bufs[UI_MAX_KNOB_TEXT_BUFS][32];
static S32 g_knob_text_buf_count = 0;

static CustomGradientData *alloc_gradient(Clay_Color top, Clay_Color bottom) {
    if (g_grad_pool_count >= UI_MAX_GRADIENTS_PER_FRAME) return nullptr;
    CustomGradientData *g = &g_grad_pool[g_grad_pool_count++];
    g->type = CUSTOM_RENDER_VGRADIENT;
    g->top_color = top;
    g->bottom_color = bottom;
    return g;
}

// Per-frame shadow layer ID counter (each shadow uses N unique IDs)
static S32 g_shadow_id_counter = 0;

// Frame counter for F64-click detection
static S32 g_frame_number = 0;

// Emit a smooth multi-layer drop shadow as floating rects.
// bb: bounding box of the element to shadow (previous frame)
// ox/oy: directional offset (light direction)
// radius: max blur spread
// peak_alpha: total alpha at center when all layers stack
// z: zIndex for shadow layers
// attach_to_root: if true, use absolute positioning; if false, attach to parent_id
// parent_id: only used when attach_to_root=false
// parent_attach: attachment point on parent (e.g. LEFT_BOTTOM for dropdowns)
#define SHADOW_LAYERS 7

static void emit_shadow(Clay_BoundingBox bb, F32 ox, F32 oy, F32 radius,
                        F32 peak_alpha, S16 z,
                        B32 attach_to_root, U32 parent_id,
                        Clay_FloatingAttachPointType parent_attach) {
    if (bb.width <= 0) return;

    F32 per_layer = peak_alpha / (F32)SHADOW_LAYERS;

    // Draw outermost first (largest, lowest alpha contribution), innermost last
    for (S32 i = SHADOW_LAYERS - 1; i >= 0; i--) {
        F32 t = (F32)(i + 1) / (F32)SHADOW_LAYERS;  // 1/N .. 1.0
        F32 expand = radius * t;

        S32 sid = g_shadow_id_counter++;
        Clay_ElementId shadow_eid = CLAY_IDI("Shadow", sid);

        if (attach_to_root) {
            CLAY(shadow_eid,
                .layout = {
                    .sizing = {
                        .width = CLAY_SIZING_FIXED(bb.width + expand*2),
                        .height = CLAY_SIZING_FIXED(bb.height + expand*2),
                    },
                },
                .backgroundColor = {0, 0, 0, per_layer},
                .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS + expand),
                .floating = {
                    .offset = { bb.x - expand + ox, bb.y - expand + oy },
                    .zIndex = z,
                    .attachPoints = {
                        .element = CLAY_ATTACH_POINT_LEFT_TOP,
                        .parent = CLAY_ATTACH_POINT_LEFT_TOP,
                    },
                    .attachTo = CLAY_ATTACH_TO_ROOT,
                }
            ) {}
        } else {
            CLAY(shadow_eid,
                .layout = {
                    .sizing = {
                        .width = CLAY_SIZING_FIXED(bb.width + expand*2),
                        .height = CLAY_SIZING_FIXED(bb.height + expand*2),
                    },
                },
                .backgroundColor = {0, 0, 0, per_layer},
                .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS + expand),
                .floating = {
                    .offset = { -expand + ox, -expand + oy },
                    .parentId = parent_id,
                    .zIndex = z,
                    .attachPoints = {
                        .element = CLAY_ATTACH_POINT_LEFT_TOP,
                        .parent = parent_attach,
                    },
                    .attachTo = CLAY_ATTACH_TO_ELEMENT_WITH_ID,
                }
            ) {}
        }
    }
}

void ui_widgets_init() {
    g_wstate = {};
}

void ui_widgets_begin_frame(PlatformInput input) {
    g_wstate.input = input;
    g_wstate.mouse_clicked = (input.mouse_down && !input.was_mouse_down);
    g_icon_pool_count = 0;
    g_grad_pool_count = 0;
    g_rotated_icon_pool_count = 0;
    g_knob_text_buf_count = 0;
    g_shadow_id_counter = 0;
    g_frame_number++;

    // Release knob drag if mouse is up
    if (!input.mouse_down && g_wstate.knob_drag.dragging_id != 0) {
        g_wstate.knob_drag.dragging_id = 0;
    }
    g_wstate.cursor_blink += 1.0f / 60.0f;
    g_wstate.text_input_count = 0;
    g_wstate.tab_pressed = 0;

    // Detect Tab key press this frame
    for (S32 k = 0; k < input.key_count; k++) {
        if (input.keys[k] == PKEY_TAB) {
            g_wstate.tab_pressed = 1;
            break;
        }
    }

    // Drag cleanup: if mouse was released between frames, end any active drag
    if (g_wstate.drag.dragging_id != 0 && !input.mouse_down) {
        g_wstate.drag.dragging_id = 0;
    }

    ui_text_input_reset_display_bufs();
}

////////////////////////////////
// Helpers

static Clay_TextElementConfig g_widget_text_config;
static Clay_TextElementConfig g_widget_text_config_dim;
static Clay_TextElementConfig g_widget_text_config_sel;
static Clay_TextElementConfig g_widget_text_config_btn;
static Clay_TextElementConfig g_widget_text_config_tab;
static Clay_TextElementConfig g_widget_text_config_tab_inactive;
static F32 g_widget_text_configs_scale = 0;

void ui_widgets_theme_changed() {
    g_widget_text_configs_scale = 0;
}

static void ensure_widget_text_configs() {
    if (g_widget_text_configs_scale == g_ui_scale) return;
    g_widget_text_configs_scale = g_ui_scale;

    g_widget_text_config = {};
    g_widget_text_config.textColor = g_theme.text;
    g_widget_text_config.fontSize = FONT_SIZE_NORMAL;
    g_widget_text_config.wrapMode = CLAY_TEXT_WRAP_NONE;

    g_widget_text_config_dim = {};
    g_widget_text_config_dim.textColor = g_theme.text_dim;
    g_widget_text_config_dim.fontSize = FONT_SIZE_NORMAL;
    g_widget_text_config_dim.wrapMode = CLAY_TEXT_WRAP_NONE;

    // Selected text: white on accent background (background set on parent element)
    g_widget_text_config_sel = {};
    g_widget_text_config_sel.textColor = Clay_Color{255, 255, 255, 255};
    g_widget_text_config_sel.fontSize = FONT_SIZE_NORMAL;
    g_widget_text_config_sel.wrapMode = CLAY_TEXT_WRAP_NONE;

    // Button text (always readable on accent background)
    g_widget_text_config_btn = {};
    g_widget_text_config_btn.textColor = g_theme.button_text;
    g_widget_text_config_btn.fontSize = FONT_SIZE_NORMAL;
    g_widget_text_config_btn.wrapMode = CLAY_TEXT_WRAP_NONE;

    // Tab text (always light — readable on colored tab gradient)
    g_widget_text_config_tab = {};
    g_widget_text_config_tab.textColor = g_theme.tab_text;
    g_widget_text_config_tab.fontSize = FONT_SIZE_TAB;
    g_widget_text_config_tab.wrapMode = CLAY_TEXT_WRAP_NONE;

    // Inactive tab text (theme text color, smaller font)
    g_widget_text_config_tab_inactive = {};
    g_widget_text_config_tab_inactive.textColor = g_theme.text;
    g_widget_text_config_tab_inactive.fontSize = FONT_SIZE_TAB;
    g_widget_text_config_tab_inactive.wrapMode = CLAY_TEXT_WRAP_NONE;
}

static Clay_String clay_str(const char *s) {
    Clay_String r = {};
    r.isStaticallyAllocated = false;
    r.length = (S32)strlen(s);
    r.chars = s;
    return r;
}

// Build Clay_ElementId from runtime string (CLAY_ID requires string literals)
#define WID(s)       CLAY_SID(clay_str(s))
#define WIDI(s, i)   CLAY_SIDI(clay_str(s), i)

////////////////////////////////
// Icon

void ui_icon(UI_IconID icon, F32 size, Clay_Color color) {
    if (g_icon_pool_count >= UI_MAX_ICONS_PER_FRAME) return;

    S32 idx = g_icon_pool_count;
    CustomIconData *data = &g_icon_pool[g_icon_pool_count++];
    data->type = CUSTOM_RENDER_ICON;
    data->icon_id = (S32)icon;
    data->color = color;

    CLAY(CLAY_IDI("UIIcon", idx),
        .layout = {
            .sizing = { .width = CLAY_SIZING_FIXED(size), .height = CLAY_SIZING_FIXED(size) },
        },
        .custom = { .customData = data }
    ) {}
}

////////////////////////////////
// Label

void ui_label(const char *id, const char *text) {
    ensure_widget_text_configs();
    CLAY(WID(id),
        .layout = {
            .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
            .padding = { 0, 0, uip(2), uip(2) },
        }
    ) {
        CLAY_TEXT(clay_str(text), &g_widget_text_config);
    }
}

////////////////////////////////
// Button

B32 ui_button(const char *id, const char *text) {
    ensure_widget_text_configs();

    Clay_ElementId eid = WID(id);
    B32 hovered = Clay_PointerOver(eid);

    Clay_Color base = hovered ? g_theme.accent_hover : g_theme.accent;
    Clay_Color top = {(F32)Min((S32)base.r+12,255), (F32)Min((S32)base.g+12,255), (F32)Min((S32)base.b+12,255), base.a};
    Clay_Color bot = {(F32)Max((S32)base.r-15,0), (F32)Max((S32)base.g-15,0), (F32)Max((S32)base.b-15,0), base.a};
    CustomGradientData *grad = alloc_gradient(top, bot);

    CLAY(eid,
        .layout = {
            .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(WIDGET_BUTTON_HEIGHT) },
            .padding = { uip(12), uip(12), uip(1), 0 },
            .childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
        },
        .backgroundColor = base,
        .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS),
        .custom = { .customData = grad },
    ) {
        CLAY_TEXT(clay_str(text), &g_widget_text_config_btn);
    }

    return (hovered && g_wstate.mouse_clicked) ? 1 : 0;
}

////////////////////////////////
// Checkbox

B32 ui_checkbox(const char *id, const char *label, B32 *value) {
    ensure_widget_text_configs();
    B32 changed = 0;

    Clay_ElementId eid = WID(id);
    B32 hovered = Clay_PointerOver(eid);

    CLAY(eid,
        .layout = {
            .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(WIDGET_CHECKBOX_HEIGHT) },
            .childGap = uip(8),
            .childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
            .layoutDirection = CLAY_LEFT_TO_RIGHT,
        }
    ) {
        // Box
        Clay_Color box_bg = *value ? g_theme.accent : g_theme.bg_dark;
        if (hovered) {
            box_bg = *value ? g_theme.accent_hover : g_theme.bg_lighter;
        }
        CLAY(WIDI(id, 1),
            .layout = {
                .sizing = { .width = CLAY_SIZING_FIXED(WIDGET_CHECKBOX_SIZE), .height = CLAY_SIZING_FIXED(WIDGET_CHECKBOX_SIZE) },
                .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER },
            },
            .backgroundColor = box_bg,
            .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS),
            .border = { .color = g_theme.border, .width = { 1, 1, 1, 1 } }
        ) {
            if (*value) {
                ui_icon(UI_ICON_CHECK, WIDGET_CHECKBOX_SIZE * 0.75f, g_theme.button_text);
            }
        }

        // Label
        CLAY_TEXT(clay_str(label), &g_widget_text_config);
    }

    if (hovered && g_wstate.mouse_clicked) {
        *value = !(*value);
        changed = 1;
    }

    return changed;
}

////////////////////////////////
// Radio group

B32 ui_radio_group(const char *id, const char **options, S32 count, S32 *selected) {
    ensure_widget_text_configs();
    B32 changed = 0;

    CLAY(WID(id),
        .layout = {
            .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() },
            .childGap = uip(4),
            .layoutDirection = CLAY_TOP_TO_BOTTOM,
        }
    ) {
        for (S32 i = 0; i < count; i++) {
            B32 is_selected = (*selected == i);
            Clay_ElementId row_id = WIDI(id, i + 100);
            B32 row_hovered = Clay_PointerOver(row_id);

            CLAY(row_id,
                .layout = {
                    .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(uis(26)) },
                    .childGap = uip(8),
                    .childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
                    .layoutDirection = CLAY_LEFT_TO_RIGHT,
                }
            ) {
                // Radio circle
                Clay_Color dot_bg = is_selected ? g_theme.accent : g_theme.bg_dark;
                if (row_hovered) {
                    dot_bg = is_selected ? g_theme.accent_hover : g_theme.bg_lighter;
                }
                CLAY(WIDI(id, i + 200),
                    .layout = {
                        .sizing = { .width = CLAY_SIZING_FIXED(WIDGET_RADIO_OUTER), .height = CLAY_SIZING_FIXED(WIDGET_RADIO_OUTER) },
                        .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER },
                    },
                    .backgroundColor = dot_bg,
                    .cornerRadius = CLAY_CORNER_RADIUS(uis(8)),
                    .border = { .color = g_theme.border, .width = { 1, 1, 1, 1 } }
                ) {
                    if (is_selected) {
                        CLAY(WIDI(id, i + 300),
                            .layout = {
                                .sizing = { .width = CLAY_SIZING_FIXED(WIDGET_RADIO_INNER), .height = CLAY_SIZING_FIXED(WIDGET_RADIO_INNER) },
                            },
                            .backgroundColor = g_theme.button_text,
                            .cornerRadius = CLAY_CORNER_RADIUS(uis(4))
                        ) {}
                    }
                }

                // Label
                CLAY_TEXT(clay_str(options[i]), &g_widget_text_config);
            }

            if (row_hovered && g_wstate.mouse_clicked && !is_selected) {
                *selected = i;
                changed = 1;
            }
        }
    }

    return changed;
}

////////////////////////////////
// Text input
//
// Each text input gets its own display buffer(s) so Clay string pointers
// remain valid until the end of the frame. We support up to
// MAX_TEXT_INPUTS simultaneous text inputs per frame.
// Each text input may need up to 3 display buffers (before/sel/after).

#define MAX_TEXT_INPUTS 8
#define DISPLAY_BUF_SIZE 512
// 3 buffers per input: [0]=before selection, [1]=selected text, [2]=after selection
static char g_text_display_bufs[MAX_TEXT_INPUTS * 3][DISPLAY_BUF_SIZE];
static S32  g_text_display_buf_idx = 0;

void ui_text_input_reset_display_bufs() {
    g_text_display_buf_idx = 0;
}

// Helper: get selection range in normalized order (lo <= hi)
static void text_input_get_sel(S32 *lo, S32 *hi) {
    S32 a = g_wstate.sel_start;
    S32 b = g_wstate.sel_end;
    if (a <= b) { *lo = a; *hi = b; }
    else        { *lo = b; *hi = a; }
}

// Helper: true if there's an active selection
static B32 text_input_has_sel() {
    return (g_wstate.sel_start != g_wstate.sel_end);
}

// Helper: delete selected range from buf, update cursor, clear selection
// Returns new string length.
static S32 text_input_delete_sel(char *buf, S32 len) {
    S32 lo, hi;
    text_input_get_sel(&lo, &hi);
    if (lo == hi) return len;
    memmove(&buf[lo], &buf[hi], len - hi + 1);
    g_wstate.cursor_pos = lo;
    g_wstate.sel_start = lo;
    g_wstate.sel_end = lo;
    return len - (hi - lo);
}

// Helper: copy selected text to clipboard
static void text_input_copy_sel(const char *buf, S32 len) {
    S32 lo, hi;
    text_input_get_sel(&lo, &hi);
    if (lo == hi || lo >= len) return;
    if (hi > len) hi = len;
    // Use a temp buffer to null-terminate the selection
    char tmp[DISPLAY_BUF_SIZE];
    S32 sel_len = hi - lo;
    if (sel_len > (S32)sizeof(tmp) - 1) sel_len = (S32)sizeof(tmp) - 1;
    memcpy(tmp, &buf[lo], sel_len);
    tmp[sel_len] = '\0';
    platform_clipboard_set(tmp);
}

B32 ui_text_input(const char *id, char *buf, S32 buf_size) {
    ensure_widget_text_configs();
    B32 text_changed = 0;

    // Register this text input for Tab cycling
    Clay_ElementId eid = WID(id);
    if (g_wstate.text_input_count < UI_WIDGET_MAX_TEXT_INPUTS) {
        g_wstate.text_input_ids[g_wstate.text_input_count++] = eid.id;
    }

    // Grab unique display buffers for this text input (up to 3)
    S32 my_buf_base = g_text_display_buf_idx;
    g_text_display_buf_idx += 3;
    if (my_buf_base + 2 >= MAX_TEXT_INPUTS * 3) my_buf_base = 0; // safety
    char *dbuf_before = g_text_display_bufs[my_buf_base + 0];
    char *dbuf_sel    = g_text_display_bufs[my_buf_base + 1];
    char *dbuf_after  = g_text_display_bufs[my_buf_base + 2];

    B32 hovered = Clay_PointerOver(eid);
    B32 is_focused = (g_wstate.focused_id == eid.id);

    // Tab cycling: if Tab was pressed and we're focused, move to next input.
    // We handle this BEFORE processing other input so the Tab key doesn't
    // get processed as a regular key. The actual focus change is deferred
    // to after all text inputs have registered (see end of function).
    B32 wants_tab = 0;
    if (is_focused && g_wstate.tab_pressed) {
        wants_tab = 1;
    }

    // Click to focus / unfocus
    if (g_wstate.mouse_clicked) {
        if (hovered) {
            if (!is_focused) {
                g_wstate.focused_id = eid.id;
                g_wstate.cursor_pos = (S32)strlen(buf);
                g_wstate.sel_start = g_wstate.cursor_pos;
                g_wstate.sel_end = g_wstate.cursor_pos;
                g_wstate.cursor_blink = 0;
                is_focused = 1;
            } else {
                // Already focused, clicking clears selection
                g_wstate.sel_start = g_wstate.cursor_pos;
                g_wstate.sel_end = g_wstate.cursor_pos;
            }
        } else if (is_focused) {
            g_wstate.focused_id = 0;
            is_focused = 0;
        }
    }

    // Process keyboard input if focused
    if (is_focused) {
        S32 len = (S32)strlen(buf);
        B32 ctrl = g_wstate.input.ctrl_held;

        // Clamp cursor and selection
        if (g_wstate.cursor_pos > len) g_wstate.cursor_pos = len;
        if (g_wstate.cursor_pos < 0) g_wstate.cursor_pos = 0;
        if (g_wstate.sel_start > len) g_wstate.sel_start = len;
        if (g_wstate.sel_end > len) g_wstate.sel_end = len;

        // Key events
        for (S32 k = 0; k < g_wstate.input.key_count; k++) {
            U8 key = g_wstate.input.keys[k];

            // Skip Tab — handled via tab cycling
            if (key == PKEY_TAB) continue;

            // Ctrl shortcuts
            if (ctrl) {
                if (key == PKEY_A) {
                    // Select all
                    g_wstate.sel_start = 0;
                    g_wstate.sel_end = len;
                    g_wstate.cursor_pos = len;
                    continue;
                }
                if (key == PKEY_C) {
                    // Copy
                    text_input_copy_sel(buf, len);
                    continue;
                }
                if (key == PKEY_X) {
                    // Cut
                    if (text_input_has_sel()) {
                        text_input_copy_sel(buf, len);
                        len = text_input_delete_sel(buf, len);
                        text_changed = 1;
                    }
                    continue;
                }
                if (key == PKEY_V) {
                    // Paste
                    const char *clip = platform_clipboard_get();
                    if (clip) {
                        // Delete selection first if any
                        if (text_input_has_sel()) {
                            len = text_input_delete_sel(buf, len);
                        }
                        S32 clip_len = (S32)strlen(clip);
                        // Filter to single line (stop at newline)
                        for (S32 i = 0; i < clip_len; i++) {
                            if (clip[i] == '\n' || clip[i] == '\r') { clip_len = i; break; }
                        }
                        S32 space = buf_size - 1 - len;
                        if (clip_len > space) clip_len = space;
                        if (clip_len > 0) {
                            memmove(&buf[g_wstate.cursor_pos + clip_len], &buf[g_wstate.cursor_pos], len - g_wstate.cursor_pos + 1);
                            memcpy(&buf[g_wstate.cursor_pos], clip, clip_len);
                            g_wstate.cursor_pos += clip_len;
                            len += clip_len;
                            text_changed = 1;
                        }
                        g_wstate.sel_start = g_wstate.cursor_pos;
                        g_wstate.sel_end = g_wstate.cursor_pos;
                    }
                    continue;
                }
                // Other Ctrl combos: ignore
                continue;
            }

            switch (key) {
            case PKEY_BACKSPACE:
                if (text_input_has_sel()) {
                    len = text_input_delete_sel(buf, len);
                    text_changed = 1;
                } else if (g_wstate.cursor_pos > 0) {
                    memmove(&buf[g_wstate.cursor_pos - 1], &buf[g_wstate.cursor_pos], len - g_wstate.cursor_pos + 1);
                    g_wstate.cursor_pos--;
                    len--;
                    text_changed = 1;
                }
                g_wstate.sel_start = g_wstate.cursor_pos;
                g_wstate.sel_end = g_wstate.cursor_pos;
                break;
            case PKEY_DELETE:
                if (text_input_has_sel()) {
                    len = text_input_delete_sel(buf, len);
                    text_changed = 1;
                } else if (g_wstate.cursor_pos < len) {
                    memmove(&buf[g_wstate.cursor_pos], &buf[g_wstate.cursor_pos + 1], len - g_wstate.cursor_pos);
                    len--;
                    text_changed = 1;
                }
                g_wstate.sel_start = g_wstate.cursor_pos;
                g_wstate.sel_end = g_wstate.cursor_pos;
                break;
            case PKEY_LEFT:
                if (text_input_has_sel()) {
                    S32 lo, hi; text_input_get_sel(&lo, &hi);
                    g_wstate.cursor_pos = lo;
                } else if (g_wstate.cursor_pos > 0) {
                    g_wstate.cursor_pos--;
                }
                g_wstate.sel_start = g_wstate.cursor_pos;
                g_wstate.sel_end = g_wstate.cursor_pos;
                g_wstate.cursor_blink = 0;
                break;
            case PKEY_RIGHT:
                if (text_input_has_sel()) {
                    S32 lo, hi; text_input_get_sel(&lo, &hi);
                    g_wstate.cursor_pos = hi;
                } else if (g_wstate.cursor_pos < len) {
                    g_wstate.cursor_pos++;
                }
                g_wstate.sel_start = g_wstate.cursor_pos;
                g_wstate.sel_end = g_wstate.cursor_pos;
                g_wstate.cursor_blink = 0;
                break;
            case PKEY_HOME:
                g_wstate.cursor_pos = 0;
                g_wstate.sel_start = 0;
                g_wstate.sel_end = 0;
                g_wstate.cursor_blink = 0;
                break;
            case PKEY_END:
                g_wstate.cursor_pos = len;
                g_wstate.sel_start = len;
                g_wstate.sel_end = len;
                g_wstate.cursor_blink = 0;
                break;
            case PKEY_RETURN:
            case PKEY_ESCAPE:
                g_wstate.focused_id = 0;
                is_focused = 0;
                break;
            }
        }

        // Character input (printable only, skip control chars)
        if (is_focused) {
            for (S32 c = 0; c < g_wstate.input.char_count; c++) {
                U16 ch = g_wstate.input.chars[c];
                if (ch >= 32 && ch < 127) {
                    // Delete selection first if any
                    if (text_input_has_sel()) {
                        len = text_input_delete_sel(buf, len);
                    }
                    if (len < buf_size - 1) {
                        memmove(&buf[g_wstate.cursor_pos + 1], &buf[g_wstate.cursor_pos], len - g_wstate.cursor_pos + 1);
                        buf[g_wstate.cursor_pos] = (char)ch;
                        g_wstate.cursor_pos++;
                        len++;
                        text_changed = 1;
                        g_wstate.cursor_blink = 0;
                    }
                    g_wstate.sel_start = g_wstate.cursor_pos;
                    g_wstate.sel_end = g_wstate.cursor_pos;
                }
            }
        }
    }

    // Tab cycling: find next text input after this one
    if (wants_tab && is_focused) {
        // We need to find our index. Since text inputs register in order,
        // our ID was just added above. Find it and advance to next.
        S32 my_idx = -1;
        for (S32 i = 0; i < g_wstate.text_input_count; i++) {
            if (g_wstate.text_input_ids[i] == eid.id) { my_idx = i; break; }
        }
        if (my_idx >= 0) {
            // Focus next (wrapping). But we might not have all inputs registered
            // yet this frame. Store the "next index" request; actual focus change
            // happens via a simple approach: just focus the next ID if known,
            // otherwise wrap to first.
            S32 next_idx = my_idx + 1;
            // We can't know total count yet (more inputs may register after us).
            // Instead, unfocus ourselves and set a "pending tab focus" that will
            // be resolved. Simpler approach: Tab from last input wraps to first,
            // Tab from others goes to next. Since we know the IDs registered so
            // far, and inputs are registered in declaration order:
            // If there IS a next already-registered input, go to it.
            // Otherwise, wrap to index 0.
            if (next_idx < g_wstate.text_input_count) {
                g_wstate.focused_id = g_wstate.text_input_ids[next_idx];
            } else {
                // Wrap to first
                g_wstate.focused_id = g_wstate.text_input_ids[0];
            }
            g_wstate.cursor_pos = 0;
            g_wstate.sel_start = 0;
            g_wstate.sel_end = 0;
            is_focused = 0; // no longer focused on THIS input
        }
    }

    // Build display
    S32 len = (S32)strlen(buf);
    S32 sel_lo = 0, sel_hi = 0;
    B32 has_sel = 0;
    if (is_focused) {
        text_input_get_sel(&sel_lo, &sel_hi);
        if (sel_lo < 0) sel_lo = 0;
        if (sel_hi > len) sel_hi = len;
        has_sel = (sel_lo != sel_hi);
    }

    Clay_Color bg = is_focused ? g_theme.bg_dark : g_theme.bg_medium;
    Clay_Color border_color = is_focused ? g_theme.accent : g_theme.border;

    // Inset effect: darker top, lighter bottom (recessed look)
    Clay_Color inset_top = {(F32)Max((S32)bg.r-8,0), (F32)Max((S32)bg.g-8,0), (F32)Max((S32)bg.b-8,0), bg.a};
    Clay_Color inset_bot = {(F32)Min((S32)bg.r+3,255), (F32)Min((S32)bg.g+3,255), (F32)Min((S32)bg.b+3,255), bg.a};
    CustomGradientData *inset_grad = alloc_gradient(inset_top, inset_bot);

    CLAY(eid,
        .layout = {
            .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(WIDGET_INPUT_HEIGHT) },
            .padding = { uip(8), uip(8), 0, 0 },
            .childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
            .layoutDirection = CLAY_LEFT_TO_RIGHT,
        },
        .backgroundColor = bg,
        .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS),
        .custom = { .customData = inset_grad },
        .border = { .color = border_color, .width = { 1, 1, 1, 1 } },
    ) {
        if (len == 0 && !is_focused) {
            // Placeholder
            CLAY_TEXT(CLAY_STRING("..."), &g_widget_text_config_dim);
        } else if (is_focused && has_sel) {
            // Three segments: before | selected | after
            // Before selection
            if (sel_lo > 0) {
                S32 n = sel_lo < (S32)DISPLAY_BUF_SIZE - 1 ? sel_lo : (S32)DISPLAY_BUF_SIZE - 1;
                memcpy(dbuf_before, buf, n);
                dbuf_before[n] = '\0';
                CLAY_TEXT(clay_str(dbuf_before), &g_widget_text_config);
            }
            // Selected text with highlight background
            {
                S32 slen = sel_hi - sel_lo;
                if (slen > (S32)DISPLAY_BUF_SIZE - 1) slen = (S32)DISPLAY_BUF_SIZE - 1;
                memcpy(dbuf_sel, &buf[sel_lo], slen);
                dbuf_sel[slen] = '\0';
                CLAY(WIDI(id, 900),
                    .layout = {
                        .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() },
                    },
                    .backgroundColor = g_theme.accent
                ) {
                    CLAY_TEXT(clay_str(dbuf_sel), &g_widget_text_config_sel);
                }
            }
            // After selection
            if (sel_hi < len) {
                S32 n = len - sel_hi;
                if (n > (S32)DISPLAY_BUF_SIZE - 1) n = (S32)DISPLAY_BUF_SIZE - 1;
                memcpy(dbuf_after, &buf[sel_hi], n);
                dbuf_after[n] = '\0';
                CLAY_TEXT(clay_str(dbuf_after), &g_widget_text_config);
            }
        } else {
            // No selection: show text with cursor '|'
            if (is_focused) {
                S32 cp = g_wstate.cursor_pos;
                if (cp > len) cp = len;
                S32 total = len + 1; // +1 for cursor char
                if (total > (S32)DISPLAY_BUF_SIZE - 1) total = (S32)DISPLAY_BUF_SIZE - 1;
                S32 before = cp < total ? cp : total;
                memcpy(dbuf_before, buf, before);
                dbuf_before[before] = '|';
                S32 after = total - before - 1;
                if (after > 0) memcpy(dbuf_before + before + 1, buf + cp, after);
                dbuf_before[total] = '\0';
                CLAY_TEXT(clay_str(dbuf_before), &g_widget_text_config);
            } else {
                S32 n = len < (S32)DISPLAY_BUF_SIZE - 1 ? len : (S32)DISPLAY_BUF_SIZE - 1;
                memcpy(dbuf_before, buf, n);
                dbuf_before[n] = '\0';
                CLAY_TEXT(clay_str(dbuf_before), &g_widget_text_config);
            }
        }
    }

    return text_changed;
}

////////////////////////////////
// Dropdown

B32 ui_dropdown(const char *id, const char **options, S32 count, S32 *selected) {
    ensure_widget_text_configs();
    B32 changed = 0;

    Clay_ElementId eid = WID(id);
    B32 header_hovered = Clay_PointerOver(eid);
    B32 is_open = (g_wstate.open_dropdown_id == eid.id);

    // Display current selection — truncate with "..." if it overflows the header
    const char *current_label = (*selected >= 0 && *selected < count) ? options[*selected] : "Select...";

    static char dd_trunc_buf[256];
    const char *display_label = current_label;
    Clay_ElementId text_eid = WIDI(id, 500);
    F32 avail_w = Clay_GetElementData(text_eid).boundingBox.width;
    if (avail_w > 0) {
        S32 label_len = (S32)strlen(current_label);
        Vec2F32 text_size = ui_measure_text(current_label, label_len, FONT_SIZE_NORMAL);
        if (text_size.x > avail_w) {
            Vec2F32 dots = ui_measure_text("...", 3, FONT_SIZE_NORMAL);
            F32 target_w = avail_w - dots.x;
            S32 lo = 0, hi = label_len;
            while (lo < hi) {
                S32 mid = (lo + hi + 1) / 2;
                Vec2F32 seg = ui_measure_text(current_label, mid, FONT_SIZE_NORMAL);
                if (seg.x <= target_w) lo = mid; else hi = mid - 1;
            }
            if (lo + 3 < (S32)sizeof(dd_trunc_buf)) {
                memcpy(dd_trunc_buf, current_label, lo);
                dd_trunc_buf[lo] = '.'; dd_trunc_buf[lo+1] = '.'; dd_trunc_buf[lo+2] = '.';
                dd_trunc_buf[lo+3] = '\0';
                display_label = dd_trunc_buf;
            }
        }
    }

    Clay_Color bg = is_open ? g_theme.bg_dark : g_theme.bg_medium;
    if (header_hovered && !is_open) bg = g_theme.bg_lighter;

    Clay_Color dd_top = {(F32)Min((S32)bg.r+6,255), (F32)Min((S32)bg.g+6,255), (F32)Min((S32)bg.b+6,255), bg.a};
    CustomGradientData *dd_grad = alloc_gradient(dd_top, bg);

    CLAY(eid,
        .layout = {
            .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(WIDGET_DROPDOWN_HEIGHT) },
            .padding = { uip(8), uip(8), 0, 0 },
            .childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
            .layoutDirection = CLAY_LEFT_TO_RIGHT,
        },
        .backgroundColor = bg,
        .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS),
        .custom = { .customData = dd_grad },
        .border = { .color = is_open ? g_theme.accent : g_theme.border, .width = { 1, 1, 1, 1 } },
    ) {
        CLAY(text_eid,
            .layout = {
                .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
            }
        ) {
            CLAY_TEXT(clay_str(display_label), &g_widget_text_config);
        }

        CLAY(WIDI(id, 501),
            .layout = {
                .sizing = { .width = CLAY_SIZING_FIXED(uis(20)), .height = CLAY_SIZING_FIXED(uis(20)) },
                .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER },
            }
        ) {
            ui_icon(UI_ICON_CHEVRON_DOWN, uis(12), g_theme.text_dim);
        }
    }

    // Toggle open on click of header
    if (header_hovered && g_wstate.mouse_clicked) {
        if (is_open) {
            g_wstate.open_dropdown_id = 0;
            is_open = 0;
        } else {
            g_wstate.open_dropdown_id = eid.id;
            is_open = 1;
        }
    }

    // Draw dropdown list if open (floating so it escapes modal/container clipping)
    if (is_open) {
        Clay_ElementId list_id = WIDI(id, 502);
        F32 header_width = Clay_GetElementData(eid).boundingBox.width;

        // Dropdown list shadow
        {
            Clay_BoundingBox dd_bb = Clay_GetElementData(list_id).boundingBox;
            emit_shadow(dd_bb, uis(2), uis(2), uis(5),
                        g_theme.shadow.a, 1999,
                        0, eid.id, CLAY_ATTACH_POINT_LEFT_BOTTOM);
        }

        CLAY(list_id,
            .layout = {
                .sizing = { .width = CLAY_SIZING_FIT(.min = header_width), .height = CLAY_SIZING_FIT() },
                .layoutDirection = CLAY_TOP_TO_BOTTOM,
            },
            .backgroundColor = g_theme.bg_dark,
            .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS),
            .floating = {
                .parentId = eid.id,
                .zIndex = 2000,
                .attachPoints = {
                    .element = CLAY_ATTACH_POINT_LEFT_TOP,
                    .parent = CLAY_ATTACH_POINT_LEFT_BOTTOM,
                },
                .attachTo = CLAY_ATTACH_TO_ELEMENT_WITH_ID,
            },
            .border = { .color = g_theme.border, .width = { 1, 1, 1, 1 } },
        ) {
            for (S32 i = 0; i < count; i++) {
                B32 is_item_selected = (*selected == i);
                Clay_ElementId item_id = WIDI(id, i + 600);
                B32 item_hovered = Clay_PointerOver(item_id);

                Clay_Color item_bg = is_item_selected ? g_theme.accent : g_theme.bg_dark;
                if (item_hovered) item_bg = g_theme.bg_lighter;

                Clay_TextElementConfig *item_text = (is_item_selected && !item_hovered)
                    ? &g_widget_text_config_btn : &g_widget_text_config;

                Clay_CornerRadius item_radius = {};
                if (i == 0) { item_radius.topLeft = CORNER_RADIUS; item_radius.topRight = CORNER_RADIUS; }
                if (i == count - 1) { item_radius.bottomLeft = CORNER_RADIUS; item_radius.bottomRight = CORNER_RADIUS; }

                CLAY(item_id,
                    .layout = {
                        .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(WIDGET_DROPDOWN_ITEM_H) },
                        .padding = { uip(8), uip(8), 0, 0 },
                        .childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
                    },
                    .backgroundColor = item_bg,
                    .cornerRadius = item_radius
                ) {
                    CLAY_TEXT(clay_str(options[i]), item_text);
                }

                if (item_hovered && g_wstate.mouse_clicked) {
                    *selected = i;
                    changed = 1;
                    g_wstate.open_dropdown_id = 0;
                }
            }
        }

        // Close if clicked outside both header and list
        if (g_wstate.mouse_clicked && !header_hovered && !Clay_PointerOver(list_id)) {
            // Check if any item was clicked (already handled above)
            B32 clicked_item = 0;
            for (S32 i = 0; i < count; i++) {
                if (Clay_PointerOver(WIDI(id, i + 600))) { clicked_item = 1; break; }
            }
            if (!clicked_item) {
                g_wstate.open_dropdown_id = 0;
            }
        }
    }

    return changed;
}

////////////////////////////////
// Modal dialog

B32 ui_modal_is_active() {
    return g_wstate.modal.active;
}

S32 ui_modal(const char *id, const char *title, const char *message,
             const char **buttons, S32 button_count) {
    ensure_widget_text_configs();

    Clay_ElementId eid = WID(id);

    // First call activates the modal
    if (!g_wstate.modal.active) {
        g_wstate.modal.active = 1;
        g_wstate.modal.id = eid.id;
        g_wstate.modal.result = -1;
    }

    // If a different modal is active, ignore this one
    if (g_wstate.modal.id != eid.id) return -1;

    S32 result = -1;

    // Check Escape key to dismiss
    for (S32 k = 0; k < g_wstate.input.key_count; k++) {
        if (g_wstate.input.keys[k] == PKEY_ESCAPE) {
            result = -2;
            break;
        }
    }

    // Full-screen overlay (dims background, captures all pointer events)
    CLAY(WIDI(id, 2000),
        .layout = {
            .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
        },
        .backgroundColor = Clay_Color{0, 0, 0, 120},
        .floating = {
            .zIndex = 1000,
            .attachPoints = {
                .element = CLAY_ATTACH_POINT_LEFT_TOP,
                .parent = CLAY_ATTACH_POINT_LEFT_TOP,
            },
            .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_CAPTURE,
            .attachTo = CLAY_ATTACH_TO_ROOT,
        }
    ) {}

    // Modal drop shadow
    {
        Clay_ElementId modal_box_id = WIDI(id, 2001);
        Clay_BoundingBox modal_bb = Clay_GetElementData(modal_box_id).boundingBox;
        emit_shadow(modal_bb, uis(3), uis(4), uis(10),
                    g_theme.shadow.a, 1000,
                    1, 0, CLAY_ATTACH_POINT_LEFT_TOP);
    }

    // Dialog box (centered)
    CLAY(WIDI(id, 2001),
        .layout = {
            .sizing = { .width = CLAY_SIZING_FIXED(MODAL_WIDTH), .height = CLAY_SIZING_FIT() },
            .layoutDirection = CLAY_TOP_TO_BOTTOM,
        },
        .backgroundColor = g_theme.bg_medium,
        .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS),
        .floating = {
            .zIndex = 1001,
            .attachPoints = {
                .element = CLAY_ATTACH_POINT_CENTER_CENTER,
                .parent = CLAY_ATTACH_POINT_CENTER_CENTER,
            },
            .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_CAPTURE,
            .attachTo = CLAY_ATTACH_TO_ROOT,
        },
        .border = { .color = g_theme.border, .width = { 1, 1, 1, 1 } }
    ) {
        // Title bar (gradient: lighter top)
        {
            Clay_Color mtb = g_theme.title_bar;
            Clay_Color mtb_top = {(F32)Min((S32)mtb.r+12,255), (F32)Min((S32)mtb.g+12,255), (F32)Min((S32)mtb.b+12,255), mtb.a};
            CustomGradientData *mtb_grad = alloc_gradient(mtb_top, mtb);

            CLAY(WIDI(id, 2002),
                .layout = {
                    .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(WINDOW_TITLE_HEIGHT) },
                    .padding = { uip(12), uip(12), 0, 0 },
                    .childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
                },
                .backgroundColor = g_theme.title_bar,
                .cornerRadius = { CORNER_RADIUS, CORNER_RADIUS, 0, 0 },
                .custom = { .customData = mtb_grad },
                .border = { .color = g_theme.border, .width = { .bottom = 1 } },
            ) {
                CLAY_TEXT(clay_str(title), &g_widget_text_config);
            }
        }

        // Message body
        static Clay_TextElementConfig msg_text_config;
        msg_text_config = g_widget_text_config;
        msg_text_config.wrapMode = CLAY_TEXT_WRAP_WORDS;

        CLAY(WIDI(id, 2003),
            .layout = {
                .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
                .padding = { uip(16), uip(16), uip(16), uip(16) },
            }
        ) {
            CLAY_TEXT(clay_str(message), &msg_text_config);
        }

        // Button row (right-aligned)
        CLAY(WIDI(id, 2004),
            .layout = {
                .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
                .padding = { uip(16), uip(16), uip(8), uip(16) },
                .childGap = uip(8),
                .childAlignment = { .x = CLAY_ALIGN_X_RIGHT, .y = CLAY_ALIGN_Y_CENTER },
                .layoutDirection = CLAY_LEFT_TO_RIGHT,
            }
        ) {
            for (S32 i = 0; i < button_count; i++) {
                Clay_ElementId btn_id = WIDI(id, 2100 + i);
                B32 btn_hovered = Clay_PointerOver(btn_id);

                Clay_Color mbtn_base = btn_hovered ? g_theme.accent_hover : g_theme.accent;
                Clay_Color mbtn_top = {(F32)Min((S32)mbtn_base.r+12,255), (F32)Min((S32)mbtn_base.g+12,255), (F32)Min((S32)mbtn_base.b+12,255), mbtn_base.a};
                Clay_Color mbtn_bot = {(F32)Max((S32)mbtn_base.r-15,0), (F32)Max((S32)mbtn_base.g-15,0), (F32)Max((S32)mbtn_base.b-15,0), mbtn_base.a};
                CustomGradientData *mbtn_grad = alloc_gradient(mbtn_top, mbtn_bot);

                CLAY(btn_id,
                    .layout = {
                        .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(WIDGET_BUTTON_HEIGHT) },
                        .padding = { uip(16), uip(16), uip(1), 0 },
                        .childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
                    },
                    .backgroundColor = mbtn_base,
                    .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS),
                    .custom = { .customData = mbtn_grad },
                ) {
                    CLAY_TEXT(clay_str(buttons[i]), &g_widget_text_config_btn);
                }

                if (btn_hovered && g_wstate.mouse_clicked) {
                    result = i;
                }
            }
        }
    }

    // Deactivate modal if a result was produced
    if (result != -1) {
        g_wstate.modal.active = 0;
        g_wstate.modal.id = 0;
        g_wstate.modal.result = result;
    }

    return result;
}

////////////////////////////////
// Draggable window

static UI_WindowSlot *find_or_create_window_slot(U32 id, Vec2F32 initial_pos, Vec2F32 initial_size) {
    // Look for existing slot
    for (S32 i = 0; i < g_wstate.window_count; i++) {
        if (g_wstate.windows[i].id == id) {
            return &g_wstate.windows[i];
        }
    }
    // Create new slot
    if (g_wstate.window_count >= UI_WIDGET_MAX_WINDOWS) return nullptr;
    UI_WindowSlot *slot = &g_wstate.windows[g_wstate.window_count++];
    slot->id = id;
    slot->position = initial_pos;
    slot->size = initial_size;
    slot->open = 1;
    slot->z_order = g_wstate.next_z++;
    return slot;
}

static void bring_window_to_front(UI_WindowSlot *slot) {
    // Renormalize if approaching modal z-range
    if (g_wstate.next_z > 800) {
        S16 sorted[UI_WIDGET_MAX_WINDOWS];
        S32 count = g_wstate.window_count;
        for (S32 i = 0; i < count; i++) sorted[i] = g_wstate.windows[i].z_order;
        // Bubble sort (tiny array)
        for (S32 i = 0; i < count - 1; i++) {
            for (S32 j = i + 1; j < count; j++) {
                if (sorted[j] < sorted[i]) {
                    S16 tmp = sorted[i]; sorted[i] = sorted[j]; sorted[j] = tmp;
                }
            }
        }
        for (S32 i = 0; i < count; i++) {
            for (S32 j = 0; j < count; j++) {
                if (g_wstate.windows[j].z_order == sorted[i]) {
                    g_wstate.windows[j].z_order = (S16)i;
                }
            }
        }
        g_wstate.next_z = (S16)count;
    }
    slot->z_order = g_wstate.next_z++;
}

B32 ui_window(const char *id, const char *title, B32 *open,
              Vec2F32 initial_pos, Vec2F32 initial_size,
              UI_WindowContentFn content_fn, void *user_data) {
    ensure_widget_text_configs();

    if (!*open) return 0;

    Clay_ElementId eid = WID(id);
    UI_WindowSlot *slot = find_or_create_window_slot(eid.id, initial_pos, initial_size);
    if (!slot) return 0;

    // Drag handling
    Clay_ElementId title_bar_id = WIDI(id, 3000);
    B32 title_hovered = Clay_PointerOver(title_bar_id);

    // Start drag on title bar click
    if (title_hovered && g_wstate.mouse_clicked && g_wstate.drag.dragging_id == 0) {
        g_wstate.drag.dragging_id = eid.id;
        g_wstate.drag.drag_anchor = g_wstate.input.mouse_pos;
        g_wstate.drag.pos_anchor = slot->position;
        bring_window_to_front(slot);
    }

    // Continue drag
    if (g_wstate.drag.dragging_id == eid.id && g_wstate.input.mouse_down) {
        Vec2F32 delta;
        delta.x = g_wstate.input.mouse_pos.x - g_wstate.drag.drag_anchor.x;
        delta.y = g_wstate.input.mouse_pos.y - g_wstate.drag.drag_anchor.y;
        slot->position.x = g_wstate.drag.pos_anchor.x + delta.x;
        slot->position.y = g_wstate.drag.pos_anchor.y + delta.y;
    }

    // End drag on release
    if (g_wstate.drag.dragging_id == eid.id && !g_wstate.input.mouse_down) {
        g_wstate.drag.dragging_id = 0;
    }

    // Click anywhere on window body brings to front
    B32 body_hovered = Clay_PointerOver(eid);
    if (body_hovered && g_wstate.mouse_clicked && g_wstate.drag.dragging_id == 0) {
        bring_window_to_front(slot);
    }

    // Close button
    Clay_ElementId close_id = WIDI(id, 3001);
    B32 close_hovered = Clay_PointerOver(close_id);

    // Drop shadow
    {
        Clay_BoundingBox win_bb = Clay_GetElementData(eid).boundingBox;
        // Use absolute position since window uses offset-based floating
        Clay_BoundingBox shadow_bb = { slot->position.x, slot->position.y, win_bb.width, win_bb.height };
        emit_shadow(shadow_bb, uis(3), uis(3), uis(8),
                    g_theme.shadow.a, (S16)(100 + slot->z_order - 1),
                    1, 0, CLAY_ATTACH_POINT_LEFT_TOP);
    }

    // Window floating element
    CLAY(eid,
        .layout = {
            .sizing = { .width = CLAY_SIZING_FIXED(slot->size.x * g_ui_scale), .height = CLAY_SIZING_FIT() },
            .layoutDirection = CLAY_TOP_TO_BOTTOM,
        },
        .backgroundColor = g_theme.bg_medium,
        .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS),
        .floating = {
            .offset = { slot->position.x, slot->position.y },
            .zIndex = (S16)(100 + slot->z_order),
            .attachPoints = {
                .element = CLAY_ATTACH_POINT_LEFT_TOP,
                .parent = CLAY_ATTACH_POINT_LEFT_TOP,
            },
            .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_CAPTURE,
            .attachTo = CLAY_ATTACH_TO_ROOT,
        },
        .border = { .color = g_theme.border, .width = { 1, 1, 1, 1 } }
    ) {
        // Title bar (gradient: lighter top)
        Clay_Color tb = g_theme.title_bar;
        Clay_Color tb_top = {(F32)Min((S32)tb.r+12,255), (F32)Min((S32)tb.g+12,255), (F32)Min((S32)tb.b+12,255), tb.a};
        CustomGradientData *tb_grad = alloc_gradient(tb_top, tb);

        CLAY(title_bar_id,
            .layout = {
                .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(WINDOW_TITLE_HEIGHT) },
                .padding = { uip(10), uip(10), 0, 0 },
                .childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
                .layoutDirection = CLAY_LEFT_TO_RIGHT,
            },
            .backgroundColor = g_theme.title_bar,
            .cornerRadius = { CORNER_RADIUS, CORNER_RADIUS, 0, 0 },
            .custom = { .customData = tb_grad },
            .border = { .color = g_theme.border, .width = { .bottom = 1 } },
        ) {
            // Title text (grows to push close button right)
            CLAY(WIDI(id, 3002),
                .layout = {
                    .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
                }
            ) {
                CLAY_TEXT(clay_str(title), &g_widget_text_config);
            }

            // Close button
            Clay_Color close_bg = g_theme_id == 1
                ? (close_hovered ? Clay_Color{220, 50, 50, 255} : Clay_Color{200, 70, 70, 255})
                : (close_hovered ? Clay_Color{200, 60, 60, 255} : Clay_Color{120, 50, 50, 255});
            CLAY(close_id,
                .layout = {
                    .sizing = { .width = CLAY_SIZING_FIXED(uis(20)), .height = CLAY_SIZING_FIXED(uis(20)) },
                    .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER },
                },
                .backgroundColor = close_bg,
                .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS)
            ) {
                ui_icon(UI_ICON_CLOSE, uis(12), g_theme.button_text);
            }
        }

        // Content area
        CLAY(WIDI(id, 3003),
            .layout = {
                .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
                .padding = { uip(12), uip(12), uip(10), uip(10) },
                .childGap = uip(8),
                .layoutDirection = CLAY_TOP_TO_BOTTOM,
            }
        ) {
            if (content_fn) {
                content_fn(user_data);
            }
        }
    }

    // Handle close button click
    if (close_hovered && g_wstate.mouse_clicked) {
        *open = 0;
        return 0;
    }

    return 1;
}

////////////////////////////////
// Tab bar

static CustomGradientData g_tab_gradient;

S32 ui_tab_bar(const char *id, const char **labels, S32 count, S32 *selected) {
    g_tab_gradient.type = CUSTOM_RENDER_VGRADIENT;
    g_tab_gradient.top_color = TAB_ACTIVE_TOP;
    g_tab_gradient.bottom_color = TAB_ACTIVE_BOTTOM;

    Clay_String id_str = clay_str(id);
    Clay_ElementId row_eid = Clay__HashString(id_str, 0);

    CLAY(row_eid,
        .layout = {
            .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
            .padding = { 0, 0, 0, 0 },
            .layoutDirection = CLAY_LEFT_TO_RIGHT,
        },
        .backgroundColor = g_theme.bg_medium,
        .border = { .color = g_theme.border, .width = { .bottom = 1 } },
    ) {
        for (S32 i = 0; i < count; i++) {
            Clay_ElementId tab_eid = Clay__HashStringWithOffset(id_str, (U32)i, 0);
            B32 is_active = (i == *selected);
            B32 hovered = Clay_PointerOver(tab_eid);

            Clay_String lbl_str = clay_str(labels[i]);

            if (is_active) {
                CLAY(tab_eid,
                    .layout = {
                        .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(TAB_HEIGHT) },
                        .padding = { TAB_PADDING_H, TAB_PADDING_H, 0, uip(6) },
                        .childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
                    },
                    .cornerRadius = { .topLeft = TAB_CORNER_RADIUS, .topRight = TAB_CORNER_RADIUS, .bottomLeft = 0, .bottomRight = 0 },
                    .custom = { .customData = &g_tab_gradient },
                ) {
                    CLAY_TEXT(lbl_str, &g_widget_text_config_tab);
                }
            } else {
                Clay_Color bg = hovered ? (Clay_Color)TAB_INACTIVE_HOVER : (Clay_Color)TAB_INACTIVE_BG;
                CLAY(tab_eid,
                    .layout = {
                        .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(TAB_HEIGHT) },
                        .padding = { TAB_PADDING_H, TAB_PADDING_H, 0, uip(6) },
                        .childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
                    },
                    .backgroundColor = bg,
                    .cornerRadius = { .topLeft = TAB_CORNER_RADIUS, .topRight = TAB_CORNER_RADIUS, .bottomLeft = 0, .bottomRight = 0 },
                ) {
                    CLAY_TEXT(lbl_str, &g_widget_text_config_tab_inactive);
                }
            }

            if (hovered && g_wstate.mouse_clicked) {
                *selected = i;
            }
        }
    }
    return *selected;
}

////////////////////////////////
// Shared value-edit helpers (used by knob, sliders, fader)

// Process keyboard input for value text editing.
// Returns 0 = still editing, 1 = committed, 2 = cancelled.
static S32 value_edit_process_keys(F32 *value, F32 max_val, B32 is_signed, B32 *changed) {
    char *ebuf = g_wstate.knob_edit_buf;
    S32 elen = (S32)strlen(ebuf);
    S32 *ecur = &g_wstate.knob_edit_cursor;
    S32 *esel0 = &g_wstate.knob_edit_sel_start;
    S32 *esel1 = &g_wstate.knob_edit_sel_end;
    B32 commit = 0;
    B32 cancel = 0;
    B32 ctrl = g_wstate.input.ctrl_held;

    if (*ecur > elen) *ecur = elen;
    if (*esel0 > elen) *esel0 = elen;
    if (*esel1 > elen) *esel1 = elen;

    #define KE_HAS_SEL() (*esel0 != *esel1)
    #define KE_SEL_LO()  (*esel0 < *esel1 ? *esel0 : *esel1)
    #define KE_SEL_HI()  (*esel0 < *esel1 ? *esel1 : *esel0)
    #define KE_CLEAR_SEL() do { *esel0 = *ecur; *esel1 = *ecur; } while(0)

    auto ke_delete_sel = [&]() -> S32 {
        S32 lo = KE_SEL_LO(), hi = KE_SEL_HI();
        if (lo == hi) return elen;
        memmove(&ebuf[lo], &ebuf[hi], elen - hi + 1);
        *ecur = lo;
        KE_CLEAR_SEL();
        return elen - (hi - lo);
    };

    for (S32 k = 0; k < g_wstate.input.key_count; k++) {
        U8 key = g_wstate.input.keys[k];

        if (ctrl) {
            if (key == PKEY_A) {
                *esel0 = 0; *esel1 = elen; *ecur = elen; continue;
            }
            if (key == PKEY_C) {
                if (KE_HAS_SEL()) {
                    S32 lo = KE_SEL_LO(), hi = KE_SEL_HI();
                    char tmp[32]; S32 n = hi - lo; if (n > 31) n = 31;
                    memcpy(tmp, &ebuf[lo], n); tmp[n] = '\0';
                    platform_clipboard_set(tmp);
                }
                continue;
            }
            if (key == PKEY_X) {
                if (KE_HAS_SEL()) {
                    S32 lo = KE_SEL_LO(), hi = KE_SEL_HI();
                    char tmp[32]; S32 n = hi - lo; if (n > 31) n = 31;
                    memcpy(tmp, &ebuf[lo], n); tmp[n] = '\0';
                    platform_clipboard_set(tmp);
                    elen = ke_delete_sel();
                }
                continue;
            }
            if (key == PKEY_V) {
                const char *clip = platform_clipboard_get();
                if (clip) {
                    if (KE_HAS_SEL()) elen = ke_delete_sel();
                    S32 clip_len = (S32)strlen(clip);
                    char filtered[32]; S32 flen = 0;
                    for (S32 i = 0; i < clip_len && flen < 30; i++) {
                        char c = clip[i];
                        if ((c >= '0' && c <= '9') || c == '.' || c == '-')
                            filtered[flen++] = c;
                    }
                    S32 space = 30 - elen;
                    if (flen > space) flen = space;
                    if (flen > 0) {
                        memmove(&ebuf[*ecur + flen], &ebuf[*ecur], elen - *ecur + 1);
                        memcpy(&ebuf[*ecur], filtered, flen);
                        *ecur += flen; elen += flen;
                    }
                    KE_CLEAR_SEL();
                }
                continue;
            }
            continue;
        }

        if (key == PKEY_RETURN) { commit = 1; }
        else if (key == PKEY_ESCAPE) { cancel = 1; }
        else if (key == PKEY_BACKSPACE) {
            if (KE_HAS_SEL()) { elen = ke_delete_sel(); }
            else if (*ecur > 0) {
                memmove(&ebuf[*ecur - 1], &ebuf[*ecur], elen - *ecur + 1);
                (*ecur)--; elen--;
            }
            KE_CLEAR_SEL();
        } else if (key == PKEY_DELETE) {
            if (KE_HAS_SEL()) { elen = ke_delete_sel(); }
            else if (*ecur < elen) {
                memmove(&ebuf[*ecur], &ebuf[*ecur + 1], elen - *ecur);
                elen--;
            }
            KE_CLEAR_SEL();
        } else if (key == PKEY_LEFT) {
            if (KE_HAS_SEL()) { *ecur = KE_SEL_LO(); }
            else if (*ecur > 0) { (*ecur)--; }
            KE_CLEAR_SEL();
        } else if (key == PKEY_RIGHT) {
            if (KE_HAS_SEL()) { *ecur = KE_SEL_HI(); }
            else if (*ecur < elen) { (*ecur)++; }
            KE_CLEAR_SEL();
        }
    }

    if (!commit && !cancel) {
        for (S32 c = 0; c < g_wstate.input.char_count; c++) {
            U16 ch = g_wstate.input.chars[c];
            B32 valid = (ch >= '0' && ch <= '9') || ch == '.' || ch == '-';
            if (valid) {
                if (KE_HAS_SEL()) elen = ke_delete_sel();
                if (elen < 30) {
                    memmove(&ebuf[*ecur + 1], &ebuf[*ecur], elen - *ecur + 1);
                    ebuf[*ecur] = (char)ch; (*ecur)++; elen++;
                }
                KE_CLEAR_SEL();
            }
        }
    }

    #undef KE_HAS_SEL
    #undef KE_SEL_LO
    #undef KE_SEL_HI
    #undef KE_CLEAR_SEL

    if (commit) {
        char *end = nullptr;
        F32 parsed = strtof(ebuf, &end);
        if (end != ebuf) {
            F32 lo = is_signed ? -max_val : 0.0f;
            if (parsed < lo) parsed = lo;
            if (parsed > max_val) parsed = max_val;
            if (parsed != *value) { *value = parsed; *changed = 1; }
        }
        g_wstate.knob_edit_id = 0;
        return 1;
    } else if (cancel) {
        g_wstate.knob_edit_id = 0;
        return 2;
    }
    return 0;
}

// Render the text edit box with selection/cursor display.
static void value_edit_render(U32 hash, F32 width) {
    Clay_ElementId edit_eid = CLAY_IDI("ValEdit", (S32)hash);

    char *ebuf = g_wstate.knob_edit_buf;
    S32 elen = (S32)strlen(ebuf);
    S32 ecur = g_wstate.knob_edit_cursor;
    S32 esel0 = g_wstate.knob_edit_sel_start;
    S32 esel1 = g_wstate.knob_edit_sel_end;
    if (ecur > elen) ecur = elen;
    if (esel0 > elen) esel0 = elen;
    if (esel1 > elen) esel1 = elen;
    S32 sel_lo = esel0 < esel1 ? esel0 : esel1;
    S32 sel_hi = esel0 < esel1 ? esel1 : esel0;
    B32 has_sel = (sel_lo != sel_hi);

    static char ke_dbuf_before[32];
    static char ke_dbuf_sel[32];
    static char ke_dbuf_after[32];

    static Clay_TextElementConfig edit_cfg = {};
    edit_cfg.textColor = g_theme.text;
    edit_cfg.fontSize = FONT_SIZE_SMALL;
    edit_cfg.wrapMode = CLAY_TEXT_WRAP_NONE;

    static Clay_TextElementConfig edit_sel_cfg = {};
    edit_sel_cfg.textColor = Clay_Color{255, 255, 255, 255};
    edit_sel_cfg.fontSize = FONT_SIZE_SMALL;
    edit_sel_cfg.wrapMode = CLAY_TEXT_WRAP_NONE;

    CLAY(edit_eid,
        .layout = {
            .sizing = { .width = CLAY_SIZING_FIXED(width), .height = CLAY_SIZING_FIT() },
            .padding = { uip(2), uip(2), uip(1), uip(1) },
            .childAlignment = { .x = CLAY_ALIGN_X_CENTER },
            .layoutDirection = CLAY_LEFT_TO_RIGHT,
        },
        .backgroundColor = g_theme.bg_dark,
        .cornerRadius = CLAY_CORNER_RADIUS(uis(2)),
        .border = { .color = g_theme.accent, .width = { 1, 1, 1, 1 } }
    ) {
        if (has_sel) {
            if (sel_lo > 0) {
                S32 n = sel_lo;
                memcpy(ke_dbuf_before, ebuf, n); ke_dbuf_before[n] = '\0';
                Clay_String s_before = { .length = n, .chars = ke_dbuf_before };
                CLAY_TEXT(s_before, &edit_cfg);
            }
            {
                S32 n = sel_hi - sel_lo;
                memcpy(ke_dbuf_sel, &ebuf[sel_lo], n); ke_dbuf_sel[n] = '\0';
                Clay_String s_sel = { .length = n, .chars = ke_dbuf_sel };
                CLAY(CLAY_IDI("ValEditSel", (S32)hash),
                    .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() } },
                    .backgroundColor = g_theme.accent
                ) {
                    CLAY_TEXT(s_sel, &edit_sel_cfg);
                }
            }
            if (sel_hi < elen) {
                S32 n = elen - sel_hi;
                memcpy(ke_dbuf_after, &ebuf[sel_hi], n); ke_dbuf_after[n] = '\0';
                Clay_String s_after = { .length = n, .chars = ke_dbuf_after };
                CLAY_TEXT(s_after, &edit_cfg);
            }
        } else {
            static char edit_display[64];
            S32 di = 0;
            for (S32 i = 0; i < elen + 1 && di < 62; i++) {
                if (i == ecur) edit_display[di++] = '|';
                if (i < elen) edit_display[di++] = ebuf[i];
            }
            edit_display[di] = '\0';
            Clay_String s_cursor = { .length = di, .chars = edit_display };
            CLAY_TEXT(s_cursor, &edit_cfg);
        }
    }
}

// Get the Clay element ID of the current edit box (for click-away detection).
static Clay_ElementId value_edit_eid(U32 hash) {
    return CLAY_IDI("ValEdit", (S32)hash);
}

// Handle click-outside to commit the edit.
static void value_edit_click_away(Clay_ElementId eid, F32 *value, F32 max_val, B32 is_signed, B32 *changed) {
    if (g_wstate.mouse_clicked && !Clay_PointerOver(eid)) {
        char *end = nullptr;
        F32 parsed = strtof(g_wstate.knob_edit_buf, &end);
        if (end != g_wstate.knob_edit_buf) {
            F32 lo = is_signed ? -max_val : 0.0f;
            if (parsed < lo) parsed = lo;
            if (parsed > max_val) parsed = max_val;
            if (parsed != *value) { *value = parsed; *changed = 1; }
        }
        g_wstate.knob_edit_id = 0;
    }
}

// Enter text edit mode for a widget.
static void value_edit_enter(U32 hash, F32 value, B32 is_signed) {
    g_wstate.knob_edit_id = hash;
    g_wstate.focused_id = 0;
    if (is_signed) {
        snprintf(g_wstate.knob_edit_buf, sizeof(g_wstate.knob_edit_buf), "%+.1f", value);
    } else {
        snprintf(g_wstate.knob_edit_buf, sizeof(g_wstate.knob_edit_buf), "%.1f", value);
    }
    S32 slen = (S32)strlen(g_wstate.knob_edit_buf);
    g_wstate.knob_edit_cursor = slen;
    g_wstate.knob_edit_sel_start = 0;
    g_wstate.knob_edit_sel_end = slen;
}

// Normalize a value to [0,1] range.
static F32 value_normalize(F32 value, F32 max_val, B32 is_signed) {
    F32 n;
    if (is_signed) { n = (value + max_val) / (2.0f * max_val); }
    else { n = value / max_val; }
    if (n < 0.0f) n = 0.0f;
    if (n > 1.0f) n = 1.0f;
    return n;
}

// Format a value into a text buf from the pool. Returns null if pool exhausted.
static char *value_format_text(F32 value, B32 is_signed, S32 *out_len) {
    if (g_knob_text_buf_count >= UI_MAX_KNOB_TEXT_BUFS) return nullptr;
    char *buf = g_knob_text_bufs[g_knob_text_buf_count++];
    if (is_signed) { *out_len = snprintf(buf, 32, "%+.1f", value); }
    else { *out_len = snprintf(buf, 32, "%.1f", value); }
    return buf;
}

////////////////////////////////
// Knob / Potentiometer

B32 ui_knob(const char *id, const char *label, F32 *value, F32 max_val, B32 is_signed, F32 default_val, B32 editable) {
    ensure_widget_text_configs();

    F32 knob_size = WIDGET_KNOB_SIZE;
    B32 changed = 0;

    F32 normalized = value_normalize(*value, max_val, is_signed);

    // Angle: 270-degree sweep, -135 to +135 degrees
    F32 deg_to_rad = 3.14159265f / 180.0f;
    F32 angle_rad = (-135.0f + normalized * 270.0f) * deg_to_rad;

    U32 knob_hash = Clay__HashString(clay_str(id), 0).id;
    B32 is_editing = (editable && g_wstate.knob_edit_id == knob_hash);
    UI_KnobDragState *kd = &g_wstate.knob_drag;

    // Drag interaction (vertical mouse delta)
    if (!is_editing && kd->dragging_id == knob_hash && g_wstate.input.mouse_down) {
        B32 shift_now = g_wstate.input.shift_held;
        if (shift_now != kd->was_shift) {
            kd->drag_start_y = g_wstate.input.mouse_pos.y;
            kd->value_at_start = *value;
            kd->was_shift = shift_now;
        }
        F32 dy = kd->drag_start_y - g_wstate.input.mouse_pos.y;
        F32 sensitivity = 200.0f * g_ui_scale;
        if (shift_now) sensitivity *= 5.0f;
        F32 range = is_signed ? (2.0f * max_val) : max_val;
        F32 new_val = kd->value_at_start + (dy / sensitivity) * range;
        F32 lo = is_signed ? -max_val : 0.0f;
        if (new_val < lo) new_val = lo;
        if (new_val > max_val) new_val = max_val;
        if (new_val != *value) { *value = new_val; changed = 1; }
        normalized = value_normalize(*value, max_val, is_signed);
        angle_rad = (-135.0f + normalized * 270.0f) * deg_to_rad;
    }

    // Text edit keyboard input
    if (is_editing) {
        S32 result = value_edit_process_keys(value, max_val, is_signed, &changed);
        if (result) {
            is_editing = 0;
            normalized = value_normalize(*value, max_val, is_signed);
            angle_rad = (-135.0f + normalized * 270.0f) * deg_to_rad;
        }
    }

    // Format value text
    S32 val_len = 0;
    char *val_text = is_editing ? nullptr : value_format_text(*value, is_signed, &val_len);

    // Layout: vertical column (knob → value text → label)
    CLAY(WID(id),
        .layout = {
            .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() },
            .childGap = WIDGET_KNOB_LABEL_GAP,
            .childAlignment = { .x = CLAY_ALIGN_X_CENTER },
            .layoutDirection = CLAY_TOP_TO_BOTTOM,
        }
    ) {
        Clay_ElementId knob_eid = CLAY_IDI("KnobBg", (S32)knob_hash);
        B32 hovered = Clay_PointerOver(knob_eid);

        CLAY(knob_eid,
            .layout = {
                .sizing = { .width = CLAY_SIZING_FIXED(knob_size), .height = CLAY_SIZING_FIXED(knob_size) },
                .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER },
            },
            .backgroundColor = g_theme.bg_dark,
            .cornerRadius = CLAY_CORNER_RADIUS(knob_size / 2.0f)
        ) {
            if (g_rotated_icon_pool_count < UI_MAX_ROTATED_ICONS_PER_FRAME) {
                S32 ri_idx = g_rotated_icon_pool_count;
                CustomRotatedIconData *rdata = &g_rotated_icon_pool[g_rotated_icon_pool_count++];
                rdata->type = CUSTOM_RENDER_ROTATED_ICON;
                rdata->icon_id = (S32)UI_ICON_KNOB;
                rdata->color = g_theme.accent;
                rdata->angle_rad = angle_rad;

                F32 icon_size = knob_size * 0.75f;
                CLAY(CLAY_IDI("KnobIcon", ri_idx),
                    .layout = {
                        .sizing = { .width = CLAY_SIZING_FIXED(icon_size), .height = CLAY_SIZING_FIXED(icon_size) },
                    },
                    .custom = { .customData = rdata }
                ) {}
            }
        }

        // Click: F64-click resets, single click starts drag
        if (!is_editing && hovered && g_wstate.mouse_clicked && kd->dragging_id == 0) {
            B32 is_double_click = (kd->last_click_id == knob_hash &&
                                   (g_frame_number - kd->last_click_frame) < 20);
            kd->last_click_id = knob_hash;
            kd->last_click_frame = g_frame_number;

            if (is_double_click) {
                if (*value != default_val) { *value = default_val; changed = 1; }
                normalized = value_normalize(*value, max_val, is_signed);
                angle_rad = (-135.0f + normalized * 270.0f) * deg_to_rad;
                kd->last_click_id = 0;
            } else {
                kd->dragging_id = knob_hash;
                kd->drag_start_y = g_wstate.input.mouse_pos.y;
                kd->value_at_start = *value;
            }
        }

        // Value text / text edit area
        if (is_editing) {
            value_edit_render(knob_hash, knob_size);
            value_edit_click_away(value_edit_eid(knob_hash), value, max_val, is_signed, &changed);
        } else if (val_text && val_len > 0) {
            Clay_ElementId val_eid = CLAY_IDI("KnobVal", (S32)knob_hash);
            B32 val_hovered = Clay_PointerOver(val_eid);

            Clay_String val_str = { .isStaticallyAllocated = false, .length = val_len, .chars = val_text };
            static Clay_TextElementConfig knob_val_cfg = {};
            knob_val_cfg.textColor = g_theme.text;
            knob_val_cfg.fontSize = FONT_SIZE_SMALL;
            knob_val_cfg.wrapMode = CLAY_TEXT_WRAP_NONE;

            CLAY(val_eid,
                .layout = {
                    .sizing = { .width = CLAY_SIZING_FIXED(knob_size), .height = CLAY_SIZING_FIT() },
                    .padding = { uip(2), uip(2), uip(1), uip(1) },
                    .childAlignment = { .x = CLAY_ALIGN_X_CENTER },
                }
            ) {
                CLAY_TEXT(val_str, &knob_val_cfg);
            }

            if (editable && val_hovered && g_wstate.mouse_clicked) {
                value_edit_enter(knob_hash, *value, is_signed);
            }
        }

        // Label
        CLAY_TEXT(clay_str(label), &g_widget_text_config_dim);
    }

    return changed;
}

////////////////////////////////
// Horizontal Slider

B32 ui_slider_h(const char *id, const char *label, F32 *value, F32 max_val, B32 is_signed, F32 default_val, B32 editable) {
    ensure_widget_text_configs();

    B32 changed = 0;
    F32 normalized = value_normalize(*value, max_val, is_signed);
    U32 hash = Clay__HashString(clay_str(id), 0).id;
    B32 is_editing = (editable && g_wstate.knob_edit_id == hash);
    UI_KnobDragState *kd = &g_wstate.knob_drag;

    F32 track_w = WIDGET_SLIDER_H_WIDTH;
    F32 track_h = WIDGET_SLIDER_H_TRACK_H;
    F32 thumb_w = WIDGET_SLIDER_H_THUMB_W;
    F32 thumb_h = WIDGET_SLIDER_H_THUMB_H;

    // Drag interaction (horizontal mouse delta)
    if (!is_editing && kd->dragging_id == hash && g_wstate.input.mouse_down) {
        B32 shift_now = g_wstate.input.shift_held;
        if (shift_now != kd->was_shift) {
            kd->drag_start_x = g_wstate.input.mouse_pos.x;
            kd->value_at_start = *value;
            kd->was_shift = shift_now;
        }
        F32 dx = g_wstate.input.mouse_pos.x - kd->drag_start_x; // right = positive
        F32 sensitivity = 200.0f * g_ui_scale;
        if (shift_now) sensitivity *= 5.0f;
        F32 range = is_signed ? (2.0f * max_val) : max_val;
        F32 new_val = kd->value_at_start + (dx / sensitivity) * range;
        F32 lo = is_signed ? -max_val : 0.0f;
        if (new_val < lo) new_val = lo;
        if (new_val > max_val) new_val = max_val;
        if (new_val != *value) { *value = new_val; changed = 1; }
        normalized = value_normalize(*value, max_val, is_signed);
    }

    // Text edit keyboard input
    if (is_editing) {
        S32 result = value_edit_process_keys(value, max_val, is_signed, &changed);
        if (result) {
            is_editing = 0;
            normalized = value_normalize(*value, max_val, is_signed);
        }
    }

    // Format value text
    S32 val_len = 0;
    char *val_text = is_editing ? nullptr : value_format_text(*value, is_signed, &val_len);

    // Dimmed accent for fill bar
    Clay_Color fill_color = g_theme.accent;
    fill_color.a = 160;

    // Layout: vertical column (label → hit area with track → value/edit)
    CLAY(WID(id),
        .layout = {
            .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() },
            .childGap = WIDGET_KNOB_LABEL_GAP,
            .childAlignment = { .x = CLAY_ALIGN_X_CENTER },
            .layoutDirection = CLAY_TOP_TO_BOTTOM,
        }
    ) {
        // Label
        CLAY_TEXT(clay_str(label), &g_widget_text_config_dim);

        // Hit area (transparent, sized to encompass thumb travel)
        Clay_ElementId hit_eid = CLAY_IDI("SlHHit", (S32)hash);
        B32 hovered = Clay_PointerOver(hit_eid);

        CLAY(hit_eid,
            .layout = {
                .sizing = { .width = CLAY_SIZING_FIXED(track_w), .height = CLAY_SIZING_FIXED(thumb_h) },
                .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER },
            }
        ) {
            // Visible track (centered inside hit area)
            CLAY(CLAY_IDI("SlHTrack", (S32)hash),
                .layout = {
                    .sizing = { .width = CLAY_SIZING_FIXED(track_w), .height = CLAY_SIZING_FIXED(track_h) },
                },
                .backgroundColor = g_theme.bg_dark,
                .cornerRadius = CLAY_CORNER_RADIUS(track_h / 2.0f)
            ) {
                // Fill bar
                F32 fill_w = normalized * track_w;
                if (fill_w < 1.0f) fill_w = 1.0f;
                CLAY(CLAY_IDI("SlHFill", (S32)hash),
                    .layout = {
                        .sizing = { .width = CLAY_SIZING_FIXED(fill_w), .height = CLAY_SIZING_GROW() },
                    },
                    .backgroundColor = fill_color,
                    .cornerRadius = CLAY_CORNER_RADIUS(track_h / 2.0f)
                ) {}
            }

            // Floating icon thumb (attached to hit area)
            if (g_icon_pool_count < UI_MAX_ICONS_PER_FRAME) {
                CustomIconData *idata = &g_icon_pool[g_icon_pool_count++];
                idata->type = CUSTOM_RENDER_ICON;
                idata->icon_id = (S32)UI_ICON_SLIDER_THUMB;
                idata->color = g_theme.accent;

                CLAY(CLAY_IDI("SlHThumb", (S32)hash),
                    .layout = {
                        .sizing = { .width = CLAY_SIZING_FIXED(thumb_w), .height = CLAY_SIZING_FIXED(thumb_h) },
                    },
                    .floating = {
                        .offset = {
                            .x = normalized * (track_w - thumb_w),
                            .y = 0,
                        },
                        .attachPoints = {
                            .element = CLAY_ATTACH_POINT_LEFT_TOP,
                            .parent = CLAY_ATTACH_POINT_LEFT_TOP,
                        },
                        .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH,
                        .attachTo = CLAY_ATTACH_TO_PARENT,
                        .clipTo = CLAY_CLIP_TO_ATTACHED_PARENT,
                    },
                    .custom = { .customData = idata },
                ) {}
            }
        }

        // Click: F64-click resets, single click starts drag
        if (!is_editing && hovered && g_wstate.mouse_clicked && kd->dragging_id == 0) {
            B32 is_double_click = (kd->last_click_id == hash &&
                                   (g_frame_number - kd->last_click_frame) < 20);
            kd->last_click_id = hash;
            kd->last_click_frame = g_frame_number;

            if (is_double_click) {
                if (*value != default_val) { *value = default_val; changed = 1; }
                normalized = value_normalize(*value, max_val, is_signed);
                kd->last_click_id = 0;
            } else {
                kd->dragging_id = hash;
                kd->drag_start_x = g_wstate.input.mouse_pos.x;
                kd->value_at_start = *value;
            }
        }

        // Value text / text edit area
        if (is_editing) {
            value_edit_render(hash, track_w);
            value_edit_click_away(value_edit_eid(hash), value, max_val, is_signed, &changed);
        } else if (val_text && val_len > 0) {
            Clay_ElementId val_eid = CLAY_IDI("SlHVal", (S32)hash);
            B32 val_hovered = Clay_PointerOver(val_eid);

            Clay_String val_str = { .isStaticallyAllocated = false, .length = val_len, .chars = val_text };
            static Clay_TextElementConfig sl_val_cfg = {};
            sl_val_cfg.textColor = g_theme.text;
            sl_val_cfg.fontSize = FONT_SIZE_SMALL;
            sl_val_cfg.wrapMode = CLAY_TEXT_WRAP_NONE;

            CLAY(val_eid,
                .layout = {
                    .sizing = { .width = CLAY_SIZING_FIXED(track_w), .height = CLAY_SIZING_FIT() },
                    .padding = { uip(2), uip(2), uip(1), uip(1) },
                    .childAlignment = { .x = CLAY_ALIGN_X_CENTER },
                }
            ) {
                CLAY_TEXT(val_str, &sl_val_cfg);
            }

            if (editable && val_hovered && g_wstate.mouse_clicked) {
                value_edit_enter(hash, *value, is_signed);
            }
        }
    }

    return changed;
}

////////////////////////////////
// Vertical Slider

B32 ui_slider_v(const char *id, const char *label, F32 *value, F32 max_val, B32 is_signed, F32 default_val, B32 editable) {
    ensure_widget_text_configs();

    B32 changed = 0;
    F32 normalized = value_normalize(*value, max_val, is_signed);
    U32 hash = Clay__HashString(clay_str(id), 0).id;
    B32 is_editing = (editable && g_wstate.knob_edit_id == hash);
    UI_KnobDragState *kd = &g_wstate.knob_drag;

    F32 track_w = WIDGET_SLIDER_V_TRACK_W;
    F32 track_h = WIDGET_SLIDER_V_HEIGHT;
    F32 thumb_w = WIDGET_SLIDER_V_THUMB_W;
    F32 thumb_h = WIDGET_SLIDER_V_THUMB_H;

    // Drag interaction (vertical mouse delta, up = increase)
    if (!is_editing && kd->dragging_id == hash && g_wstate.input.mouse_down) {
        B32 shift_now = g_wstate.input.shift_held;
        if (shift_now != kd->was_shift) {
            kd->drag_start_y = g_wstate.input.mouse_pos.y;
            kd->value_at_start = *value;
            kd->was_shift = shift_now;
        }
        F32 dy = kd->drag_start_y - g_wstate.input.mouse_pos.y; // up = positive
        F32 sensitivity = 200.0f * g_ui_scale;
        if (shift_now) sensitivity *= 5.0f;
        F32 range = is_signed ? (2.0f * max_val) : max_val;
        F32 new_val = kd->value_at_start + (dy / sensitivity) * range;
        F32 lo = is_signed ? -max_val : 0.0f;
        if (new_val < lo) new_val = lo;
        if (new_val > max_val) new_val = max_val;
        if (new_val != *value) { *value = new_val; changed = 1; }
        normalized = value_normalize(*value, max_val, is_signed);
    }

    // Text edit keyboard input
    if (is_editing) {
        S32 result = value_edit_process_keys(value, max_val, is_signed, &changed);
        if (result) {
            is_editing = 0;
            normalized = value_normalize(*value, max_val, is_signed);
        }
    }

    // Format value text
    S32 val_len = 0;
    char *val_text = is_editing ? nullptr : value_format_text(*value, is_signed, &val_len);

    // Dimmed accent for fill bar
    Clay_Color fill_color = g_theme.accent;
    fill_color.a = 160;

    // Layout: vertical column (label → hit area with track → value/edit)
    CLAY(WID(id),
        .layout = {
            .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() },
            .childGap = WIDGET_KNOB_LABEL_GAP,
            .childAlignment = { .x = CLAY_ALIGN_X_CENTER },
            .layoutDirection = CLAY_TOP_TO_BOTTOM,
        }
    ) {
        // Label
        CLAY_TEXT(clay_str(label), &g_widget_text_config_dim);

        // Hit area (transparent, sized to encompass thumb travel)
        Clay_ElementId hit_eid = CLAY_IDI("SlVHit", (S32)hash);
        B32 hovered = Clay_PointerOver(hit_eid);

        CLAY(hit_eid,
            .layout = {
                .sizing = { .width = CLAY_SIZING_FIXED(thumb_w), .height = CLAY_SIZING_FIXED(track_h) },
                .childAlignment = { .x = CLAY_ALIGN_X_CENTER },
            }
        ) {
            // Visible track (centered inside hit area)
            CLAY(CLAY_IDI("SlVTrack", (S32)hash),
                .layout = {
                    .sizing = { .width = CLAY_SIZING_FIXED(track_w), .height = CLAY_SIZING_FIXED(track_h) },
                    .childAlignment = { .y = CLAY_ALIGN_Y_BOTTOM },
                },
                .backgroundColor = g_theme.bg_dark,
                .cornerRadius = CLAY_CORNER_RADIUS(track_w / 2.0f)
            ) {
                // Fill bar (from bottom)
                F32 fill_h = normalized * track_h;
                if (fill_h < 1.0f) fill_h = 1.0f;
                CLAY(CLAY_IDI("SlVFill", (S32)hash),
                    .layout = {
                        .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(fill_h) },
                    },
                    .backgroundColor = fill_color,
                    .cornerRadius = CLAY_CORNER_RADIUS(track_w / 2.0f)
                ) {}
            }

            // Floating icon thumb (attached to hit area)
            if (g_icon_pool_count < UI_MAX_ICONS_PER_FRAME) {
                CustomIconData *idata = &g_icon_pool[g_icon_pool_count++];
                idata->type = CUSTOM_RENDER_ICON;
                idata->icon_id = (S32)UI_ICON_SLIDER_THUMB;
                idata->color = g_theme.accent;

                CLAY(CLAY_IDI("SlVThumb", (S32)hash),
                    .layout = {
                        .sizing = { .width = CLAY_SIZING_FIXED(thumb_w), .height = CLAY_SIZING_FIXED(thumb_h) },
                    },
                    .floating = {
                        .offset = {
                            .x = 0,
                            .y = (1.0f - normalized) * (track_h - thumb_h),
                        },
                        .attachPoints = {
                            .element = CLAY_ATTACH_POINT_LEFT_TOP,
                            .parent = CLAY_ATTACH_POINT_LEFT_TOP,
                        },
                        .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH,
                        .attachTo = CLAY_ATTACH_TO_PARENT,
                        .clipTo = CLAY_CLIP_TO_ATTACHED_PARENT,
                    },
                    .custom = { .customData = idata },
                ) {}
            }
        }

        // Click: F64-click resets, single click starts drag
        if (!is_editing && hovered && g_wstate.mouse_clicked && kd->dragging_id == 0) {
            B32 is_double_click = (kd->last_click_id == hash &&
                                   (g_frame_number - kd->last_click_frame) < 20);
            kd->last_click_id = hash;
            kd->last_click_frame = g_frame_number;

            if (is_double_click) {
                if (*value != default_val) { *value = default_val; changed = 1; }
                normalized = value_normalize(*value, max_val, is_signed);
                kd->last_click_id = 0;
            } else {
                kd->dragging_id = hash;
                kd->drag_start_y = g_wstate.input.mouse_pos.y;
                kd->value_at_start = *value;
            }
        }

        // Value text / text edit area
        if (is_editing) {
            value_edit_render(hash, thumb_w * 2.0f);
            value_edit_click_away(value_edit_eid(hash), value, max_val, is_signed, &changed);
        } else if (val_text && val_len > 0) {
            Clay_ElementId val_eid = CLAY_IDI("SlVVal", (S32)hash);
            B32 val_hovered = Clay_PointerOver(val_eid);

            Clay_String val_str = { .isStaticallyAllocated = false, .length = val_len, .chars = val_text };
            static Clay_TextElementConfig sl_val_cfg = {};
            sl_val_cfg.textColor = g_theme.text;
            sl_val_cfg.fontSize = FONT_SIZE_SMALL;
            sl_val_cfg.wrapMode = CLAY_TEXT_WRAP_NONE;

            CLAY(val_eid,
                .layout = {
                    .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() },
                    .padding = { uip(2), uip(2), uip(1), uip(1) },
                    .childAlignment = { .x = CLAY_ALIGN_X_CENTER },
                }
            ) {
                CLAY_TEXT(val_str, &sl_val_cfg);
            }

            if (editable && val_hovered && g_wstate.mouse_clicked) {
                value_edit_enter(hash, *value, is_signed);
            }
        }
    }

    return changed;
}

////////////////////////////////
// Fader (DAW-style vertical slider with fader cap icon)

B32 ui_fader(const char *id, const char *label, F32 *value, F32 max_val, B32 is_signed, F32 default_val, B32 editable) {
    ensure_widget_text_configs();

    B32 changed = 0;
    F32 normalized = value_normalize(*value, max_val, is_signed);
    U32 hash = Clay__HashString(clay_str(id), 0).id;
    B32 is_editing = (editable && g_wstate.knob_edit_id == hash);
    UI_KnobDragState *kd = &g_wstate.knob_drag;

    F32 track_w = WIDGET_FADER_TRACK_W;
    F32 track_h = WIDGET_FADER_HEIGHT;
    F32 cap_w = WIDGET_FADER_CAP_W;
    F32 cap_h = WIDGET_FADER_CAP_H;

    // Drag interaction (vertical mouse delta, up = increase)
    if (!is_editing && kd->dragging_id == hash && g_wstate.input.mouse_down) {
        B32 shift_now = g_wstate.input.shift_held;
        if (shift_now != kd->was_shift) {
            kd->drag_start_y = g_wstate.input.mouse_pos.y;
            kd->value_at_start = *value;
            kd->was_shift = shift_now;
        }
        F32 dy = kd->drag_start_y - g_wstate.input.mouse_pos.y;
        F32 sensitivity = 200.0f * g_ui_scale;
        if (shift_now) sensitivity *= 5.0f;
        F32 range = is_signed ? (2.0f * max_val) : max_val;
        F32 new_val = kd->value_at_start + (dy / sensitivity) * range;
        F32 lo = is_signed ? -max_val : 0.0f;
        if (new_val < lo) new_val = lo;
        if (new_val > max_val) new_val = max_val;
        if (new_val != *value) { *value = new_val; changed = 1; }
        normalized = value_normalize(*value, max_val, is_signed);
    }

    // Text edit keyboard input
    if (is_editing) {
        S32 result = value_edit_process_keys(value, max_val, is_signed, &changed);
        if (result) {
            is_editing = 0;
            normalized = value_normalize(*value, max_val, is_signed);
        }
    }

    // Format value text
    S32 val_len = 0;
    char *val_text = is_editing ? nullptr : value_format_text(*value, is_signed, &val_len);

    // Tick mark dimensions
    F32 tick_major_w = WIDGET_FADER_TICK_MAJOR_W;
    F32 tick_minor_w = WIDGET_FADER_TICK_MINOR_W;
    F32 tick_h = WIDGET_FADER_TICK_H;
    S32 num_ticks = 10;
    F32 track_left = (cap_w - track_w) / 2.0f;

    // Layout: vertical column (label → hit area with track → value/edit)
    CLAY(WID(id),
        .layout = {
            .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() },
            .childGap = WIDGET_KNOB_LABEL_GAP,
            .childAlignment = { .x = CLAY_ALIGN_X_CENTER },
            .layoutDirection = CLAY_TOP_TO_BOTTOM,
        }
    ) {
        // Label
        CLAY_TEXT(clay_str(label), &g_widget_text_config_dim);

        // Hit area (transparent, sized to encompass fader cap travel)
        Clay_ElementId hit_eid = CLAY_IDI("FdrHit", (S32)hash);
        B32 hovered = Clay_PointerOver(hit_eid);

        CLAY(hit_eid,
            .layout = {
                .sizing = { .width = CLAY_SIZING_FIXED(cap_w), .height = CLAY_SIZING_FIXED(track_h) },
                .childAlignment = { .x = CLAY_ALIGN_X_CENTER },
            }
        ) {
            // Visible track (centered inside hit area, empty)
            CLAY(CLAY_IDI("FdrTrack", (S32)hash),
                .layout = {
                    .sizing = { .width = CLAY_SIZING_FIXED(track_w), .height = CLAY_SIZING_FIXED(track_h) },
                },
                .backgroundColor = g_theme.bg_dark,
                .cornerRadius = CLAY_CORNER_RADIUS(track_w / 2.0f)
            ) {}

            // Tick marks on both sides of the track
            for (S32 i = 0; i <= num_ticks; i++) {
                F32 norm = (F32)i / (F32)num_ticks;
                F32 tick_y = (1.0f - norm) * (track_h - tick_h);
                F32 tw = (i % 5 == 0) ? tick_major_w : tick_minor_w;

                // Left tick
                CLAY(CLAY_IDI("FdrTkL", (S32)(hash * 100 + i)),
                    .layout = {
                        .sizing = { .width = CLAY_SIZING_FIXED(tw), .height = CLAY_SIZING_FIXED(tick_h) },
                    },
                    .backgroundColor = g_theme.text_dim,
                    .floating = {
                        .offset = {
                            .x = track_left - tw,
                            .y = tick_y,
                        },
                        .attachPoints = {
                            .element = CLAY_ATTACH_POINT_LEFT_TOP,
                            .parent = CLAY_ATTACH_POINT_LEFT_TOP,
                        },
                        .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH,
                        .attachTo = CLAY_ATTACH_TO_PARENT,
                        .clipTo = CLAY_CLIP_TO_ATTACHED_PARENT,
                    },
                ) {}

                // Right tick
                CLAY(CLAY_IDI("FdrTkR", (S32)(hash * 100 + i)),
                    .layout = {
                        .sizing = { .width = CLAY_SIZING_FIXED(tw), .height = CLAY_SIZING_FIXED(tick_h) },
                    },
                    .backgroundColor = g_theme.text_dim,
                    .floating = {
                        .offset = {
                            .x = track_left + track_w,
                            .y = tick_y,
                        },
                        .attachPoints = {
                            .element = CLAY_ATTACH_POINT_LEFT_TOP,
                            .parent = CLAY_ATTACH_POINT_LEFT_TOP,
                        },
                        .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH,
                        .attachTo = CLAY_ATTACH_TO_PARENT,
                        .clipTo = CLAY_CLIP_TO_ATTACHED_PARENT,
                    },
                ) {}
            }

            // Floating fader cap (RGBA icon from asset SVG)
            if (g_icon_pool_count < UI_MAX_ICONS_PER_FRAME) {
                CustomIconData *idata = &g_icon_pool[g_icon_pool_count++];
                idata->type = CUSTOM_RENDER_ICON;
                idata->icon_id = (S32)UI_ICON_FADER;
                idata->color = Clay_Color{255, 255, 255, 255};

                F32 cap_y = (1.0f - normalized) * (track_h - cap_h);

                CLAY(CLAY_IDI("FdrCap", (S32)hash),
                    .layout = {
                        .sizing = { .width = CLAY_SIZING_FIXED(cap_w), .height = CLAY_SIZING_FIXED(cap_h) },
                    },
                    .floating = {
                        .offset = { .x = 0, .y = cap_y },
                        .attachPoints = {
                            .element = CLAY_ATTACH_POINT_LEFT_TOP,
                            .parent = CLAY_ATTACH_POINT_LEFT_TOP,
                        },
                        .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH,
                        .attachTo = CLAY_ATTACH_TO_PARENT,
                        .clipTo = CLAY_CLIP_TO_ATTACHED_PARENT,
                    },
                    .custom = { .customData = idata },
                ) {}

                // Color tint overlay on top of fader cap
                Clay_Color tint = g_theme.accent;
                tint.a = 80;
                F32 cap_corner = cap_w * (3.0f / 62.2f);
                CLAY(CLAY_IDI("FdrTint", (S32)hash),
                    .layout = {
                        .sizing = { .width = CLAY_SIZING_FIXED(cap_w), .height = CLAY_SIZING_FIXED(cap_h) },
                    },
                    .backgroundColor = tint,
                    .cornerRadius = CLAY_CORNER_RADIUS(cap_corner),
                    .floating = {
                        .offset = { .x = 0, .y = cap_y },
                        .attachPoints = {
                            .element = CLAY_ATTACH_POINT_LEFT_TOP,
                            .parent = CLAY_ATTACH_POINT_LEFT_TOP,
                        },
                        .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_PASSTHROUGH,
                        .attachTo = CLAY_ATTACH_TO_PARENT,
                        .clipTo = CLAY_CLIP_TO_ATTACHED_PARENT,
                    },
                ) {}
            }
        }

        // Click: F64-click resets, single click starts drag
        if (!is_editing && hovered && g_wstate.mouse_clicked && kd->dragging_id == 0) {
            B32 is_double_click = (kd->last_click_id == hash &&
                                   (g_frame_number - kd->last_click_frame) < 20);
            kd->last_click_id = hash;
            kd->last_click_frame = g_frame_number;

            if (is_double_click) {
                if (*value != default_val) { *value = default_val; changed = 1; }
                normalized = value_normalize(*value, max_val, is_signed);
                kd->last_click_id = 0;
            } else {
                kd->dragging_id = hash;
                kd->drag_start_y = g_wstate.input.mouse_pos.y;
                kd->value_at_start = *value;
            }
        }

        // Value text / text edit area
        if (is_editing) {
            value_edit_render(hash, cap_w * 2.0f);
            value_edit_click_away(value_edit_eid(hash), value, max_val, is_signed, &changed);
        } else if (val_text && val_len > 0) {
            Clay_ElementId val_eid = CLAY_IDI("FdrVal", (S32)hash);
            B32 val_hovered = Clay_PointerOver(val_eid);

            Clay_String val_str = { .isStaticallyAllocated = false, .length = val_len, .chars = val_text };
            static Clay_TextElementConfig fdr_val_cfg = {};
            fdr_val_cfg.textColor = g_theme.text;
            fdr_val_cfg.fontSize = FONT_SIZE_SMALL;
            fdr_val_cfg.wrapMode = CLAY_TEXT_WRAP_NONE;

            CLAY(val_eid,
                .layout = {
                    .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() },
                    .padding = { uip(2), uip(2), uip(1), uip(1) },
                    .childAlignment = { .x = CLAY_ALIGN_X_CENTER },
                }
            ) {
                CLAY_TEXT(val_str, &fdr_val_cfg);
            }

            if (editable && val_hovered && g_wstate.mouse_clicked) {
                value_edit_enter(hash, *value, is_signed);
            }
        }
    }

    return changed;
}
