import { createSignal, createEffect, createMemo, onCleanup, For, Show, splitProps, JSX, JSXElement } from "solid-js";
import { Portal } from "solid-js/web";
import {IconInline} from "./Icons.tsx";
import { readAccessor } from "../utils/accessors.ts";

// --- Tailwind class building helpers ---
const INPUT_BASE = "bg-white block w-full border rounded-default shadow-xs text-sm focus:outline-2 focus:outline-offset-1 disabled:bg-neutral-100 disabled:cursor-not-allowed";
const INPUT_BASE_DARK = "bg-dark text-text-on-dark placeholder:text-text-on-dark-faint block w-full border rounded-default shadow-xs text-sm focus:outline-2 focus:outline-offset-1 disabled:opacity-50 disabled:cursor-not-allowed";

const CONTROL_H = "h-[38px]";
const CONTROL_H_SM = "h-[30px]";
const _controlH = (small?: boolean): string => (small ? CONTROL_H_SM : CONTROL_H);

function _fieldBorder(error?: unknown, success?: unknown, onDark?: boolean): string {
    const borderNormal = onDark ? "border-border-on-dark focus:outline-sky-500" : "border-neutral-300 focus:outline-sky-500";
    return error ? "border-red-500 focus:outline-red-500"
        : success ? "border-green-500 focus:outline-green-500"
        : borderNormal;
}

function _inputCls(small?: boolean, error?: unknown, success?: unknown, extra?: string, onDark?: boolean): string {
    const base = onDark ? INPUT_BASE_DARK : INPUT_BASE;
    return base + " " + _controlH(small) + (small ? " p-1" : " p-2")
        + " " + _fieldBorder(error, success, onDark)
        + (extra ? " " + extra : "");
}

// Textarea shares the input look but must grow with its content, so it opts out
// of the fixed control height.
function _textareaCls(small?: boolean, error?: unknown, extra?: string, onDark?: boolean): string {
    const base = onDark ? INPUT_BASE_DARK : INPUT_BASE;
    return base + (small ? " p-1" : " p-2")
        + " " + _fieldBorder(error, false, onDark)
        + (extra ? " " + extra : "");
}

function _prefixCls(small?: boolean, error?: unknown, onDark?: boolean): string {
    const base = onDark
        ? "bg-dark-raised text-text-on-dark-muted border shadow-xs border-r-0 flex items-center shrink-0 rounded-l-default"
        : "bg-neutral-100 border shadow-xs border-r-0 flex items-center shrink-0 rounded-l-default";
    const borderNormal = onDark ? "border-border-on-dark" : "border-neutral-300";
    return base
        + (error ? " border-red-500" : " " + borderNormal)
        + (small ? " p-1 text-sm" : " p-2");
}

const FORM_ERROR = "block text-red-600 text-xs mt-1";
const FORM_SUCCESS = "block text-green-600 text-xs mt-1";
const INPUT_GROUP = "flex flex-row items-stretch w-full text-sm";

const TRIGGER_BASE = "bg-white border border-neutral-300 rounded-default shadow-xs text-sm w-full text-left flex items-center justify-between gap-2 cursor-pointer disabled:bg-neutral-100 disabled:cursor-not-allowed";
const TRIGGER_BASE_DARK = "bg-dark-raised border border-border-on-dark text-text-on-dark rounded-default shadow-xs text-sm w-full text-left flex items-center justify-between gap-2 cursor-pointer hover:border-border-on-dark-hover disabled:opacity-50 disabled:cursor-not-allowed";
const DROPDOWN = "bg-white border border-neutral-300 rounded-default shadow-lg max-h-60 overflow-auto";
const DROPDOWN_DARK = "bg-dark-raised border border-border-on-dark rounded-default shadow-lg max-h-60 overflow-auto";
const DROPDOWN_SEARCH_WRAP = "sticky top-0 bg-white border-b border-neutral-200 p-2";
const DROPDOWN_SEARCH_WRAP_DARK = "sticky top-0 bg-dark-raised border-b border-border-on-dark p-2";
const DROPDOWN_SEARCH_INPUT = "w-full bg-white border border-neutral-300 rounded-default shadow-xs text-sm p-1 focus:outline-2 focus:outline-sky-500 focus:outline-offset-1";
const DROPDOWN_SEARCH_INPUT_DARK = "w-full bg-dark border border-border-on-dark text-text-on-dark placeholder:text-text-on-dark-faint rounded-default shadow-xs text-sm p-1 focus:outline-2 focus:outline-sky-500 focus:outline-offset-1";
const DROPDOWN_OPTION = "w-full text-left p-2 text-sm cursor-pointer flex items-center gap-2 bg-transparent border-none hover:bg-neutral-100 disabled:text-neutral-400 disabled:cursor-not-allowed whitespace-nowrap";
const DROPDOWN_OPTION_DARK = "w-full text-left p-2 text-sm cursor-pointer flex items-center gap-2 bg-transparent border-none text-text-on-dark hover:bg-white/5 disabled:text-text-on-dark-faint disabled:cursor-not-allowed whitespace-nowrap";
const DROPDOWN_OPTION_HIGHLIGHT = "bg-neutral-100";
const DROPDOWN_OPTION_HIGHLIGHT_DARK = "bg-white/10";
const DROPDOWN_NO_RESULTS = "p-2 text-sm text-neutral-500 text-center";
const DROPDOWN_NO_RESULTS_DARK = "p-2 text-sm text-text-on-dark-muted text-center";
const SELECT_ALL_WRAP = "border-b border-neutral-200";
const SELECT_ALL_BTN = "w-full text-left p-2 text-sm cursor-pointer text-neutral-600 font-medium bg-transparent border-none hover:bg-neutral-100";

// -- Shared types --------------------------------------------------
type CommonInputAttrs =
    | "autocomplete"
    | "disabled"
    | "id"
    | "inputmode"
    | "maxlength"
    | "name"
    | "onblur"
    | "onchange"
    | "onclick"
    | "onfocus"
    | "oninput"
    | "onkeydown"
    | "placeholder"
    | "readonly"
    | "required"
    | "value"
    // Legacy attributes (camel case)
    | "inputMode"
    | "maxLength"
    | "onBlur"
    | "onchange"
    | "onclick"
    | "onFocus"
    | "onInput"
    | "onKeyDown";

// Subsets valid for <select> (excludes input-only attrs)
type CommonSelectAttrs = Exclude<CommonInputAttrs, "maxlength" | "inputmode" | "placeholder" | "readonly" | "maxLength" | "inputMode">;

export interface FormInputCommonProps {
    passwordManagerIgnore?: boolean;
    error?: string;
    success?: string;
    style?: string | Record<string, string | number>;
}

export interface FormInputProps
    extends
        FormInputCommonProps,
        Pick<JSX.InputHTMLAttributes<HTMLInputElement>, CommonInputAttrs | "type" | "max" | "min" | "step" | "accept"> {
    small?: boolean;
    ref?: (el: HTMLInputElement) => void;
    class?: string;
    onDark?: boolean;
}

export interface FormTextareaProps
    extends FormInputCommonProps, Pick<JSX.TextareaHTMLAttributes<HTMLTextAreaElement>, CommonInputAttrs | "rows" > {
    ref?: (el: HTMLTextAreaElement) => void;
    small?: boolean;
    class?: string;
    onDark?: boolean;
}

export interface FormInputGroupProps extends FormInputProps {
    prefix?: JSXElement;
}

export interface FormNumberInputProps extends FormInputProps {
    int?: boolean;
    unsigned?: boolean;
}

export interface FormSelectProps
    extends FormInputCommonProps, Pick<JSX.SelectHTMLAttributes<HTMLSelectElement>, CommonSelectAttrs> {
    small?: boolean;
    ref?: (el: HTMLSelectElement) => void;
    class?: string;
    onDark?: boolean;
}

export interface FormSelectOption {
    value: string;
    label: string;
    disabled?: boolean;
}

export type ComboboxFieldWidth = "fill" | "grow" | "narrow" | "default" | "wide" | "auto";

const COMBOBOX_WIDTH_CLS: Record<ComboboxFieldWidth, string> = {
    fill: "w-full min-w-0 max-w-full",
    grow: "w-full min-w-[8rem] max-w-[16rem]",
    narrow: "w-full min-w-[7rem] max-w-[11rem]",
    default: "w-full min-w-[9rem] max-w-[14rem]",
    wide: "w-full min-w-[10rem] max-w-[20rem]",
    auto: "w-auto min-w-[8rem] max-w-full",
};

function _floatingDropdownPos(triggerEl: HTMLElement | undefined, gap = 4): { top: number; left: number; width: number } {
    if (!triggerEl) return { top: 0, left: 0, width: 0 };
    const rect = triggerEl.getBoundingClientRect();
    return { top: rect.bottom + gap, left: rect.left, width: rect.width };
}

function _floatingDropdownStyle(pos: { top: number; left: number; width: number }, maxWidth = 400): string {
    const maxW = Math.min(maxWidth, window.innerWidth - pos.left - 8);
    return `position:fixed;top:${pos.top}px;left:${pos.left}px;min-width:${pos.width}px;width:max-content;max-width:${maxW}px;z-index:200;`;
}

