component InputNumber {
    outputs {
      input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
      change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
      increment(payload: { value: number; previousValue?: number; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
      decrement(payload: { value: number; previousValue?: number; sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
    }

    props {
        size: string = "default"
        color: string = "primary"
        variant: string = "default"
        class: string = ""

        id: string = ""
        name: string = "quantity"
        value: number = 0
        min: string = ""
        max: string = ""
        step: number = 1
        precision: string = "auto"

        label: string = ""
        description: string = ""
        helpText: string = ""
        error: string = ""
        invalid: boolean = false

        prefix: string = ""
        suffix: string = ""
        placeholder: string = ""
        autocomplete: string = "off"
        inputMode: string = "decimal"
        ariaLabel: string = ""

        required: boolean = false
        disabled: boolean = false
        inputDisabled: boolean = false
        buttonsDisabled: boolean = false
        readonly: boolean = false
        allowInput: boolean = true
        keyboard: boolean = true
        wheel: boolean = false
        clamp: boolean = true
        fullWidth: boolean = false
        showButtons: boolean = true
        showValidationMessage: boolean = true

        decrementLabel: string = "Decrease value"
        incrementLabel: string = "Increase value"
        controlsLabel: string = "Quantity controls"
        requiredMessage: string = "A value is required."
        minMessage: string = "Value is below the minimum."
        maxMessage: string = "Value is above the maximum."

    }

    state currentValue = value
    state committedValue = value

    functions {
        shared function inputId() {
            if (id !== "") {
                return id
            }

            if (name !== "") {
                return name
            }

            return "input-number"
        }

        shared function descriptionId() {
            return inputId() + "-description"
        }

        shared function messageId() {
            return inputId() + "-message"
        }

        shared function componentColor() {
            if (color === "secondary") {
                return "var(--wire-color-secondary)"
            }

            if (color === "success") {
                return "var(--wire-color-success)"
            }

            if (color === "warning") {
                return "var(--wire-color-warning)"
            }

            if (color === "danger") {
                return "var(--wire-color-danger)"
            }

            if (color === "info") {
                return "var(--wire-color-info)"
            }

            return "var(--wire-color-primary)"
        }

        shared function hasMin() {
            return min !== "" && min !== null && min !== undefined
        }

        shared function hasMax() {
            return max !== "" && max !== null && max !== undefined
        }

        shared function isBlank() {
            return currentValue === "" || currentValue === null || currentValue === undefined
        }

        shared function normalizedStep() {
            return Number(step) > 0 ? Number(step) : 1
        }

        shared function inferredPrecision() {
            if (precision !== "auto" && precision !== "") {
                return Math.max(0, Number(precision) || 0)
            }

            if (String(normalizedStep()).includes(".")) {
                return String(normalizedStep()).split(".")[1].length
            }

            return 0
        }

        shared function precisionFactor() {
            return Math.pow(10, inferredPrecision())
        }

        shared function roundValue(nextValue) {
            return Math.round(Number(nextValue) * precisionFactor()) / precisionFactor()
        }

        shared function clampValue(nextValue) {
            nextValue = Number(nextValue)

            if (hasMin() && nextValue < Number(min)) {
                nextValue = Number(min)
            }

            if (hasMax() && nextValue > Number(max)) {
                nextValue = Number(max)
            }

            return roundValue(nextValue)
        }

        shared function isBelowMin() {
            return !isBlank() && hasMin() && Number(currentValue) < Number(min)
        }

        shared function isAboveMax() {
            return !isBlank() && hasMax() && Number(currentValue) > Number(max)
        }

        shared function isInvalid() {
            return (
                invalid ||
                error !== "" ||
                (required && isBlank()) ||
                isBelowMin() ||
                isAboveMax()
            )
        }

        shared function validationMessage() {
            if (error !== "") {
                return error
            }

            if (required && isBlank()) {
                return requiredMessage
            }

            if (isBelowMin()) {
                return minMessage
            }

            if (isAboveMax()) {
                return maxMessage
            }

            return ""
        }

        shared function hasMessage() {
            return (
                helpText !== "" ||
                (showValidationMessage && isInvalid() && validationMessage() !== "")
            )
        }

        shared function describedBy() {
            if (description !== "" && hasMessage()) {
                return descriptionId() + " " + messageId()
            }

            if (description !== "") {
                return descriptionId()
            }

            if (hasMessage()) {
                return messageId()
            }

            return ""
        }

        shared function decrementDisabled() {
            return (
                disabled ||
                readonly ||
                buttonsDisabled ||
                (hasMin() && !isBlank() && Number(currentValue) <= Number(min))
            )
        }

        shared function incrementDisabled() {
            return (
                disabled ||
                readonly ||
                buttonsDisabled ||
                (hasMax() && !isBlank() && Number(currentValue) >= Number(max))
            )
        }

        // Outputs are resolved by name, so each one is written out. Raw
        // CustomEvents dispatched on the root -- what this did before -- never
        // reach a parent @binding.
        client function dispatchInputNumberEvent(
            sourceEvent,
            eventName,
            action,
            previousValue,
            payload
        ) {
            payload = {
                component: "InputNumber",
                name: name,
                value: currentValue,
                previousValue: previousValue,
                action: action,
                min: hasMin() ? Number(min) : null,
                max: hasMax() ? Number(max) : null,
                step: normalizedStep(),
                valid: !isInvalid()
            }

            if (eventName === "input") {
                output.input(payload)
            } else if (eventName === "change") {
                output.change(payload)
            } else if (eventName === "increment") {
                output.increment(payload)
            } else if (eventName === "decrement") {
                output.decrement(payload)
            }
        }

        client function applyControlValue(nextValue, action, sourceEvent, previousValue) {
            previousValue = currentValue
            currentValue = clampValue(nextValue)
            committedValue = currentValue

            dispatchInputNumberEvent(
                sourceEvent,
                "input",
                action,
                previousValue
            )
            dispatchInputNumberEvent(
                sourceEvent,
                "change",
                action,
                previousValue
            )
            dispatchInputNumberEvent(
                sourceEvent,
                action,
                action,
                previousValue
            )
        }

        client function incrementValue(sourceEvent, nextValue) {
            if (incrementDisabled()) {
                return
            }

            if (isBlank()) {
                nextValue = hasMin() ? Number(min) : normalizedStep()
            } else {
                nextValue =
                    Number(currentValue) +
                    normalizedStep() * (sourceEvent.shiftKey ? 10 : 1)
            }

            applyControlValue(nextValue, "increment", sourceEvent)
        }

        client function decrementValue(sourceEvent, nextValue) {
            if (decrementDisabled()) {
                return
            }

            if (isBlank()) {
                nextValue = hasMax() ? Number(max) : -normalizedStep()
            } else {
                nextValue =
                    Number(currentValue) -
                    normalizedStep() * (sourceEvent.shiftKey ? 10 : 1)
            }

            applyControlValue(nextValue, "decrement", sourceEvent)
        }

        client function handleInput(sourceEvent, previousValue, nextValue) {
            sourceEvent.stopPropagation()
            previousValue = currentValue
            nextValue = sourceEvent.target.value

            if (nextValue === "") {
                currentValue = ""
            } else if (!Number.isNaN(Number(nextValue))) {
                currentValue = roundValue(Number(nextValue))
            }

            dispatchInputNumberEvent(
                sourceEvent,
                "input",
                "input",
                previousValue
            )
        }

        client function handleChange(sourceEvent, previousValue) {
            sourceEvent.stopPropagation()
            previousValue = committedValue

            if (!isBlank()) {
                currentValue = clamp
                    ? clampValue(currentValue)
                    : roundValue(currentValue)
            }

            committedValue = currentValue

            dispatchInputNumberEvent(
                sourceEvent,
                "change",
                "change",
                previousValue
            )
        }

        client function handleKeydown(sourceEvent) {
            if (
                !keyboard ||
                disabled ||
                readonly ||
                inputDisabled ||
                !allowInput
            ) {
                return
            }

            if (sourceEvent.key === "ArrowUp") {
                sourceEvent.preventDefault()
                incrementValue(sourceEvent)
            } else if (sourceEvent.key === "ArrowDown") {
                sourceEvent.preventDefault()
                decrementValue(sourceEvent)
            } else if (sourceEvent.key === "Home" && hasMin()) {
                sourceEvent.preventDefault()
                applyControlValue(Number(min), "decrement", sourceEvent)
            } else if (sourceEvent.key === "End" && hasMax()) {
                sourceEvent.preventDefault()
                applyControlValue(Number(max), "increment", sourceEvent)
            }
        }

        client function handleWheel(sourceEvent) {
            if (
                !wheel ||
                disabled ||
                readonly ||
                inputDisabled ||
                !allowInput
            ) {
                return
            }

            sourceEvent.preventDefault()

            if (sourceEvent.deltaY < 0) {
                incrementValue(sourceEvent)
            } else if (sourceEvent.deltaY > 0) {
                decrementValue(sourceEvent)
            }
        }
    }

    view {
        <div
            {...attrs}
            data-wrn-input-number
            data-variant='{variant}'
            data-size='{size}'
            data-color='{color}'
            data-value='{currentValue}'
            data-invalid='{isInvalid() ? "true" : "false"}'
            data-disabled='{disabled ? "true" : "false"}'
            data-full-width='{fullWidth ? "true" : "false"}'
            style='--input-number-accent: {componentColor()};'
            class='wire-input-number {class}'
        >
            {#if label !== "" && variant !== "labeled" && variant !== "seat"}
            <label
                for='{inputId()}'
                class='wire-input-number__label'
            >
                <span>{label}</span>
                {#if required}
                <span
                    aria-hidden="true"
                    class='wire-input-number__required'
                >*</span>
                {/if}
            </label>
            {/if}

            {#if description !== "" && variant !== "labeled" && variant !== "seat"}
            <p
                id='{descriptionId()}'
                class='wire-input-number__description'
            >
                {description}
            </p>
            {/if}

            <div
                class='wire-input-number__control'
            >
                {#if variant === "horizontal" && showButtons}
                <button
                    type="button"
                    aria-label='{decrementLabel}'
                    aria-controls='{inputId()}'
                    disabled='{decrementDisabled()}'
                    @click='decrementValue(event)'
                    class='wire-input-number__button wire-input-number__button--start'
                >
                    <span
                        aria-hidden="true"
                        class='icon-[lucide--minus] wire-input-number__icon'
                    ></span>
                </button>
                {/if}

                <div
                    class='wire-input-number__content'
                >
                    {#if variant === "labeled" || variant === "seat"}
                    <div class='wire-input-number__copy'>
                        {#if label !== ""}
                        <label
                            for='{inputId()}'
                            class='wire-input-number__inner-label'
                        >
                            {label}
                            {#if required}
                            <span
                                aria-hidden="true"
                                class='wire-input-number__required'
                            >*</span>
                            {/if}
                        </label>
                        {/if}

                        {#if description !== ""}
                        <span
                            id='{descriptionId()}'
                            class='wire-input-number__inner-description'
                        >
                            {description}
                        </span>
                        {/if}
                    </div>
                    {/if}

                    <div
                        class='wire-input-number__field'
                    >
                        {#if prefix !== ""}
                        <span
                            aria-hidden="true"
                            class='wire-input-number__prefix'
                        >
                            {prefix}
                        </span>
                        {/if}

                        <input
                            id='{inputId()}'
                            name='{name}'
                            type="number"
                            value='{currentValue}'
                            min='{min}'
                            max='{max}'
                            step='{normalizedStep()}'
                            placeholder='{placeholder}'
                            autocomplete='{autocomplete}'
                            inputmode='{inputMode}'
                            aria-label='{ariaLabel !== "" ? ariaLabel : label !== "" ? label : name}'
                            aria-describedby='{describedBy()}'
                            aria-invalid='{isInvalid() ? "true" : "false"}'
                            aria-required='{required ? "true" : "false"}'
                            aria-disabled='{disabled || inputDisabled ? "true" : "false"}'
                            required='{required}'
                            disabled='{disabled}'
                            readonly='{readonly || inputDisabled || !allowInput}'
                            tabindex='{inputDisabled ? "-1" : "0"}'
                            @input='handleInput(event)'
                            @change='handleChange(event)'
                            @keydown='handleKeydown(event)'
                            @wheel='handleWheel(event)'
                            class='wire-input-number__input'
                        />

                        {#if suffix !== ""}
                        <span
                            aria-hidden="true"
                            class='wire-input-number__suffix'
                        >
                            {suffix}
                        </span>
                        {/if}
                    </div>
                </div>

                {#if variant === "horizontal" && showButtons}
                <button
                    type="button"
                    aria-label='{incrementLabel}'
                    aria-controls='{inputId()}'
                    disabled='{incrementDisabled()}'
                    @click='incrementValue(event)'
                    class='wire-input-number__button wire-input-number__button--end'
                >
                    <span
                        aria-hidden="true"
                        class='icon-[lucide--plus] wire-input-number__icon'
                    ></span>
                </button>
                {:else}
                {#if showButtons}
                <div
                    role="group"
                    aria-label='{controlsLabel}'
                    class='wire-input-number__buttons'
                >
                    <button
                        type="button"
                        aria-label='{decrementLabel}'
                        aria-controls='{inputId()}'
                        disabled='{decrementDisabled()}'
                        @click='decrementValue(event)'
                        class='wire-input-number__button wire-input-number__button--decrement'
                    >
                        <span
                            aria-hidden="true"
                            class='icon-[lucide--minus] wire-input-number__icon'
                        ></span>
                    </button>

                    <button
                        type="button"
                        aria-label='{incrementLabel}'
                        aria-controls='{inputId()}'
                        disabled='{incrementDisabled()}'
                        @click='incrementValue(event)'
                        class='wire-input-number__button wire-input-number__button--increment'
                    >
                        <span
                            aria-hidden="true"
                            class='icon-[lucide--plus] wire-input-number__icon'
                        ></span>
                    </button>
                </div>
                {/if}
                {/if}
            </div>

            {#if showValidationMessage && isInvalid() && validationMessage() !== ""}
            <p
                id='{messageId()}'
                role="alert"
                aria-live="polite"
                class='wire-input-number__message wire-input-number__message--error'
            >
                <span
                    aria-hidden="true"
                    class='icon-[lucide--circle-alert] wire-input-number__error-icon'
                ></span>
                <span>{validationMessage()}</span>
            </p>
            {/if}

            {#if (!showValidationMessage || !isInvalid() || validationMessage() === "") && helpText !== ""}
            <p
                id='{messageId()}'
                class='wire-input-number__message'
            >
                {helpText}
            </p>
            {/if}
        </div>
    }

    style {
        .wire-input-number { position: relative; display: flex; width: 100%; max-width: 24rem; flex-direction: column; gap: 0.375rem; color: var(--wire-color-text); }
        .wire-input-number[data-full-width="true"] { max-width: none; }
        .wire-input-number[data-variant="compact"] { width: fit-content; max-width: 100%; }
        .wire-input-number[data-disabled="true"] { opacity: 0.6; }
        .wire-input-number__label { display: inline-flex; align-items: center; gap: 0.25rem; color: var(--wire-color-text); font-size: 0.875rem; font-weight: 600; line-height: 1.25rem; }
        .wire-input-number__required { margin-left: 0.125rem; color: var(--wire-color-danger); }
        .wire-input-number__description, .wire-input-number__message { margin: 0; color: var(--wire-color-muted); font-size: 0.75rem; line-height: 1.25rem; }
        .wire-input-number__control { display: flex; min-width: 0; min-height: 2.5rem; overflow: hidden; border: 1px solid var(--wire-color-border); border-radius: var(--wire-radius-sm); color: var(--wire-color-text); background: var(--wire-color-surface); box-shadow: var(--wire-shadow-1); transition: border-color var(--wire-motion-base), box-shadow var(--wire-motion-base), background var(--wire-motion-base); }
        .wire-input-number__control:focus-within { border-color: var(--input-number-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--input-number-accent) 18%, transparent); }
        .wire-input-number[data-invalid="true"] .wire-input-number__control { border-color: var(--wire-color-danger); }
        .wire-input-number[data-invalid="true"] .wire-input-number__control:focus-within { box-shadow: 0 0 0 3px color-mix(in srgb, var(--wire-color-danger) 18%, transparent); }
        .wire-input-number[data-disabled="true"] .wire-input-number__control { cursor: not-allowed; background: var(--wire-color-surface-2); }
        .wire-input-number[data-variant="compact"] .wire-input-number__control { border-radius: 999px; }
        .wire-input-number[data-size="xs"] .wire-input-number__control { min-height: 2rem; font-size: 0.75rem; }
        .wire-input-number[data-size="sm"] .wire-input-number__control { min-height: 2.25rem; font-size: 0.875rem; }
        .wire-input-number[data-size="lg"] .wire-input-number__control { min-height: 3rem; font-size: 1rem; }
        .wire-input-number[data-size="xl"] .wire-input-number__control { min-height: 3.5rem; font-size: 1.125rem; }
        .wire-input-number__content { display: flex; min-width: 0; flex: 1; align-items: center; padding-inline: 0.75rem; }
        .wire-input-number[data-size="xs"] .wire-input-number__content { padding-inline: 0.5rem; }
        .wire-input-number[data-size="sm"] .wire-input-number__content { padding-inline: 0.625rem; }
        .wire-input-number[data-size="lg"] .wire-input-number__content { padding-inline: 1rem; }
        .wire-input-number[data-size="xl"] .wire-input-number__content { padding-inline: 1.25rem; }
        .wire-input-number[data-variant="labeled"] .wire-input-number__content { flex-direction: column; align-items: stretch; justify-content: center; gap: 0.125rem; }
        .wire-input-number[data-variant="seat"] .wire-input-number__content { justify-content: space-between; gap: 0.75rem; }
        .wire-input-number__copy { min-width: 0; flex: 1; }
        .wire-input-number__inner-label, .wire-input-number__inner-description { display: block; overflow: hidden; color: var(--wire-color-text); font-size: 0.875rem; font-weight: 600; line-height: 1rem; text-overflow: ellipsis; white-space: nowrap; }
        .wire-input-number__inner-description { color: var(--wire-color-muted); font-size: 0.75rem; font-weight: 400; }
        .wire-input-number[data-variant="labeled"] .wire-input-number__inner-label { color: var(--wire-color-muted); font-size: 0.75rem; font-weight: 500; }
        .wire-input-number__field { display: flex; width: 100%; min-width: 0; align-items: center; }
        .wire-input-number[data-variant="seat"] .wire-input-number__field { width: auto; flex: none; }
        .wire-input-number__prefix, .wire-input-number__suffix { flex: none; color: var(--wire-color-muted); }
        .wire-input-number__prefix { padding-right: 0.375rem; } .wire-input-number__suffix { padding-left: 0.375rem; }
        .wire-input-number__input { width: 100%; min-width: 0; flex: 1; padding: 0; border: 0; outline: none; color: var(--wire-color-text); background: transparent; font: inherit; font-weight: 500; line-height: 1; appearance: textfield; }
        .wire-input-number__input::-webkit-inner-spin-button, .wire-input-number__input::-webkit-outer-spin-button { appearance: none; }
        .wire-input-number__input::placeholder { color: var(--wire-color-muted); }
        .wire-input-number__input:read-only { cursor: default; }
        .wire-input-number__input:disabled { cursor: not-allowed; }
        .wire-input-number:is([data-variant="horizontal"], [data-variant="compact"], [data-variant="seat"]) .wire-input-number__input { text-align: center; }
        .wire-input-number[data-variant="seat"] .wire-input-number__input { width: 2.5rem; flex: none; }
        .wire-input-number__buttons { display: flex; flex: none; border-left: 1px solid var(--wire-color-border); }
        .wire-input-number[data-variant="vertical"] .wire-input-number__buttons { flex-direction: column; }
        .wire-input-number__button { display: inline-flex; width: 2.5rem; flex: none; align-items: center; justify-content: center; padding: 0; border: 0; color: var(--wire-color-muted); background: transparent; transition: color var(--wire-motion-fast), background var(--wire-motion-fast); }
        .wire-input-number[data-size="xs"] .wire-input-number__button { width: 2rem; } .wire-input-number[data-size="sm"] .wire-input-number__button { width: 2.25rem; } .wire-input-number[data-size="lg"] .wire-input-number__button { width: 3rem; } .wire-input-number[data-size="xl"] .wire-input-number__button { width: 3.5rem; }
        .wire-input-number__button--start { border-right: 1px solid var(--wire-color-border); } .wire-input-number__button--end { border-left: 1px solid var(--wire-color-border); }
        .wire-input-number__button--decrement { border-right: 1px solid var(--wire-color-border); }
        .wire-input-number[data-variant="vertical"] .wire-input-number__button--decrement { border-right: 0; border-bottom: 1px solid var(--wire-color-border); }
        .wire-input-number__button:hover { color: var(--input-number-accent); background: var(--wire-color-surface-2); }
        .wire-input-number__button:focus-visible { z-index: 1; outline: 2px solid var(--input-number-accent); outline-offset: -2px; }
        .wire-input-number__button:disabled { cursor: not-allowed; opacity: 0.4; }
        .wire-input-number__icon { width: 1rem; height: 1rem; }
        .wire-input-number__message--error { display: flex; align-items: center; gap: 0.375rem; color: var(--wire-color-danger); }
        .wire-input-number__error-icon { width: 0.875rem; height: 0.875rem; flex: none; }
    }
}
