{"version":3,"file":"MultiSelect.cjs","names":[],"sources":["../../../src/components/MultiSelect/MultiSelect.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — options/value/onChange\n * plus the shared field contract, and the three knobs that make it multi: maxItems,\n * filter, emptyMessage. The body is the listbox with chip removal, type-ahead and\n * the ARIA active-descendant wiring reading one active index.\n */\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./MultiSelect.module.css\";\n\nexport interface MultiSelectOption {\n    value: string;\n    label: string;\n    disabled?: boolean;\n}\n\nexport interface MultiSelectProps {\n    options: MultiSelectOption[];\n    /** Currently selected values. */\n    value: string[];\n    onChange: (value: string[]) => void;\n    label?: string;\n    placeholder?: string;\n    helperText?: string;\n    error?: string;\n    disabled?: boolean;\n    /** Cap the number of selectable items. */\n    maxItems?: number;\n    /** Custom filter — return true to keep the option. Default: case-insensitive substring on label. */\n    filter?: (option: MultiSelectOption, query: string) => boolean;\n    /** Message shown when no option matches. */\n    emptyMessage?: string;\n    className?: string;\n}\n\nfunction defaultFilter(option: MultiSelectOption, query: string): boolean {\n    if (!query) return true;\n    return option.label.toLowerCase().includes(query.toLowerCase());\n}\n\n/**\n * MultiSelect — a filterable dropdown that selects many options, shown as\n * removable chips inside the field. Selecting toggles a value; Backspace on an\n * empty query removes the last chip.\n *\n * Keyboard: ArrowUp/ArrowDown navigate, Enter toggles the active option, Esc\n * closes, Backspace (empty input) pops the last chip.\n */\nexport function MultiSelect({\n    options,\n    value,\n    onChange,\n    label,\n    placeholder = \"Selecione\",\n    helperText,\n    error,\n    disabled,\n    maxItems,\n    filter = defaultFilter,\n    emptyMessage = \"Nenhuma opção encontrada\",\n    className,\n}: MultiSelectProps) {\n    const id = useId();\n    const rootRef = useRef<HTMLDivElement>(null);\n    const inputRef = useRef<HTMLInputElement>(null);\n    const [open, setOpen] = useState(false);\n    const [query, setQuery] = useState(\"\");\n    const [activeIndex, setActiveIndex] = useState(0);\n\n    const selectedSet = useMemo(() => new Set(value), [value]);\n    const selectedOptions = useMemo(\n        () =>\n            value\n                .map((v) => options.find((o) => o.value === v))\n                .filter((o): o is MultiSelectOption => Boolean(o)),\n        [value, options],\n    );\n    const filtered = useMemo(\n        () => options.filter((o) => filter(o, query)),\n        [options, filter, query],\n    );\n\n    const atMax = maxItems !== undefined && value.length >= maxItems;\n\n    const toggle = useCallback(\n        (option: MultiSelectOption): void => {\n            if (option.disabled) return;\n            if (selectedSet.has(option.value)) {\n                onChange(value.filter((v) => v !== option.value));\n            } else {\n                if (atMax) return;\n                onChange([...value, option.value]);\n            }\n            setQuery(\"\");\n            inputRef.current?.focus();\n        },\n        [selectedSet, onChange, value, atMax],\n    );\n\n    const removeAt = useCallback(\n        (optionValue: string): void => {\n            onChange(value.filter((v) => v !== optionValue));\n        },\n        [onChange, value],\n    );\n\n    useEffect(() => {\n        if (!open) return;\n        const onDown = (event: MouseEvent): void => {\n            if (rootRef.current && !rootRef.current.contains(event.target as Node)) {\n                setOpen(false);\n                setQuery(\"\");\n            }\n        };\n        window.addEventListener(\"mousedown\", onDown);\n        return () => window.removeEventListener(\"mousedown\", onDown);\n    }, [open]);\n\n    const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {\n        if (event.key === \"ArrowDown\") {\n            event.preventDefault();\n            setOpen(true);\n            setActiveIndex((current) => Math.min(filtered.length - 1, current + 1));\n        } else if (event.key === \"ArrowUp\") {\n            event.preventDefault();\n            setActiveIndex((current) => Math.max(0, current - 1));\n        } else if (event.key === \"Enter\") {\n            event.preventDefault();\n            const option = filtered[activeIndex];\n            if (option) toggle(option);\n        } else if (event.key === \"Escape\") {\n            setOpen(false);\n            setQuery(\"\");\n        } else if (event.key === \"Backspace\" && query === \"\" && value.length > 0) {\n            removeAt(value[value.length - 1]);\n        }\n    };\n\n    return (\n        <div ref={rootRef} className={cn(styles.wrapper, error && styles.error, className)}>\n            {label && (\n                <label htmlFor={id} className={styles.label}>\n                    {label}\n                </label>\n            )}\n            <div\n                className={cn(styles.field, disabled && styles.disabled)}\n                onMouseDown={(event) => {\n                    if (disabled) return;\n                    // Keep focus on the input when clicking empty field space.\n                    if (event.target === event.currentTarget) {\n                        event.preventDefault();\n                        inputRef.current?.focus();\n                        setOpen(true);\n                    }\n                }}\n            >\n                {selectedOptions.map((option) => (\n                    <span key={option.value} className={styles.chip}>\n                        {option.label}\n                        <button\n                            type=\"button\"\n                            className={styles.chipRemove}\n                            aria-label={`Remover ${option.label}`}\n                            disabled={disabled}\n                            onMouseDown={(event) => {\n                                event.preventDefault();\n                                removeAt(option.value);\n                            }}\n                        >\n                            ×\n                        </button>\n                    </span>\n                ))}\n                <input\n                    id={id}\n                    ref={inputRef}\n                    type=\"text\"\n                    role=\"combobox\"\n                    aria-expanded={open}\n                    aria-controls={`${id}-listbox`}\n                    aria-autocomplete=\"list\"\n                    className={styles.input}\n                    placeholder={value.length === 0 ? placeholder : \"\"}\n                    disabled={disabled}\n                    value={query}\n                    onFocus={() => setOpen(true)}\n                    onChange={(event) => {\n                        setQuery(event.target.value);\n                        setOpen(true);\n                        setActiveIndex(0);\n                    }}\n                    onKeyDown={handleKeyDown}\n                />\n            </div>\n            {open && (\n                <ul\n                    id={`${id}-listbox`}\n                    role=\"listbox\"\n                    aria-multiselectable\n                    className={styles.menu}\n                >\n                    {filtered.length === 0 ? (\n                        <li className={styles.empty}>{emptyMessage}</li>\n                    ) : (\n                        filtered.map((option, index) => {\n                            const isSelected = selectedSet.has(option.value);\n                            return (\n                                <li\n                                    key={option.value}\n                                    role=\"option\"\n                                    aria-selected={isSelected}\n                                    aria-disabled={option.disabled || (!isSelected && atMax)}\n                                    className={cn(\n                                        styles.option,\n                                        index === activeIndex && styles.active,\n                                        isSelected && styles.selected,\n                                    )}\n                                    onMouseEnter={() => setActiveIndex(index)}\n                                    onMouseDown={(event) => {\n                                        event.preventDefault();\n                                        toggle(option);\n                                    }}\n                                >\n                                    <span className={styles.check} aria-hidden>\n                                        {isSelected ? \"✓\" : \"\"}\n                                    </span>\n                                    {option.label}\n                                </li>\n                            );\n                        })\n                    )}\n                </ul>\n            )}\n            {error ? (\n                <span className={styles.errorText}>{error}</span>\n            ) : helperText ? (\n                <span className={styles.helper}>{helperText}</span>\n            ) : null}\n        </div>\n    );\n}\n"],"mappings":"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"}