function _comboboxRootCls(className?: string, fieldWidth: ComboboxFieldWidth = "fill"): string {
    return "relative " + COMBOBOX_WIDTH_CLS[fieldWidth] + (className ? " " + className : "");
}

export interface FormComboboxProps extends Pick<JSX.SelectHTMLAttributes<HTMLSelectElement>, CommonSelectAttrs> {
    options: FormSelectOption[];
    placeholder?: string;
    searchable?: boolean;
    searchPlaceholder?: string;
    small?: boolean;
    renderOption?: (option: FormSelectOption) => JSXElement;
    class?: string;
    fieldWidth?: ComboboxFieldWidth;
    onDark?: boolean;
    maxDisplayLength?: number;
}

export interface FormMultiSelectProps {
    options: FormSelectOption[];
    value?: string[] | (() => string[]);
    onchange?: (value: string[]) => void;
    placeholder?: string;
    searchable?: boolean;
    searchPlaceholder?: string;
    showSelectAll?: boolean;
    maxTagsBeforeCollapse?: number;
    disabled?: boolean;
    small?: boolean;
    class?: string;
    fieldWidth?: ComboboxFieldWidth;
}

export interface FormComboboxTriggerProps {
    trigger: unknown;
    options: FormSelectOption[];
    value?: string | (() => string);
    onchange?: (value: string) => void;
    searchable?: boolean;
    searchPlaceholder?: string;
    disabled?: boolean | (() => boolean);
    small?: boolean;
    minWidth?: number;
    align?: "left" | "right";
    class?: string;
}

export interface FormMultiSelectTriggerProps {
    trigger: JSXElement;
    options: FormSelectOption[] | (() => FormSelectOption[]);
    value?: string[] | (() => string[]);
    onchange?: (value: string[]) => void;
    searchable?: boolean;
    searchPlaceholder?: string;
    showSelectAll?: boolean;
    disabled?: boolean;
    small?: boolean;
    minWidth?: number;
    align?: "left" | "right";
    class?: string;
}

export function FormInput(props: FormInputProps) {
    const cls = () => _inputCls(props.small, props.error, props.success, props.class, props.onDark);
    const inputValue = () => readAccessor(props.value, "");
    return <div class="ui-form">
        <input
            ref={(el) => typeof props.ref === "function" && props.ref(el)}
            class={cls()}
            type={props.type}
            value={inputValue()}
            placeholder={props.placeholder}
            autocomplete={props.autocomplete || (props.passwordManagerIgnore ? "off" : undefined)}
            inputmode={props.inputmode}
            data-1p-ignore={props.passwordManagerIgnore}
            data-lpignore={props.passwordManagerIgnore ? "other" : undefined}
            data-form-type={props.passwordManagerIgnore}
            data-bwignore={props.passwordManagerIgnore}
            data-protonpass-ignore={props.passwordManagerIgnore}
            disabled={props.disabled}
            maxlength={props.maxlength}
            min={props.min}
            max={props.max}
            step={props.step}
            name={props.name}
            id={props.id}
            oninput={props.oninput}
            onchange={props.onchange}
            onBlur={props.onblur}
            onFocus={props.onfocus}
            onKeyDown={props.onkeydown}

        />
        <Show when={props.error}>
            <span class={FORM_ERROR}>{props.error}</span>
        </Show>
        <Show when={props.success}>
            <span class={FORM_SUCCESS}>{props.success}</span>
        </Show>
    </div>;
}

export function FormInputGroup(props: FormInputGroupProps) {
    const pfxCls = () => _prefixCls(props.small, props.error, props.onDark);
    const inputCls = () => _inputCls(props.small, props.error, props.success, "rounded-l-none" + (props.class ? " " + props.class : ""), props.onDark);
    const inputValue = () => readAccessor(props.value, "");
    return <div class="ui-form">
        <div class={INPUT_GROUP}>
            <span class={pfxCls()}>{props.prefix}</span>
            <span class="grow flex">
                <input
                    ref={(el) => typeof props.ref === "function" && props.ref(el)}
                    class={inputCls()}
                    type={props.type}
                    value={inputValue()}
                    placeholder={props.placeholder}
                    autocomplete={props.autocomplete || (props.passwordManagerIgnore ? "off" : undefined)}
                    inputmode={props.inputmode}
                    data-1p-ignore={props.passwordManagerIgnore}
                    data-lpignore={props.passwordManagerIgnore ? "other" : undefined}
                    data-form-type={props.passwordManagerIgnore}
                    data-bwignore={props.passwordManagerIgnore}
                    data-protonpass-ignore={props.passwordManagerIgnore}
                    disabled={props.disabled}
                    maxlength={props.maxlength}
                    name={props.name}
                    id={props.id}
                    oninput={props.oninput}
                    onchange={props.onchange}
                    onBlur={props.onblur}
                    onFocus={props.onfocus}
                    onKeyDown={props.onkeydown}
                />
            </span>
        </div>
        <Show when={props.error}>
            <span class={FORM_ERROR}>{props.error}</span>
        </Show>
        <Show when={props.success}>
            <span class={FORM_SUCCESS}>{props.success}</span>
        </Show>
    </div>;
}

export function FormNumberInput(props: FormNumberInputProps) {
    const [local, inputProps] = splitProps(props, ["small", "ref", "class", "oninput", "int", "unsigned"]);
    var regex: RegExp;

    if (local.int) {
        regex = local.unsigned ? /[^0-9]/g : /[^-0-9]/g;
    } else {
        regex = local.unsigned ? /[^0-9.]/g : /[^-0-9.]/g;
    }

    const handleInput = (e) => {
        if (!e || !e.currentTarget) return;
        const input = e.currentTarget;
        input.value = input.value.replace(regex, "");

        // Remove hyphens that aren't at the start of the number
        if (!local.unsigned) {
            const isNegative = input.value.startsWith("-");
            input.value = input.value.replace(/-/g, "");
            if (isNegative) input.value = "-" + input.value;
        }
        const parts = input.value.split(".");
        if (parts.length > 2) {
            input.value = parts[0] + "." + parts.slice(1).join("");
        }
        if (typeof local.oninput === "function") {
            local.oninput(e);
        }
    };

    return <FormInput
        ref={(el) => typeof local.ref === "function" && local.ref(el)}
        type="text"
        inputMode="decimal"
        oninput={handleInput}
        class={local.class}
        small={local.small}
        {...inputProps}
    />;
}

export function FormCarNumberInput(props: FormInputProps) {
    const [local, inputProps] = splitProps(props, ["small", "ref", "class", "oninput"]);

    const handleInput = (e) => {
        if (!e || !e.currentTarget) return;
        const input = e.currentTarget;
        const raw = input.value.toUpperCase().replace(/[^0-9A-Z]/g, "");
        const match = raw.match(/^([0-9]{0,2})([A-Z]{0,2})/);
        input.value = match ? match[1] + match[2] : "";
        if (typeof local.oninput === "function") {
            local.oninput(e);
        }
    };

    return <FormInput
        ref={(el) => typeof local.ref === "function" && local.ref(el)}
        type="text"
        maxLength="4"
        oninput={handleInput}
        class={local.class}
        small={local.small}
        placeholder="e.g. 28"
        {...inputProps}
    />;
}

export function FormCurrencyInput(props: {hideSymbol?: boolean}  & FormInputProps) {
    const [local, inputProps] = splitProps(props, ["small", "ref", "class", "oninput", "hideSymbol"]);

    const handleInput = (e) => {
        if (!e || !e.currentTarget) return;
        const input = e.currentTarget;
        let value = input.value.replace(/[^0-9.]/g, "");
        const parts = value.split(".");
        if (parts.length > 2) {
            value = parts[0] + "." + parts.slice(1).join("");
        }
        if (parts.length === 2 && parts[1].length > 2) {
            value = parts[0] + "." + parts[1].slice(0, 2);
        }
        input.value = value;
        if (typeof local.oninput === "function") {
            local.oninput(e);
        }
    };

    if (local.hideSymbol) {
        return <FormInput
            ref={(el) => typeof local.ref === "function" && local.ref(el)}
            type="text"
            inputMode="decimal"
            oninput={handleInput}
            class={local.class}
            small={local.small}
            {...inputProps}
        />;
    } else {
        return <FormInputGroup
            ref={(el) => typeof local.ref === "function" && local.ref(el)}
            prefix="$"
            type="text"
            inputMode="decimal"
            oninput={handleInput}
            class={local.class}
            small={local.small}
            {...inputProps}
        />;
    }
}

export function FormPercentInput(props: FormInputProps) {
    const [local, inputProps] = splitProps(props, ["small", "ref", "class", "oninput"]);

    const handleInput = (e) => {
        if (!e || !e.currentTarget) return;
        const input = e.currentTarget;
        input.value = input.value.replace(/[^0-9.]/g, "");
        const parts = input.value.split(".");
        if (parts.length > 2) {
            input.value = parts[0] + "." + parts.slice(1).join("");
        }
        if (typeof local.oninput === "function") {
            local.oninput(e);
        }
    };

    return <FormInputGroup
        ref={(el) => typeof local.ref === "function" && local.ref(el)}
        prefix="%"
        type="text"
        inputMode="decimal"
        oninput={handleInput}
        class={local.class}
        small={local.small}
        {...inputProps}
    />;
}

export function FormPhoneInput(props: FormInputProps) {
    const [local, inputProps] = splitProps(props, ["small", "ref", "class", "oninput"]);

    const handleInput = (e) => {
        if (!e || !e.currentTarget) return;
        const input = e.currentTarget;

        let digits = input.value.replace(/\D/g, "");
        digits = digits.slice(0, 10);
        let formatted = "";
        if (digits.length > 0) {
            formatted = "(" + digits.slice(0, 3);
        }
        if (digits.length > 3) {
            formatted += ") " + digits.slice(3, 6);
        }
        if (digits.length > 6) {
            formatted += "-" + digits.slice(6, 10);
        }
        input.value = formatted;
        if (typeof local.oninput === "function") {
            local.oninput(e);
        }
    };

    return <FormInput
        ref={(el) => typeof local.ref === "function" && local.ref(el)}
        type="text"
        inputMode="numeric"
        oninput={handleInput}
        class={local.class}
        small={local.small}
        placeholder="(555) 555-5555"
        {...inputProps}
    />;
}

// @TODO replace this with showIcon prop
export function FormPhoneInputWithIcon(props: {icon?: string} & FormInputGroupProps) {
    const [local, inputProps] = splitProps(props, ["icon", "small", "ref", "class", "oninput"]);

    const handleInput = (e) => {
        if (!e || !e.currentTarget) return;
        const input = e.currentTarget;
        let digits = input.value.replace(/\D/g, "");
        digits = digits.slice(0, 10);
        let formatted = "";
        if (digits.length > 0) {
            formatted = "(" + digits.slice(0, 3);
        }
        if (digits.length > 3) {
            formatted += ") " + digits.slice(3, 6);
        }
        if (digits.length > 6) {
            formatted += "-" + digits.slice(6, 10);
        }
        input.value = formatted;
        if (typeof local.oninput === "function") {
            local.oninput(e);
        }
    };

    const iconEl = <IconInline icon={local.icon || "phone"} size={16} />;

    return <FormInputGroup
        prefix={iconEl}
        ref={(el) => typeof local.ref === "function" && local.ref(el)}
        type="text"
        inputMode="numeric"
        oninput={handleInput}
        class={local.class}
        small={local.small}
        placeholder="(555) 555-5555"
        {...inputProps}
    />;
}

export function FormEmailInput(props: {showIcon?: boolean} & FormInputProps) {
    const [local, inputProps] = splitProps(props, ["showIcon", "small", "ref", "class", "error", "success", "onDark"]);
    const showIcon = () => local.showIcon !== false;
    const pfxCls = () => _prefixCls(local.small, local.error, local.onDark);
    const inputCls = () => _inputCls(local.small, local.error, local.success, "rounded-l-none" + (local.class ? " " + local.class : ""), local.onDark);

    return <Show when={showIcon()} fallback={
        <FormInput
            ref={(el) => typeof local.ref === "function" && local.ref(el)}
            type="email"
            inputMode="email"
            class={local.class}
            small={local.small}
            error={local.error}
            success={local.success}
            onDark={local.onDark}
            {...inputProps}
        />}>
        <div class="ui-form">
            <div class={INPUT_GROUP}>
                <span class={pfxCls()}>
                    <IconInline icon="envelope" size={16} />
                </span>
                <span class="grow flex">
                    <input
                        ref={(el) => typeof local.ref === "function" && local.ref(el)}
                        type="email"
                        inputMode="email"
                        class={inputCls()}
                        {...inputProps}
                    />
                </span>
            </div>
            <Show when={local.error}>
                <span class={FORM_ERROR}>{local.error}</span>
            </Show>
            <Show when={local.success}>
                <span class={FORM_SUCCESS}>{local.success}</span>
            </Show>
        </div>
    </Show>;
}

export function FormURLInput(props: {showIcon: boolean} & FormInputProps) {
    const [local, inputProps] = splitProps(props, ["showIcon", "small", "ref", "class", "onDark"]);
    const showIcon = () => local.showIcon !== false;
    const pfxCls = () => _prefixCls(local.small, false, local.onDark);
    const inputCls = () => _inputCls(local.small, false, false, "rounded-l-none" + (local.class ? " " + local.class : ""), local.onDark);

    return <Show when={showIcon()} fallback={
        <FormInput
            ref={(el) => typeof local.ref === "function" && local.ref(el)}
            type="text"
            inputMode="url"
            class={local.class}
            small={local.small}
            onDark={local.onDark}
            {...inputProps}
        />}>
        <div class="ui-form">
            <div class={INPUT_GROUP}>
                <span class={pfxCls()}>
                    <IconInline icon="globe" size={16} />
                </span>
                <span class="grow flex">
                    <input
                        ref={(el) => typeof local.ref === "function" && local.ref(el)}
                        type="text"
                        inputMode="url"
                        class={inputCls()}
                        {...inputProps}
                    />
                </span>
            </div>
        </div>
    </Show>;
}

export function FormZipCodeInput(props: FormInputProps) {
    const [local, inputProps] = splitProps(props, ["small", "ref", "class", "oninput"]);

    const handleInput = (e) => {
        if (!e || !e.currentTarget) return;
        const input = e.currentTarget;

        input.value = input.value
            .replace(/[^0-9]/g, "")
            .substring(0, 9)
            .replace(/^(\d{5})(\d{1,4})?$/, function (_, a, b) {
                return b ? a + "-" + b : a;
            });

        if (typeof props.oninput === "function") {
            props.oninput(e);
        }
    };

    return <FormInput
        ref={(el) => typeof local.ref === "function" && local.ref(el)}
        type="text"
        inputMode="numeric"
        oninput={handleInput}
        class={local.class}
        small={local.small}
        placeholder="12345"
        {...inputProps}
    />;
}

export const handleTaxIdInput = (e) => {
    if (!e || !e.currentTarget) return;
    const input = e.currentTarget;

    let digits = input.value.replace(/\D/g, "");
    digits = digits.slice(0, 9);
    let formatted = digits.slice(0,2);
    if (digits.length > 2) {
        formatted += "-" + digits.slice(2, 9);
    }
    input.value = formatted;
}

export const handleRateInput = (e) => {
    if (!e || !e.currentTarget) return;
    const input = e.currentTarget;

    let value = input.value.replace(/[^0-9.]/g, "");
    const parts = value.split(".");
    if (parts.length > 3) {
        value = parts[0] + "." + parts.slice(1).join("");
    }
    if (value.includes(".")) {
        const [intPart, decPart] = value.split(".");
        value = intPart + "." + decPart.slice(0, 3);
    }
    value = value.replace(/^0+(?=\d)/, "");
    input.value = value;
}

export function FormSignaturePad(props: { value?: () => string; onchange?: (svg: string) => void; class?: string }) {
    let canvasEl;
    let ctx;
    const CANVAS_WIDTH = 600;
    const CANVAS_HEIGHT = 120;
    const [isDrawing, setIsDrawing] = createSignal(false);
    const [strokes, setStrokes] = createSignal([]);
    const [currentStroke, setCurrentStroke] = createSignal([]);
    const [isEmpty, setIsEmpty] = createSignal(true);

    const sizeCanvas = () => {
        if (!canvasEl) return;
        canvasEl.width = CANVAS_WIDTH;
        canvasEl.height = CANVAS_HEIGHT;
        ctx = canvasEl.getContext("2d");
        redraw();
    };

    const getPoint = (e) => {
        const rect = canvasEl.getBoundingClientRect();
        const scaleX = CANVAS_WIDTH / rect.width;
        const scaleY = CANVAS_HEIGHT / rect.height;
        if (e.touches) {
            return { x: (e.touches[0].clientX - rect.left) * scaleX, y: (e.touches[0].clientY - rect.top) * scaleY };
        }
        return { x: (e.clientX - rect.left) * scaleX, y: (e.clientY - rect.top) * scaleY };
    };

    const redraw = () => {
        if (!ctx) return;
        ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
        ctx.strokeStyle = "#1a1a2e";
        ctx.lineWidth = 2;
        ctx.lineCap = "round";
        ctx.lineJoin = "round";
        const allStrokes = [...strokes(), ...(currentStroke().length > 0 ? [currentStroke()] : [])];
        for (const stroke of allStrokes) {
            if (stroke.length < 2) continue;
            ctx.beginPath();
            ctx.moveTo(stroke[0].x, stroke[0].y);
            for (let i = 1; i < stroke.length; i++) {
                ctx.lineTo(stroke[i].x, stroke[i].y);
            }
            ctx.stroke();
        }
    };

    const strokesToSvg = (allStrokes) => {
        const w = CANVAS_WIDTH;
        const h = CANVAS_HEIGHT;
        const paths = allStrokes.map((stroke) => {
            if (stroke.length < 2) return "";
            let d = `M${stroke[0].x.toFixed(1)},${stroke[0].y.toFixed(1)}`;
            if (stroke.length === 2) {
                d += ` L${stroke[1].x.toFixed(1)},${stroke[1].y.toFixed(1)}`;
            } else {
                for (let i = 1; i < stroke.length - 1; i++) {
                    const mx = ((stroke[i].x + stroke[i + 1].x) / 2).toFixed(1);
                    const my = ((stroke[i].y + stroke[i + 1].y) / 2).toFixed(1);
                    d += ` Q${stroke[i].x.toFixed(1)},${stroke[i].y.toFixed(1)},${mx},${my}`;
                }
                const last = stroke[stroke.length - 1];
                d += ` L${last.x.toFixed(1)},${last.y.toFixed(1)}`;
            }
            return `<path d="${d}" fill="none" stroke="#1a1a2e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`;
        }).filter(Boolean);
        return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="${w}" height="${h}">${paths.join("")}</svg>`;
    };

    const emitChange = (allStrokes) => {
        const hasContent = allStrokes.length > 0 && allStrokes.some(s => s.length >= 2);
        setIsEmpty(!hasContent);
        props.onchange?.(hasContent ? strokesToSvg(allStrokes) : "");
    };

    const startDrawing = (e) => {
        e.preventDefault();
        if (!ctx) sizeCanvas();
        setIsDrawing(true);
        setCurrentStroke([getPoint(e)]);
    };

    const draw = (e) => {
        if (!isDrawing()) return;
        e.preventDefault();
        const point = getPoint(e);
        setCurrentStroke(prev => [...prev, point]);
        redraw();
    };

    const stopDrawing = (e) => {
        if (!isDrawing()) return;
        e?.preventDefault();
        setIsDrawing(false);
        const stroke = currentStroke();
        if (stroke.length >= 2) {
            const newStrokes = [...strokes(), stroke];
            setStrokes(newStrokes);
            emitChange(newStrokes);
        }
        setCurrentStroke([]);
        redraw();
    };

    const clearSignature = () => {
        setStrokes([]);
        setCurrentStroke([]);
        setIsEmpty(true);
        redraw();
        props.onchange?.("");
    };

    const initCanvas = (el) => {
        canvasEl = el;
        requestAnimationFrame(() => sizeCanvas());
    };

    return <div class="ui-form">
        <div class="border border-neutral-300 rounded-default bg-white overflow-hidden">
            <canvas
                class="block w-full h-auto cursor-crosshair touch-none"
                width={CANVAS_WIDTH}
                height={CANVAS_HEIGHT}
                ref={initCanvas}
                onMouseDown={startDrawing}
                onMouseMove={draw}
                onMouseUp={stopDrawing}
                onMouseLeave={stopDrawing}
                onTouchStart={startDrawing}
                onTouchMove={draw}
                onTouchEnd={stopDrawing}
            />
            <div class="flex items-center justify-between px-2 py-1 border-t border-neutral-200 bg-neutral-50">
                <span class="text-xs text-neutral-400 italic">{isEmpty() ? "Sign above" : ""}</span>
                <button type="button" class="text-xs font-medium text-neutral-500 bg-transparent border-none cursor-pointer px-1 py-0.5 rounded-default hover:text-red-600 hover:bg-red-50 disabled:opacity-40 disabled:cursor-not-allowed" onclick={clearSignature} disabled={isEmpty()}>Clear</button>
            </div>
        </div>
    </div>;
}

// @TODO Fix this
export function FormSelect(props: FormSelectProps & { children?: JSXElement }) {
    const [local, selectProps] = splitProps(props, ["small", "ref", "class", "children", "value", "onDark"]);
    const cls = () => _inputCls(local.small, false, false, local.class, local.onDark);
    let selectEl;
    // Set value via deferred effect so it runs AFTER children (<option>s) are in the DOM.
    // The spread's createRenderEffect runs immediately (before children), so select.value
    // can't match any option and silently fails. We also track `local.children` so that
    // when options arrive after the initial render (e.g. via async fetch), the value is
    // re-applied against the now-populated <option> list.
    createEffect(() => {
        local.children;
        if (selectEl && local.value !== undefined) {
            selectEl.value = local.value;
        }
    });
    return <div class="ui-form">
        <select
            ref={(el) => { selectEl = el; if (local.ref) local.ref(el); }}
            class={cls()}
            {...selectProps}
        >{local.children}</select>
    </div>;
}

export function FormSearchableSelect(props: {ref: string} & FormComboboxProps) {
    const [local] = splitProps(props, ["small", "ref", "class", "options", "value", "onchange", "placeholder", "searchPlaceholder", "fieldWidth"]);
    const [searchQuery, setSearchQuery] = createSignal("");
    const [isOpen, setIsOpen] = createSignal(false);
    const [highlightedIndex, setHighlightedIndex] = createSignal(-1);
    const [dropdownPos, setDropdownPos] = createSignal({ top: 0, left: 0, width: 0 });
    let containerRef;
    let triggerRef;
    let searchInputRef;
    let dropdownRef;

    const filteredOptions = () => {
        const q = searchQuery().toLowerCase();
        return q
            ? (local.options || []).filter((opt) => opt.label.toLowerCase().includes(q))
            : (local.options || []);
    };

    const selectedOption = () => (local.options || []).find((opt) => opt.value === local.value);

    const updatePos = () => {
        if (!triggerRef) return;
        const rect = triggerRef.getBoundingClientRect();
        setDropdownPos({ top: rect.bottom + 4, left: rect.left, width: rect.width });
    };

    createEffect(() => {
        if (isOpen()) {
            updatePos();
            const idx = filteredOptions().findIndex((opt) => opt.value === local.value);
            setHighlightedIndex(idx >= 0 ? idx : 0);
            if (searchInputRef) searchInputRef.focus();
        } else {
            setHighlightedIndex(-1);
        }
    });

    createEffect(() => {
        function onMouseDown(e) {
            const inContainer = containerRef?.contains(e.target);
            const inDropdown = dropdownRef?.contains(e.target);
            if (!inContainer && !inDropdown) { setIsOpen(false); setSearchQuery(""); }
        }
        document.addEventListener("mousedown", onMouseDown);
        onCleanup(() => document.removeEventListener("mousedown", onMouseDown));
    });

    const handleSelect = (optionValue) => {
        if (typeof local.onchange === "function") {
            local.onchange(optionValue);
        }
        setIsOpen(false);
        setSearchQuery("");
    };

    const handleKeyDown = (e) => {
        if (e.key === "Escape") { setIsOpen(false); setSearchQuery(""); }
        else if (e.key === "Enter") {
            e.preventDefault();
            const opt = filteredOptions()[highlightedIndex()];
            if (opt && !opt.disabled) handleSelect(opt.value);
        } else if (e.key === "ArrowDown") {
            e.preventDefault();
            setHighlightedIndex((prev) => {
                let next = prev + 1;
                while (next < filteredOptions().length && filteredOptions()[next].disabled) next++;
                return next < filteredOptions().length ? next : prev;
            });
        } else if (e.key === "ArrowUp") {
            e.preventDefault();
            setHighlightedIndex((prev) => {
                let next = prev - 1;
                while (next >= 0 && filteredOptions()[next].disabled) next--;
                return next >= 0 ? next : prev;
            });
        }
    };

    const cls = () => _comboboxRootCls(local.class, local.fieldWidth);
    const dropdownStyle = () => {
        const p = dropdownPos();
        const maxW = Math.min(400, window.innerWidth - p.left - 8);
        return `position:fixed;top:${p.top}px;left:${p.left}px;min-width:${p.width}px;width:max-content;max-width:${maxW}px;z-index:200;`;
    };

    const triggerCls = () => "w-full flex items-center justify-between gap-2 bg-white border border-neutral-300 rounded-default shadow-xs text-sm cursor-pointer text-left focus:outline-2 focus:outline-sky-500 focus:outline-offset-1" + " " + _controlH(local.small) + (local.small ? " p-1" : " p-2");
    const ssBtnCls = (index, optionValue) => {
        const base = "w-full p-2 text-left text-sm border-none cursor-pointer disabled:text-neutral-400 disabled:cursor-not-allowed";
        if (optionValue === local.value) return base + " bg-sky-100 font-medium hover:bg-sky-100";
        if (index() === highlightedIndex()) return base + " bg-sky-50";
        return base + " hover:bg-sky-50";
    };

    return <div ref={(el) => containerRef = el} class={cls()}>
        <button
            ref={(el) => triggerRef = el}
            type="button"
            onclick={(_e) => { updatePos(); setIsOpen(!isOpen()); }}
            class={triggerCls()}
        >
            <span class={"min-w-0 truncate" + (!selectedOption() ? " text-neutral-400" : "")}>
                {selectedOption()?.label || local.placeholder || "Select an option"}
            </span>
            <IconInline icon={isOpen() ? "chevron-up" : "chevron-down"} size={16} />
        </button>
        <Show when={isOpen()}>
            <Portal>
                <div ref={(el) => dropdownRef = el} data-floating-content="true" class="bg-white border border-neutral-300 rounded-default shadow-lg overflow-hidden" style={dropdownStyle()}>
                    <input
                        ref={(el) => searchInputRef = el}
                        type="text"
                        value={searchQuery()}
                        oninput={(e) => setSearchQuery(e.currentTarget.value)}
                        onKeyDown={handleKeyDown}
                        placeholder={local.searchPlaceholder || "Search..."}
                        class="w-full p-2 border-0 border-b border-b-neutral-200 text-sm outline-hidden focus:bg-sky-50"
                    />
                    <div class="max-h-[200px] overflow-y-auto">
                        <Show when={filteredOptions().length === 0}>
                            <div class={DROPDOWN_NO_RESULTS}>No options found</div>
                        </Show>
                        <For each={filteredOptions()}>
                            {(option, index) => <button
                                type="button"
                                disabled={option.disabled}
                                onclick={(_e) => !option.disabled && handleSelect(option.value)}
                                onMouseEnter={(_e) => setHighlightedIndex(index())}
                                class={ssBtnCls(index, option.value)}
                            >{option.label}</button>}
                        </For>
                    </div>
                </div>
            </Portal>
        </Show>
    </div>;
}

export function FormTextarea(props: FormTextareaProps) {
    const cls = () => _textareaCls(props.small, props.error, props.class, props.onDark);
    const inputValue = () => readAccessor(props.value, "");
    return <div class="ui-form">
        <textarea
            ref={(el) => typeof props.ref === "function" && props.ref(el)}
            class={cls()}
            value={inputValue()}
            placeholder={props.placeholder}
            rows={props.rows}
            disabled={props.disabled}
            name={props.name}
            id={props.id}
            oninput={props.oninput}
            onchange={props.onchange}
            onblur={props.onblur}
        />
        <Show when={props.error}>
            <span class={FORM_ERROR}>{props.error}</span>
        </Show>
    </div>;
}

export function FormLabel(props: {inline?: boolean; for?: string; title?: string; class?: string; onDark?: boolean; children?: JSXElement}) {
    return <label
            for={props.for}
            title={props.title}
            class={
                (props.inline ? "inline" : "block") +
                (props.onDark ? " text-text-on-dark" : " text-neutral-700") +
                " text-sm font-medium mb-2" +
                (props.class ? " " + props.class : "")}
            >
        {props.children}
        </label>;
}

export function FormFileInput(props: FormInputProps) {
    const cls = () => INPUT_BASE + " p-1 border-neutral-300 focus:outline-sky-500 cursor-pointer"
        + " file:ml-1 file:mr-2 file:bg-neutral-100 file:border file:border-neutral-300 file:rounded-default file:shadow-xs file:text-sm file:cursor-pointer file:hover:bg-neutral-200"
        + (props.small ? " file:py-[2px] file:px-3" : " file:py-[3px] file:px-4")
        + (props.class ? " " + props.class : "");
    return <div class="ui-form">
        <input
            ref={(el) => typeof props.ref === "function" && props.ref(el)}
            type="file"
            class={cls()}
            disabled={props.disabled}
            name={props.name}
            id={props.id}
            accept={props.accept}
            onchange={props.onchange}
        />
    </div>;
}

export function FormSpacer() {
    return <div class="mb-3" />;
}

export function FormFieldset(props: { legend?: string; class?: string; children: JSXElement }) {
    return <fieldset class={"border border-neutral-300 rounded-default py-3 px-4" + (props.class ? " " + props.class : "")}>
        <legend class="px-2 text-sm font-medium text-neutral-600">{props.legend}</legend>
        {props.children}
    </fieldset>;
}

export const US_STATES: FormSelectOption[] = [
    { value: "", label: "Please select a state", disabled: true },
    { value: "AL", label: "Alabama" },
    { value: "AK", label: "Alaska" },
    { value: "AZ", label: "Arizona" },
    { value: "AR", label: "Arkansas" },
    { value: "CA", label: "California" },
    { value: "CO", label: "Colorado" },
    { value: "CT", label: "Connecticut" },
    { value: "DE", label: "Delaware" },
    { value: "DC", label: "District of Columbia" },
    { value: "FL", label: "Florida" },
    { value: "GA", label: "Georgia" },
    { value: "HI", label: "Hawaii" },
    { value: "ID", label: "Idaho" },
    { value: "IL", label: "Illinois" },
    { value: "IN", label: "Indiana" },
    { value: "IA", label: "Iowa" },
    { value: "KS", label: "Kansas" },
    { value: "KY", label: "Kentucky" },
    { value: "LA", label: "Louisiana" },
    { value: "ME", label: "Maine" },
    { value: "MD", label: "Maryland" },
    { value: "MA", label: "Massachusetts" },
    { value: "MI", label: "Michigan" },
    { value: "MN", label: "Minnesota" },
    { value: "MS", label: "Mississippi" },
    { value: "MO", label: "Missouri" },
    { value: "MT", label: "Montana" },
    { value: "NE", label: "Nebraska" },
    { value: "NV", label: "Nevada" },
    { value: "NH", label: "New Hampshire" },
    { value: "NJ", label: "New Jersey" },
    { value: "NM", label: "New Mexico" },
    { value: "NY", label: "New York" },
    { value: "NC", label: "North Carolina" },
    { value: "ND", label: "North Dakota" },
    { value: "OH", label: "Ohio" },
    { value: "OK", label: "Oklahoma" },
    { value: "OR", label: "Oregon" },
    { value: "PA", label: "Pennsylvania" },
    { value: "PR", label: "Puerto Rico" },
    { value: "RI", label: "Rhode Island" },
    { value: "SC", label: "South Carolina" },
    { value: "SD", label: "South Dakota" },
    { value: "TN", label: "Tennessee" },
    { value: "TX", label: "Texas" },
    { value: "UT", label: "Utah" },
    { value: "VT", label: "Vermont" },
    { value: "VI", label: "Virgin Islands" },
    { value: "VA", label: "Virginia" },
    { value: "WA", label: "Washington" },
    { value: "WV", label: "West Virginia" },
    { value: "WI", label: "Wisconsin" },
    { value: "WY", label: "Wyoming" },
];

const TIMEZONES: FormSelectOption[] = [
    { value: "", label: "Please select a timezone", disabled: true },
    { value: "America/New_York", label: "Eastern" },
    { value: "America/Chicago", label: "Central" },
    { value: "America/Denver", label: "Mountain" },
    { value: "America/Los_Angeles", label: "Pacific" },
    { value: "America/Anchorage", label: "Alaska" },
    { value: "Pacific/Honolulu", label: "Hawaii" },
];

export function FormStateSelector(props: FormComboboxProps) {
    return <FormSelect value={props.value} {...props}>
        <For each={US_STATES}>
            {(state) => <option value={state.value} disabled={state.disabled}>
                {state.label}
            </option>}
        </For>
    </FormSelect>;
}

export function FormTimezoneSelector(props: FormComboboxProps) {
    return <FormSelect value={props.value} {...props}>
        <For each={TIMEZONES}>
            {(tz) => <option value={tz.value} disabled={tz.disabled}>
                {tz.label}
            </option>}
        </For>
    </FormSelect>;
}


export function FormCombobox(
    props: Omit<FormComboboxProps, "onchange" | "onChange"> & {
        onchange?: (value: string) => void;
        onChange?: (value: string) => void;
    },
) {
    const [isOpen, setIsOpen] = createSignal(false);
    const [searchQuery, setSearchQuery] = createSignal("");
    const [internalValue, setInternalValue] = createSignal(props.value);
    const [highlightedIndex, setHighlightedIndex] = createSignal(-1);
    const [dropdownPos, setDropdownPos] = createSignal({top: 0, left: 0, width: 0});
    let containerRef;
    let searchInputRef;
    let triggerRef;
    let optionsRef;
    let dropdownRef;

    const value = () => readAccessor(props.value !== undefined ? props.value : internalValue(), "");
    const selectedOption = () => props.options.find((opt) => opt.value === value());
    const displayValue = () => {
        const label = selectedOption()?.label;
        if (!label) return props.placeholder || "Select an option";
        if (props.maxDisplayLength && label.length > props.maxDisplayLength) {
            return label.slice(0, props.maxDisplayLength).trimEnd() + "…";
        }
        return label;
    };

    const filteredOptions = () => {
        if (props.searchable && searchQuery()) {
            return props.options.filter((opt) => opt.label.toLowerCase().includes(searchQuery().toLowerCase()));
        }
        return props.options;
    };

    const updateDropdownPos = () => {
        if (!triggerRef) return;
        const rect = triggerRef.getBoundingClientRect();
        setDropdownPos({top: rect.bottom + 4, left: rect.left, width: rect.width});
    };

    createEffect(() => {
        if (isOpen()) {
            updateDropdownPos();
            const selectedIndex = filteredOptions().findIndex((opt) => opt.value === value());
            setHighlightedIndex(selectedIndex >= 0 ? selectedIndex : 0);

            let rafId;
            const trackPosition = () => {
                updateDropdownPos();
                rafId = requestAnimationFrame(trackPosition);
            };
            rafId = requestAnimationFrame(trackPosition);
            onCleanup(() => cancelAnimationFrame(rafId));
        } else {
            setHighlightedIndex(-1);
        }
    });

    createEffect(() => {
        function handleClickOutside(event) {
            const inContainer = containerRef?.contains(event.target);
            const inDropdown = dropdownRef?.contains(event.target);
            if (!inContainer && !inDropdown) {
                setIsOpen(false);
                setSearchQuery("");
            }
        }
        document.addEventListener("mousedown", handleClickOutside);
        onCleanup(() => document.removeEventListener("mousedown", handleClickOutside));
    });

    createEffect(() => {
        if (isOpen() && props.searchable && searchInputRef) {
            searchInputRef.focus();
        }
    });

    createEffect(() => {
        if (isOpen() && highlightedIndex() >= 0 && optionsRef) {
            const optionElements = optionsRef.querySelectorAll("button");
            const highlightedElement = optionElements[highlightedIndex()];
            if (highlightedElement) {
                highlightedElement.scrollIntoView({block: "nearest"});
            }
        }
    });

    const handleSelect = (optionValue) => {
        setInternalValue(optionValue);
        props.onchange?.(optionValue);
        setIsOpen(false);
        setSearchQuery("");
    };

    const handleKeyDown = (e) => {
        if (e.key === "Escape") {
            setIsOpen(false);
            setSearchQuery("");
            triggerRef?.focus();
        } else if (e.key === "Enter") {
            e.preventDefault();
            if (!isOpen()) {
                setIsOpen(true);
            } else if (highlightedIndex() >= 0 && highlightedIndex() < filteredOptions().length) {
                const option = filteredOptions()[highlightedIndex()];
                if (!option.disabled) {
                    handleSelect(option.value);
                    triggerRef?.focus();
                }
            }
        } else if (e.key === "ArrowDown") {
            e.preventDefault();
            if (!isOpen()) {
                setIsOpen(true);
            } else {
                setHighlightedIndex((prev) => {
                    let next = prev + 1;
                    while (next < filteredOptions().length && filteredOptions()[next].disabled) {
                        next++;
                    }
                    return next < filteredOptions().length ? next : prev;
                });
            }
        } else if (e.key === "ArrowUp") {
            e.preventDefault();
            if (isOpen()) {
                setHighlightedIndex((prev) => {
                    let next = prev - 1;
                    while (next >= 0 && filteredOptions()[next].disabled) {
                        next--;
                    }
                    return next >= 0 ? next : prev;
                });
            }
        }
    };

    const triggerCls = () =>
        (props.onDark ? TRIGGER_BASE_DARK : TRIGGER_BASE) +
        " " +
        _controlH(props.small) +
        (props.small ? " p-1" : " p-2");
    const placeholderCls = () =>
        !selectedOption() ? (props.onDark ? "text-text-on-dark-muted" : "text-neutral-500") : "";
    const chevronCls = () => (props.onDark ? "text-text-on-dark-muted" : "text-neutral-400");
    const dropdownCls = () => (props.onDark ? DROPDOWN_DARK : DROPDOWN);
    const searchWrapCls = () => (props.onDark ? DROPDOWN_SEARCH_WRAP_DARK : DROPDOWN_SEARCH_WRAP);
    const searchInputCls = () => (props.onDark ? DROPDOWN_SEARCH_INPUT_DARK : DROPDOWN_SEARCH_INPUT);
    const noResultsCls = () => (props.onDark ? DROPDOWN_NO_RESULTS_DARK : DROPDOWN_NO_RESULTS);
    const optionCls = (index) =>
        (props.onDark ? DROPDOWN_OPTION_DARK : DROPDOWN_OPTION) +
        (index() === highlightedIndex()
            ? " " + (props.onDark ? DROPDOWN_OPTION_HIGHLIGHT_DARK : DROPDOWN_OPTION_HIGHLIGHT)
            : "") +
        (props.small ? " py-1.5 px-2" : "");

    const dropdownStyle = () => {
        const pos = dropdownPos();
        const maxW = Math.min(400, window.innerWidth - pos.left - 8);
        return `position:fixed;top:${pos.top}px;left:${pos.left}px;min-width:${pos.width}px;width:max-content;max-width:${maxW}px;z-index:200;`;
    };

    return (
        <div ref={(el) => (containerRef = el)} class={_comboboxRootCls(props.class, props.fieldWidth)}>
            <button
                ref={(el) => (triggerRef = el)}
                type="button"
                disabled={props.disabled}
                onclick={() => !props.disabled && setIsOpen(!isOpen())}
                onkeydown={handleKeyDown}
                class={triggerCls()}
            >
                <span class={"min-w-0 truncate " + placeholderCls()}>{displayValue()}</span>
                <IconInline icon={isOpen() ? "chevron-up" : "chevron-down"} size={16} class={chevronCls()} />
            </button>

            <Show when={isOpen()}>
                <Portal>
                    <div
                        ref={(el) => (dropdownRef = el)}
                        data-floating-content="true"
                        class={dropdownCls()}
                        style={dropdownStyle()}
                    >
                        <Show when={props.searchable}>
                            <div class={searchWrapCls()}>
                                <input
                                    ref={(el) => (searchInputRef = el)}
                                    type="text"
                                    class={searchInputCls()}
                                    value={searchQuery()}
                                    oninput={(e) => setSearchQuery(e.currentTarget.value)}
                                    onkeydown={handleKeyDown}
                                    placeholder={props.searchPlaceholder || "Search..."}
                                    onclick={(e) => e.stopPropagation()}
                                />
                            </div>
                        </Show>

                        <div ref={(el) => (optionsRef = el)}>
                            <Show when={filteredOptions().length === 0}>
                                <div class={noResultsCls()}>No options found</div>
                            </Show>
                            <For each={filteredOptions()}>
                                {(option, index) => (
                                    <button
                                        type="button"
                                        disabled={option.disabled}
                                        onclick={() => !option.disabled && handleSelect(option.value)}
                                        onMouseEnter={() => setHighlightedIndex(index())}
                                        class={optionCls(index)}
                                    >
                                        {props.renderOption ? props.renderOption(option) : option.label}
                                    </button>
                                )}
                            </For>
                        </div>
                    </div>
                </Portal>
            </Show>
        </div>
    );
}

export function FormAsyncCombobox(
    props: FormComboboxProps & {
        minChars?: number;
        debounce?: number;
        onSearch: (q: string) => Promise<FormSelectOption[]> | FormSelectOption[];
        onSelect?: (value: string, option: FormSelectOption) => void;
        loadOptions?: (q: string) => Promise<FormSelectOption[]>;
    },
) {
    const [query, setQuery] = createSignal("");
    const [results, setResults] = createSignal([]);
    const [open, setOpen] = createSignal(false);
    const [highlightedIndex, setHighlightedIndex] = createSignal(-1);
    const [loading, setLoading] = createSignal(false);
    const [dropdownPos, setDropdownPos] = createSignal({top: 0, left: 0, width: 0});
    let debounceTimer;
    let inputRef;
    let containerRef;
    let dropdownRef;
    let optionsRef;

    const minChars = () => props.minChars ?? 2;
    const debounceMs = () => props.debounce ?? 200;

    const doSearch = (q) => {
        if (q.length < minChars()) {
            setResults([]);
            setOpen(false);
            return;
        }
        setLoading(true);
        Promise.resolve(props.onSearch(q))
            .then((items) => {
                setResults(items || []);
                setOpen(true);
                setHighlightedIndex(-1);
            })
            .catch(() => {
                setResults([]);
                setOpen(false);
            })
            .finally(() => setLoading(false));
    };

    const handleInput = (e) => {
        const val = e.target.value;
        setQuery(val);
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(() => doSearch(val.trim()), debounceMs());
    };

    const handleSelect = (option) => {
        setQuery("");
        setResults([]);
        setOpen(false);
        props.onSelect?.(option.value, option);
    };

    const updateDropdownPos = () => {
        if (!inputRef) return;
        const rect = inputRef.getBoundingClientRect();
        setDropdownPos({top: rect.bottom + 4, left: rect.left, width: rect.width});
    };

    createEffect(() => {
        if (open()) {
            updateDropdownPos();
            let rafId;
            const trackPosition = () => {
                updateDropdownPos();
                rafId = requestAnimationFrame(trackPosition);
            };
            rafId = requestAnimationFrame(trackPosition);
            onCleanup(() => cancelAnimationFrame(rafId));
        }
    });

    createEffect(() => {
        if (open() && highlightedIndex() >= 0 && optionsRef) {
            const els = optionsRef.querySelectorAll("button");
            els[highlightedIndex()]?.scrollIntoView({block: "nearest"});
        }
    });

    createEffect(() => {
        function handleClickOutside(event) {
            const inContainer = containerRef?.contains(event.target);
            const inDropdown = dropdownRef?.contains(event.target);
            if (!inContainer && !inDropdown) {
                setOpen(false);
            }
        }
        document.addEventListener("mousedown", handleClickOutside);
        onCleanup(() => document.removeEventListener("mousedown", handleClickOutside));
    });

    onCleanup(() => clearTimeout(debounceTimer));

    const handleKeyDown = (e) => {
        const list = results();
        if (e.key === "ArrowDown") {
            e.preventDefault();
            if (!open()) return;
            setHighlightedIndex((i) => (i < list.length - 1 ? i + 1 : 0));
        } else if (e.key === "ArrowUp") {
            e.preventDefault();
            if (!open()) return;
            setHighlightedIndex((i) => (i > 0 ? i - 1 : list.length - 1));
        } else if (e.key === "Enter" && open() && list.length > 0) {
            e.preventDefault();
            const idx = highlightedIndex() >= 0 ? highlightedIndex() : 0;
            handleSelect(list[idx]);
        } else if (e.key === "Escape") {
            setOpen(false);
        }
    };

    const dropdownStyle = () => {
        const pos = dropdownPos();
        const maxW = Math.min(400, window.innerWidth - pos.left - 8);
        return `position:fixed;top:${pos.top}px;left:${pos.left}px;min-width:${pos.width}px;width:max-content;max-width:${maxW}px;z-index:200;`;
    };

    return (
        <div ref={(el) => (containerRef = el)} class={_comboboxRootCls(props.class, props.fieldWidth)}>
            <input
                ref={(el) => (inputRef = el)}
                type="text"
                value={query()}
                oninput={handleInput}
                onFocus={() => {
                    if (results().length > 0) setOpen(true);
                }}
                onKeyDown={handleKeyDown}
                placeholder={props.placeholder || "Search..."}
                class={_inputCls(props.small, false, false)}
            />
            <Show when={open()}>
                <Portal>
                    <div
                        ref={(el) => (dropdownRef = el)}
                        data-floating-content="true"
                        class={DROPDOWN}
                        style={dropdownStyle()}
                    >
                        <div ref={(el) => (optionsRef = el)}>
                            <Show when={results().length === 0}>
                                <div class={DROPDOWN_NO_RESULTS}>{loading() ? "Searching..." : "No results"}</div>
                            </Show>
                            <For each={results()}>
                                {(option, index) => (
                                    <button
                                        type="button"
                                        onclick={() => handleSelect(option)}
                                        onMouseEnter={() => setHighlightedIndex(index())}
                                        class={
                                            DROPDOWN_OPTION + (index() === highlightedIndex() ? " bg-neutral-100" : "")
                                        }
                                    >
                                        {props.renderOption ? props.renderOption(option) : option.label}
                                    </button>
                                )}
                            </For>
                        </div>
                    </div>
                </Portal>
            </Show>
        </div>
    );
}

export function FormMultiSelect(props: FormMultiSelectProps) {
    const [isOpen, setIsOpen] = createSignal(false);
    const [searchQuery, setSearchQuery] = createSignal("");
    const [internalValue, setInternalValue] = createSignal(props.value ?? []);
    const [highlightedIndex, setHighlightedIndex] = createSignal(-1);
    const [isOverflowing, setIsOverflowing] = createSignal(false);
    const [dropdownPos, setDropdownPos] = createSignal({ top: 0, left: 0, width: 0 });
    let containerRef;
    let searchInputRef;
    let triggerRef;
    let optionsRef;
    let tagsContainerRef;
    let dropdownRef;

    const maxTags = () => props.maxTagsBeforeCollapse ?? 3;
    const value = () => readAccessor(props.value !== undefined ? props.value : internalValue(), []);
    const selectedOptions = () => props.options.filter((opt) => value().includes(opt.value));

    const updateDropdownPos = () => {
        setDropdownPos(_floatingDropdownPos(triggerRef));
    };

    const filteredOptions = () => {
        if (props.searchable && searchQuery()) {
            return props.options.filter((opt) =>
                opt.label.toLowerCase().includes(searchQuery().toLowerCase())
            );
        }
        return props.options;
    };

    createEffect(() => {
        if (tagsContainerRef) {
            setIsOverflowing(tagsContainerRef.scrollWidth > tagsContainerRef.clientWidth || selectedOptions().length > maxTags());
        }
    });

    createEffect(() => {
        if (isOpen()) { setHighlightedIndex(0); } else { setHighlightedIndex(-1); }
    });

    createEffect(() => {
        if (isOpen()) {
            updateDropdownPos();
            let rafId;
            const trackPosition = () => {
                updateDropdownPos();
                rafId = requestAnimationFrame(trackPosition);
            };
            rafId = requestAnimationFrame(trackPosition);
            onCleanup(() => cancelAnimationFrame(rafId));
        }
    });

    createEffect(() => {
        function handleClickOutside(event) {
            const inContainer = containerRef?.contains(event.target);
            const inDropdown = dropdownRef?.contains(event.target);
            if (!inContainer && !inDropdown) {
                setIsOpen(false);
                setSearchQuery("");
            }
        }
        document.addEventListener("mousedown", handleClickOutside);
        onCleanup(() => document.removeEventListener("mousedown", handleClickOutside));
    });

    createEffect(() => { if (isOpen() && props.searchable && searchInputRef) { searchInputRef.focus(); } });

    createEffect(() => {
        if (isOpen() && highlightedIndex() >= 0 && optionsRef) {
            const els = optionsRef.querySelectorAll("button");
            if (els[highlightedIndex()]) els[highlightedIndex()].scrollIntoView({ block: "nearest" });
        }
    });

    const toggleOption = (optionValue) => {
        const newValue = value().includes(optionValue)
            ? value().filter((v) => v !== optionValue)
            : [...value(), optionValue];
        setInternalValue(newValue);
        props.onchange?.(newValue);
    };

    const removeOption = (optionValue, e) => {
        e.stopPropagation();
        const newValue = value().filter((v) => v !== optionValue);
        setInternalValue(newValue);
        props.onchange?.(newValue);
    };

    const selectAll = () => {
        const selectableValues = filteredOptions().filter((opt) => !opt.disabled).map((opt) => opt.value);
        const newValue = [...new Set([...value(), ...selectableValues])];
        setInternalValue(newValue);
        props.onchange?.(newValue);
    };

    const deselectAll = () => {
        const filteredValues = new Set(filteredOptions().map((opt) => opt.value));
        const newValue = value().filter((v) => !filteredValues.has(v));
        setInternalValue(newValue);
        props.onchange?.(newValue);
    };

    const allFilteredSelected = () => filteredOptions().filter((opt) => !opt.disabled).every((opt) => value().includes(opt.value));

    const handleKeyDown = (e) => {
        if (e.key === "Escape") { setIsOpen(false); setSearchQuery(""); triggerRef?.focus(); }
        else if (e.key === "Enter") {
            e.preventDefault();
            if (!isOpen()) { setIsOpen(true); }
            else if (highlightedIndex() >= 0 && highlightedIndex() < filteredOptions().length) {
                const option = filteredOptions()[highlightedIndex()];
                if (!option.disabled) toggleOption(option.value);
            }
        } else if (e.key === "ArrowDown") {
            e.preventDefault();
            if (!isOpen()) { setIsOpen(true); }
            else {
                setHighlightedIndex((prev) => {
                    let next = prev + 1;
                    while (next < filteredOptions().length && filteredOptions()[next].disabled) next++;
                    return next < filteredOptions().length ? next : prev;
                });
            }
        } else if (e.key === "ArrowUp") {
            e.preventDefault();
            if (isOpen()) {
                setHighlightedIndex((prev) => {
                    let next = prev - 1;
                    while (next >= 0 && filteredOptions()[next].disabled) next--;
                    return next >= 0 ? next : prev;
                });
            }
        } else if (e.key === " " && isOpen() && !props.searchable) {
            e.preventDefault();
            if (highlightedIndex() >= 0 && highlightedIndex() < filteredOptions().length) {
                const option = filteredOptions()[highlightedIndex()];
                if (!option.disabled) toggleOption(option.value);
            }
        }
    };

    const triggerCls = () => TRIGGER_BASE + " overflow-hidden " + _controlH(props.small) + (props.small ? " p-1" : " p-2");
    const optionCls = (index) => DROPDOWN_OPTION + (index() === highlightedIndex() ? " bg-neutral-100" : "");
    const dropdownStyle = () => _floatingDropdownStyle(dropdownPos());

    const renderTriggerContent = () => {
        if (selectedOptions().length === 0) {
            return <span class="text-neutral-500">{props.placeholder || "Select options"}</span>;
        }
        if (isOverflowing() || selectedOptions().length > maxTags()) {
            return <span>{selectedOptions().length + " item" + (selectedOptions().length !== 1 ? "s" : "") + " selected"}</span>;
        }
        return <div ref={(el) => tagsContainerRef = el} class="flex flex-nowrap gap-1 overflow-hidden items-center">
            <For each={selectedOptions()}>
                {(option) => <span class={"inline-flex items-center gap-0.5 bg-neutral-200 rounded-default whitespace-nowrap leading-none" + (props.small ? " py-0.5 px-1.5 text-xs" : " py-0.5 px-2 text-sm")}>
                    {option.label}
                    <button type="button" class="bg-transparent border-none p-0 cursor-pointer flex items-center hover:text-neutral-900" onclick={(e) => removeOption(option.value, e)}>
                        <IconInline icon="xmark" size={10} />
                    </button>
                </span>}
            </For>
        </div>;
    };

    return <div ref={(el) => containerRef = el} class={_comboboxRootCls(props.class, props.fieldWidth)}>
        <button
            ref={(el) => triggerRef = el}
            type="button"
            disabled={props.disabled}
            onclick={() => { if (!props.disabled) { updateDropdownPos(); setIsOpen(!isOpen()); } }}
            onKeyDown={handleKeyDown}
            class={triggerCls()}
        >
            <div class="flex-1 overflow-hidden flex items-center min-w-0">
                {renderTriggerContent()}
            </div>
            <IconInline icon={isOpen() ? "chevron-up" : "chevron-down"} size={16} class="text-neutral-400" />
        </button>

        <Show when={isOpen()}>
            <Portal>
                <div ref={(el) => dropdownRef = el} data-floating-content="true" class="bg-white border border-neutral-300 rounded-default shadow-lg max-h-60 overflow-auto" style={dropdownStyle()}>
                    <Show when={props.searchable}>
                        <div class={DROPDOWN_SEARCH_WRAP}>
                            <input
                                ref={(el) => searchInputRef = el}
                                type="text"
                                class={DROPDOWN_SEARCH_INPUT}
                                value={searchQuery()}
                                oninput={(e) => setSearchQuery(e.currentTarget.value)}
                                onKeyDown={handleKeyDown}
                                placeholder={props.searchPlaceholder || "Search..."}
                                onclick={(e) => e.stopPropagation()}
                            />
                        </div>
                    </Show>
                    <Show when={props.showSelectAll && filteredOptions().length > 0}>
                        <div class={SELECT_ALL_WRAP}>
                            <button type="button" class={SELECT_ALL_BTN} onclick={() => allFilteredSelected() ? deselectAll() : selectAll()}>
                                {allFilteredSelected() ? "Deselect All" : "Select All"}
                            </button>
                        </div>
                    </Show>
                    <div ref={(el) => optionsRef = el}>
                        <Show when={filteredOptions().length === 0}>
                            <div class={DROPDOWN_NO_RESULTS}>No options found</div>
                        </Show>
                        <For each={filteredOptions()}>
                            {(option, index) => {
                                const isSelected = () => value().includes(option.value);
                                return <button
                                    type="button"
                                    disabled={option.disabled}
                                    onclick={() => !option.disabled && toggleOption(option.value)}
                                    onMouseEnter={() => setHighlightedIndex(index())}
                                    class={optionCls(index)}
                                >
                                    <input type="checkbox" checked={isSelected()} onchange={() => {}} style="pointer-events:none" />
                                    {option.label}
                                </button>;
                            }}
                        </For>
                    </div>
                </div>
            </Portal>
        </Show>
    </div>;
}

export function FormMultiSelectTrigger(props: FormMultiSelectTriggerProps) {
    const [isOpen, setIsOpen] = createSignal(false);
    const [searchQuery, setSearchQuery] = createSignal("");
    const [internalValue, setInternalValue] = createSignal(props.value ?? []);
    const [highlightedIndex, setHighlightedIndex] = createSignal(-1);
    let containerRef, searchInputRef, triggerRef, optionsRef;

    const value = () => readAccessor(props.value !== undefined ? props.value : internalValue(), []);
    const minWidth = () => props.minWidth ?? 200;

    const filteredOptions = () => {
        const opts = readAccessor(props.options, []);
        if (props.searchable && searchQuery()) {
            return opts.filter((opt) => opt.label.toLowerCase().includes(searchQuery().toLowerCase()));
        }
        return opts;
    };

    createEffect(() => { if (isOpen()) { setHighlightedIndex(0); } else { setHighlightedIndex(-1); } });

    createEffect(() => {
        function handleClickOutside(event) {
            if (containerRef && !containerRef.contains(event.target)) { setIsOpen(false); setSearchQuery(""); }
        }
        document.addEventListener("mousedown", handleClickOutside);
        onCleanup(() => document.removeEventListener("mousedown", handleClickOutside));
    });

    createEffect(() => { if (isOpen() && props.searchable && searchInputRef) searchInputRef.focus(); });

    createEffect(() => {
        if (isOpen() && highlightedIndex() >= 0 && optionsRef) {
            const els = optionsRef.querySelectorAll("button");
            if (els[highlightedIndex()]) els[highlightedIndex()].scrollIntoView({ block: "nearest" });
        }
    });

    const toggleOption = (optionValue) => {
        const newValue = value().includes(optionValue) ? value().filter((v) => v !== optionValue) : [...value(), optionValue];
        setInternalValue(newValue);
        props.onchange?.(newValue);
    };

    const selectAll = () => {
        const vals = filteredOptions().filter((opt) => !opt.disabled).map((opt) => opt.value);
        const newValue = [...new Set([...value(), ...vals])];
        setInternalValue(newValue);
        props.onchange?.(newValue);
    };

    const deselectAll = () => {
        const filteredValues = new Set(filteredOptions().map((opt) => opt.value));
        const newValue = value().filter((v) => !filteredValues.has(v));
        setInternalValue(newValue);
        props.onchange?.(newValue);
    };

    const allFilteredSelected = () => filteredOptions().filter((opt) => !opt.disabled).every((opt) => value().includes(opt.value));

    const handleKeyDown = (e) => {
        if (e.key === "Escape") { setIsOpen(false); setSearchQuery(""); triggerRef?.focus(); }
        else if (e.key === "Enter") {
            e.preventDefault();
            if (!isOpen()) { setIsOpen(true); }
            else if (highlightedIndex() >= 0 && highlightedIndex() < filteredOptions().length) {
                const opt = filteredOptions()[highlightedIndex()];
                if (!opt.disabled) toggleOption(opt.value);
            }
        } else if (e.key === "ArrowDown") {
            e.preventDefault();
            if (!isOpen()) { setIsOpen(true); }
            else { setHighlightedIndex((prev) => { let n = prev + 1; while (n < filteredOptions().length && filteredOptions()[n].disabled) n++; return n < filteredOptions().length ? n : prev; }); }
        } else if (e.key === "ArrowUp") {
            e.preventDefault();
            if (isOpen()) { setHighlightedIndex((prev) => { let n = prev - 1; while (n >= 0 && filteredOptions()[n].disabled) n--; return n >= 0 ? n : prev; }); }
        } else if (e.key === " " && isOpen() && !props.searchable) {
            e.preventDefault();
            if (highlightedIndex() >= 0 && highlightedIndex() < filteredOptions().length) {
                const opt = filteredOptions()[highlightedIndex()];
                if (!opt.disabled) toggleOption(opt.value);
            }
        }
    };

    const dropdownCls = () => "absolute z-50 mt-1 bg-white border border-neutral-300 rounded-default shadow-lg max-h-60 overflow-auto " + (props.align === "right" ? "right-0" : "left-0");
    // Rounded, inset items (within the p-1.5 list below) to match the Menu component.
    const optionCls = (index) => DROPDOWN_OPTION + " rounded-default" + (index() === highlightedIndex() ? " bg-neutral-100" : "") + (props.small ? " py-1.5 px-2" : "");

    return <div ref={(el) => containerRef = el} class={"relative inline-block " + (props.class || "")}>
        <button
            ref={(el) => triggerRef = el}
            type="button"
            class="cursor-pointer bg-transparent border-none disabled:opacity-50 disabled:cursor-not-allowed"
            disabled={props.disabled}
            onclick={() => !props.disabled && setIsOpen(!isOpen())}
            onKeyDown={handleKeyDown}
        >
            {props.trigger}
        </button>

        <Show when={isOpen()}>
            <div class={dropdownCls()} style={{ "min-width": `${minWidth()}px` }}>
                <Show when={props.searchable}>
                    <div class={DROPDOWN_SEARCH_WRAP}>
                        <input
                            ref={(el) => searchInputRef = el}
                            type="text"
                            class={DROPDOWN_SEARCH_INPUT}
                            value={searchQuery()}
                            oninput={(e) => setSearchQuery(e.currentTarget.value)}
                            onKeyDown={handleKeyDown}
                            placeholder={props.searchPlaceholder || "Search..."}
                            onclick={(e) => e.stopPropagation()}
                        />
                    </div>
                </Show>
                <Show when={props.showSelectAll && filteredOptions().length > 0}>
                    <div class={SELECT_ALL_WRAP}>
                        <button type="button" class={SELECT_ALL_BTN} onclick={() => allFilteredSelected() ? deselectAll() : selectAll()}>
                            {allFilteredSelected() ? "Deselect All" : "Select All"}
                        </button>
                    </div>
                </Show>
                <div ref={(el) => optionsRef = el} class="p-1.5">
                    <Show when={filteredOptions().length === 0}>
                        <div class={DROPDOWN_NO_RESULTS}>No options found</div>
                    </Show>
                    <For each={filteredOptions()}>
                        {(option, index) => {
                            const isSelected = () => value().includes(option.value);
                            return <button
                                type="button"
                                disabled={option.disabled}
                                onclick={() => !option.disabled && toggleOption(option.value)}
                                onMouseEnter={() => setHighlightedIndex(index())}
                                class={optionCls(index)}
                            >
                                <input type="checkbox" checked={isSelected()} onchange={() => {}} style="pointer-events:none" />
                                {option.label}
                            </button>;
                        }}
                    </For>
                </div>
            </div>
        </Show>
    </div>;
}
