import { usePage } from "@inertiajs/react"; import { cva, type VariantProps } from "class-variance-authority"; import { CheckIcon, ChevronsUpDownIcon, SearchIcon, WandSparklesIcon, XCircleIcon, XIcon, } from "lucide-react"; import { type ButtonHTMLAttributes, type ComponentType, type CSSProperties, type KeyboardEvent, type ReactNode, useCallback, useEffect, useId, useImperativeHandle, useMemo, useRef, useState, } from "react"; import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip"; import InputError from "@/back/Components/InputError"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList, } from "@/components/ui/command"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Separator } from "@/components/ui/separator"; import str from "@/hooks/use-string"; import trans from "@/hooks/use-translations"; import { cn } from "@/lib/utils"; /** * Animation types and configurations */ export interface AnimationConfig { /** Badge animation type */ badgeAnimation?: "bounce" | "pulse" | "wiggle" | "fade" | "slide" | "none"; /** Popover animation type */ popoverAnimation?: "scale" | "slide" | "fade" | "flip" | "none"; /** Option hover animation type */ optionHoverAnimation?: "highlight" | "scale" | "glow" | "none"; /** Animation duration in seconds */ duration?: number; /** Animation delay in seconds */ delay?: number; } /** * Variants for the multi-select component to handle different styles. * Uses class-variance-authority (cva) to define different styles based on "variant" prop. */ const multiSelectVariants = cva("transition-all duration-300 ease-in-out", { variants: { variant: { default: "border-foreground/10 text-foreground bg-card hover:bg-card/80", secondary: "border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80", destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", inverted: "inverted", }, badgeAnimation: { bounce: "hover:-translate-y-1 hover:scale-110", pulse: "hover:animate-pulse", wiggle: "hover:animate-wiggle", fade: "hover:opacity-80", slide: "hover:translate-x-1", none: "", }, }, defaultVariants: { variant: "default", badgeAnimation: "none", }, }); /** * Option interface for MultiSelect component */ interface MultiSelectOption { /** The text to display for the option. */ label: string; /** The unique value associated with the option. */ value: string; /** Optional icon component to display alongside the option. */ icon?: ComponentType<{ className?: string }>; /** Whether this option is disabled */ disabled?: boolean; /** Custom styling for the option */ style?: { /** Custom badge color */ badgeColor?: string; /** Custom icon color */ iconColor?: string; /** Gradient background for badge */ gradient?: string; }; } /** * Group interface for organizing options */ interface MultiSelectGroup { /** Group heading */ heading: string; /** Options in this group */ options: MultiSelectOption[]; } /** * Props for MultiSelect component */ interface MultiSelectProps extends Omit, "animationConfig">, VariantProps { /** * An array of option objects or groups to be displayed in the multi-select component. */ options: MultiSelectOption[] | MultiSelectGroup[]; /** * Callback function triggered when the selected values change. * Receives an array of the new selected values. */ onValueChange?: (value: string[]) => void; /** The default selected values when the component mounts. */ defaultValue?: string[]; /** * Placeholder text to be displayed when no values are selected. * Optional, defaults to "Select options". */ placeholder?: string; /** * Animation duration in seconds for the visual effects (e.g., bouncing badges). * Optional, defaults to 0 (no animation). */ animation?: number; /** * Advanced animation configuration for different component parts. * Optional, allows fine-tuning of various animation effects. */ animationConfig?: AnimationConfig; /** * Maximum number of items to display. Extra selected items will be summarized. * Optional, defaults to 3. */ maxCount?: number; /** * The modality of the popover. When set to true, interaction with outside elements * will be disabled and only popover content will be visible to screen readers. * Optional, defaults to false. */ modalPopover?: boolean; /** * If true, renders the multi-select component as a child of another component. * Optional, defaults to false. */ asChild?: boolean; /** * Additional class names to apply custom styles to the multi-select component. * Optional, can be used to add custom styles. */ className?: string; /** * If true, disables the select all functionality. * Optional, defaults to false. */ hideSelectAll?: boolean; /** * If true, shows search functionality in the popover. * If false, hides the search input completely. * Optional, defaults to true. */ searchable?: boolean; /** * Custom empty state message when no options match search. * Optional, defaults to "No results found." */ emptyIndicator?: ReactNode; /** * If true, allows the component to grow and shrink with its content. * If false, uses fixed width behavior. * Optional, defaults to false. */ autoSize?: boolean; /** * If true, shows badges in a single line with horizontal scroll. * If false, badges wrap to multiple lines. * Optional, defaults to false. */ singleLine?: boolean; /** * Custom CSS class for the popover content. * Optional, can be used to customize popover appearance. */ popoverClassName?: string; /** * If true, disables the component completely. * Optional, defaults to false. */ disabled?: boolean; /** * Responsive configuration for different screen sizes. * Allows customizing maxCount and other properties based on viewport. * Can be boolean true for default responsive behavior or an object for custom configuration. */ responsive?: | boolean | { /** Configuration for mobile devices (< 640px) */ mobile?: { maxCount?: number; hideIcons?: boolean; compactMode?: boolean; }; /** Configuration for tablet devices (640px - 1024px) */ tablet?: { maxCount?: number; hideIcons?: boolean; compactMode?: boolean; }; /** Configuration for desktop devices (> 1024px) */ desktop?: { maxCount?: number; hideIcons?: boolean; compactMode?: boolean; }; }; /** * Minimum width for the component. * Optional, defaults to auto-sizing based on content. * When set, component will not shrink below this width. */ minWidth?: string; /** * Maximum width for the component. * Optional, defaults to 100% of container. * Component will not exceed container boundaries. */ maxWidth?: string; /** * If true, automatically removes duplicate options based on their value. * Optional, defaults to false (shows warning in dev mode instead). */ deduplicateOptions?: boolean; /** * If true, the component will reset its internal state when defaultValue changes. * Useful for React Hook Form integration and form reset functionality. * Optional, defaults to true. */ resetOnDefaultValueChange?: boolean; /** * If true, automatically closes the popover after selecting an option. * Useful for single-selection-like behavior or mobile UX. * Optional, defaults to false. */ closeOnSelect?: boolean; /** * The name of the field to complete */ name: string; /** * Label of the field */ label: ReactNode | string; /** * Does the field required */ required?: boolean; /** * The fonction to set the data in useForm inertia */ setData: (name: string, value: string[]) => void; /** * Does the field required */ multiple?: boolean; /** * Minimum number of selections required (only when multiple is true) * Optional, defaults to undefined (no minimum) */ minSelection?: number; /** * Maximum number of selections allowed (only when multiple is true) * Optional, defaults to undefined (no maximum) */ maxSelection?: number; /** * Helper for the input * Optional, defaults to empty */ helper?: string; /** * Make the Close button visible * Optional, defaults to empty */ closeButtonVisible?: boolean; } /** * Imperative methods exposed through ref */ export interface MultiSelectRef { /** * Programmatically reset the component to its default value */ reset: () => void; /** * Get current selected values */ getSelectedValues: () => string[]; /** * Set selected values programmatically */ setSelectedValues: (values: string[]) => void; /** * Clear all selected values */ clear: () => void; /** * Focus the component */ focus: () => void; } export const MultiSelect = ({ ref, name, label, required = true, setData, multiple = true, minSelection, maxSelection, helper, options, onValueChange, variant, defaultValue = [], placeholder = "Select options", animation = 0, animationConfig, maxCount = 3, modalPopover = false, asChild: _asChild = false, className, hideSelectAll = false, searchable = true, emptyIndicator, autoSize = false, singleLine = false, popoverClassName, disabled = false, responsive, minWidth, maxWidth, deduplicateOptions = false, resetOnDefaultValueChange = true, closeOnSelect = false, closeButtonVisible = false, ...props }: MultiSelectProps & { ref?: React.RefObject }) => { const [selectedValues, setSelectedValues] = useState(defaultValue); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isAnimating, setIsAnimating] = useState(false); const [searchValue, setSearchValue] = useState(""); const [politeMessage, setPoliteMessage] = useState(""); const [assertiveMessage, setAssertiveMessage] = useState(""); const prevSelectedCountRef = useRef(selectedValues.length); const prevIsOpenRef = useRef(isPopoverOpen); const prevSearchValueRef = useRef(searchValue); const { errors } = usePage().props; const [prevIsPopoverOpen, setPrevIsPopoverOpen] = useState(isPopoverOpen); if (isPopoverOpen !== prevIsPopoverOpen) { setPrevIsPopoverOpen(isPopoverOpen); if (!isPopoverOpen) { setSearchValue(""); } } const announce = useCallback( (message: string, priority: "polite" | "assertive" = "polite") => { if (priority === "assertive") { setAssertiveMessage(message); setTimeout(() => { setAssertiveMessage(""); }, 100); } else { setPoliteMessage(message); setTimeout(() => { setPoliteMessage(""); }, 100); } }, [], ); const multiSelectId = useId(); const listboxId = `${multiSelectId}-listbox`; const triggerDescriptionId = `${multiSelectId}-description`; const selectedCountId = `${multiSelectId}-count`; // Translations. const transDelete = trans("uwil-backend::crud.actions.delete"); const transClose = trans("uwil-backend::back.close"); const transClear = trans("uwil-backend::back.clear"); const transNoResults = trans("uwil-backend::back.table_no_results"); const transSearchOptions = trans( "uwil-backend::back.multi_select.search_options", ); const transSelectAll = trans("uwil-backend::back.multi_select.select_all"); const transMore = trans("uwil-backend::back.multi_select.more"); const transClosePopover = trans( "uwil-backend::back.multi_select.close_popover", ); const transClearList = trans("uwil-backend::back.multi_select.clear_list"); const [prevDefaultValue, setPrevDefaultValue] = useState(defaultValue); const isGroupedOptions = useCallback( ( opts: MultiSelectOption[] | MultiSelectGroup[], ): opts is MultiSelectGroup[] => { return opts.length > 0 && "heading" in opts[0]; }, [], ); const arraysEqual = useCallback((a: string[], b: string[]): boolean => { if (a.length !== b.length) return false; const sortedA = [...a].sort(); const sortedB = [...b].sort(); return sortedA.every((val, index) => val === sortedB[index]); }, []); const resetToDefault = useCallback(() => { setSelectedValues(defaultValue); setIsPopoverOpen(false); setSearchValue(""); if (onValueChange) { onValueChange(defaultValue); } setData(name, defaultValue); // eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultValue, onValueChange]); const buttonRef = useRef(null); useImperativeHandle( ref, () => ({ reset: resetToDefault, getSelectedValues: () => selectedValues, setSelectedValues: (values: string[]) => { setSelectedValues(values); if (onValueChange) { onValueChange(values); } setData(name, values); }, clear: () => { setSelectedValues([]); if (onValueChange) { onValueChange([]); } setData(name, []); }, focus: () => { if (buttonRef.current) { buttonRef.current.focus(); const originalOutline = buttonRef.current.style.outline; const originalOutlineOffset = buttonRef.current.style.outlineOffset; buttonRef.current.style.outline = "2px solid hsl(var(--ring))"; buttonRef.current.style.outlineOffset = "2px"; setTimeout(() => { if (buttonRef.current) { buttonRef.current.style.outline = originalOutline; buttonRef.current.style.outlineOffset = originalOutlineOffset; } }, 1000); } }, }), // eslint-disable-next-line react-hooks/exhaustive-deps [resetToDefault, selectedValues, onValueChange], ); const [screenSize, setScreenSize] = useState<"mobile" | "tablet" | "desktop">( () => { if (typeof window === "undefined") return "desktop"; const width = window.innerWidth; if (width < 640) return "mobile"; if (width < 1024) return "tablet"; return "desktop"; }, ); useEffect(() => { if (typeof window === "undefined") return undefined; const handleResize = () => { const width = window.innerWidth; if (width < 640) { setScreenSize("mobile"); } else if (width < 1024) { setScreenSize("tablet"); } else { setScreenSize("desktop"); } }; window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize", handleResize); }; }, []); const getResponsiveSettings = () => { if (!responsive) { return { maxCount, hideIcons: false, compactMode: false, }; } if (responsive === true) { const defaultResponsive = { mobile: { maxCount: 2, hideIcons: false, compactMode: true, }, tablet: { maxCount: 4, hideIcons: false, compactMode: false, }, desktop: { maxCount: 6, hideIcons: false, compactMode: false, }, }; const currentSettings = defaultResponsive[screenSize]; return { maxCount: currentSettings.maxCount, hideIcons: currentSettings.hideIcons, compactMode: currentSettings.compactMode, }; } const currentSettings = responsive[screenSize]; return { maxCount: currentSettings?.maxCount ?? maxCount, hideIcons: currentSettings?.hideIcons ?? false, compactMode: currentSettings?.compactMode ?? false, }; }; const responsiveSettings = getResponsiveSettings(); const getBadgeAnimationClass = () => { if (animationConfig?.badgeAnimation) { switch (animationConfig.badgeAnimation) { case "bounce": return isAnimating ? "animate-bounce" : "hover:-translate-y-1 hover:scale-110"; case "pulse": return "hover:animate-pulse"; case "wiggle": return "hover:animate-wiggle"; case "fade": return "hover:opacity-80"; case "slide": return "hover:translate-x-1"; case "none": return ""; default: return ""; } } return isAnimating ? "animate-bounce" : ""; }; const getPopoverAnimationClass = () => { if (animationConfig?.popoverAnimation) { switch (animationConfig.popoverAnimation) { case "scale": return "animate-scaleIn"; case "slide": return "animate-slideInDown"; case "fade": return "animate-fadeIn"; case "flip": return "animate-flipIn"; case "none": return ""; default: return ""; } } return ""; }; const getAllOptions = useCallback((): MultiSelectOption[] => { if (options.length === 0) return []; let allOptions: MultiSelectOption[]; if (isGroupedOptions(options)) { allOptions = options.flatMap((group) => group.options); } else { allOptions = options; } const valueSet = new Set(); const duplicates: string[] = []; const uniqueOptions: MultiSelectOption[] = []; allOptions.forEach((option) => { if (valueSet.has(option.value)) { duplicates.push(option.value); if (!deduplicateOptions) { uniqueOptions.push(option); } } else { valueSet.add(option.value); uniqueOptions.push(option); } }); if (import.meta.env.MODE === "development" && duplicates.length > 0) { const action = deduplicateOptions ? "automatically removed" : "detected"; console.warn( `MultiSelect: Duplicate option values ${action}: ${duplicates.join( ", ", )}. ` + (deduplicateOptions ? "Duplicates have been removed automatically." : "This may cause unexpected behavior. Consider setting 'deduplicateOptions={true}' or ensure all option values are unique."), ); } return deduplicateOptions ? uniqueOptions : allOptions; }, [options, deduplicateOptions, isGroupedOptions]); const getOptionByValue = useCallback( (value: string): MultiSelectOption | undefined => { const option = getAllOptions().find((opt) => opt.value === value); if (!option && import.meta.env.MODE === "development") { console.warn( `MultiSelect: Option with value "${value}" not found in options list`, ); } return option; }, [getAllOptions], ); const filteredOptions = useMemo(() => { if (!searchable || !searchValue) return options; if (options.length === 0) return []; if (isGroupedOptions(options)) { return options .map((group) => ({ ...group, options: group.options.filter( (option) => option.label.toLowerCase().includes(searchValue.toLowerCase()) || option.value.toLowerCase().includes(searchValue.toLowerCase()), ), })) .filter((group) => group.options.length > 0); } return options.filter( (option) => option.label.toLowerCase().includes(searchValue.toLowerCase()) || option.value.toLowerCase().includes(searchValue.toLowerCase()), ); }, [options, searchValue, searchable, isGroupedOptions]); const handleInputKeyDown = (event: KeyboardEvent) => { if (event.key === "Enter") { setIsPopoverOpen(true); } else if (event.key === "Backspace" && !event.currentTarget.value) { const newSelectedValues = [...selectedValues]; newSelectedValues.pop(); setSelectedValues(newSelectedValues); if (onValueChange) { onValueChange(newSelectedValues); } setData(name, newSelectedValues); } }; const toggleOption = (optionValue: string) => { if (disabled) return; const option = getOptionByValue(optionValue); if (option?.disabled) return; // MODE SINGLE SELECT if (!multiple) { const newSelectedValues = selectedValues.includes(optionValue) ? [] : [optionValue]; setSelectedValues(newSelectedValues); if (onValueChange) { onValueChange(newSelectedValues); } setData(name, newSelectedValues); if (closeOnSelect) { setIsPopoverOpen(false); } return; } // MODE MULTIPLE SELECT const isCurrentlySelected = selectedValues.includes(optionValue); if (isCurrentlySelected) { if (minSelection && selectedValues.length <= minSelection) { announce( `Minimum ${String(minSelection)} selection(s) required`, "assertive", ); return; } const newSelectedValues = selectedValues.filter( (value) => value !== optionValue, ); setSelectedValues(newSelectedValues); if (onValueChange) { onValueChange(newSelectedValues); } setData(name, newSelectedValues); } else { if (maxSelection && selectedValues.length >= maxSelection) { announce( `Maximum ${String(maxSelection)} selection(s) allowed`, "assertive", ); return; } const newSelectedValues = [...selectedValues, optionValue]; setSelectedValues(newSelectedValues); if (onValueChange) { onValueChange(newSelectedValues); } setData(name, newSelectedValues); } if (closeOnSelect) { setIsPopoverOpen(false); } }; const handleClear = () => { if (disabled) return; if (multiple && minSelection && minSelection > 0) { announce( `Minimum ${String(minSelection)} selection(s) required`, "assertive", ); return; } setSelectedValues([]); if (onValueChange) { onValueChange([]); } setData(name, []); }; const handleTogglePopover = () => { if (disabled) return; setIsPopoverOpen((prev) => !prev); }; const clearExtraOptions = () => { if (disabled) return; const newSelectedValues = selectedValues.slice( 0, responsiveSettings.maxCount, ); setSelectedValues(newSelectedValues); if (onValueChange) { onValueChange(newSelectedValues); } setData(name, newSelectedValues); }; const toggleAll = () => { if (disabled || !multiple) return; const allOptions = getAllOptions().filter((option) => !option.disabled); if (selectedValues.length === allOptions.length) { if (minSelection && minSelection > 0) { announce( `Minimum ${String(minSelection)} selection(s) required`, "assertive", ); return; } handleClear(); } else { const allValues = allOptions.map((option) => option.value); if (maxSelection && allValues.length > maxSelection) { announce( `Maximum ${String(maxSelection)} selection(s) allowed`, "assertive", ); return; } setSelectedValues(allValues); if (onValueChange) { onValueChange(allValues); } setData(name, allValues); } if (closeOnSelect) { setIsPopoverOpen(false); } }; if ( resetOnDefaultValueChange && !arraysEqual(prevDefaultValue, defaultValue) ) { setPrevDefaultValue([...defaultValue]); if (!arraysEqual(selectedValues, defaultValue)) { setSelectedValues(defaultValue); } } const getWidthConstraints = () => { const defaultMinWidth = screenSize === "mobile" ? "0px" : "200px"; const effectiveMinWidth = minWidth ?? defaultMinWidth; const effectiveMaxWidth = maxWidth ?? "100%"; return { minWidth: effectiveMinWidth, maxWidth: effectiveMaxWidth, width: autoSize ? "auto" : "100%", }; }; const widthConstraints = getWidthConstraints(); const handleAnnouncements = useCallback(() => { const selectedCount = selectedValues.length; const allOptions = getAllOptions(); const totalOptions = allOptions.filter((opt) => !opt.disabled).length; if (selectedCount !== prevSelectedCountRef.current) { const diff = selectedCount - prevSelectedCountRef.current; if (diff > 0) { const addedItems = selectedValues.slice(-diff); const addedLabels = addedItems .map((value) => allOptions.find((opt) => opt.value === value)?.label) .filter(Boolean); if (addedLabels.length === 1) { announce( `${addedLabels[0] ?? ""} selected. ${String(selectedCount)} of ${String(totalOptions)} options selected.`, ); } else { announce( `${String(addedLabels.length)} options selected. ${String(selectedCount)} of ${String(totalOptions)} total selected.`, ); } } else if (diff < 0) { announce( `Option removed. ${String(selectedCount)} of ${String(totalOptions)} options selected.`, ); } prevSelectedCountRef.current = selectedCount; } if (isPopoverOpen !== prevIsOpenRef.current) { if (isPopoverOpen) { announce( `Dropdown opened. ${String(totalOptions)} options available. Use arrow keys to navigate.`, ); } else { announce("Dropdown closed."); } prevIsOpenRef.current = isPopoverOpen; } if (searchValue !== prevSearchValueRef.current) { if (searchValue && isPopoverOpen) { const filteredCount = allOptions.filter( (opt) => opt.label.toLowerCase().includes(searchValue.toLowerCase()) || opt.value.toLowerCase().includes(searchValue.toLowerCase()), ).length; announce( `${String(filteredCount)} option${ filteredCount === 1 ? "" : "s" } found for "${searchValue}"`, ); } prevSearchValueRef.current = searchValue; } }, [selectedValues, isPopoverOpen, searchValue, announce, getAllOptions]); useEffect(() => { handleAnnouncements(); }, [handleAnnouncements]); useEffect(() => { if ( minSelection !== undefined && maxSelection !== undefined && minSelection > maxSelection ) { console.error( "MultiSelect: minSelection cannot be greater than maxSelection", ); } if ( !multiple && (minSelection !== undefined || maxSelection !== undefined) ) { console.warn( "MultiSelect: minSelection and maxSelection are ignored when multiple is false", ); } if (minSelection !== undefined && minSelection < 0) { console.error("MultiSelect: minSelection cannot be negative"); } if (maxSelection !== undefined && maxSelection < 1) { console.error("MultiSelect: maxSelection must be at least 1"); } }, [minSelection, maxSelection, multiple]); return (
{politeMessage}
{assertiveMessage}
Multi-select dropdown. Use arrow keys to navigate, Enter to select, and Escape to close.
{selectedValues.length === 0 ? "No options selected" : `${String(selectedValues.length)} option${ selectedValues.length === 1 ? "" : "s" } selected: ${selectedValues .map((value) => getOptionByValue(value)?.label) .filter(Boolean) .join(", ")}`}
{ setIsPopoverOpen(false); }} > {searchable && (
{ setSearchValue(e.target.value); }} onKeyDown={handleInputKeyDown} className="w-[calc(100%-2.5rem-40px)] border-0 mx-9" /> } tooltipContent={

{str(transDelete).ucFirst().value()}

} type="button" variant="ghost" size="icon" className="absolute top-1/2 right-0 transform -translate-y-1/2 text-muted-foreground" onClick={() => { setSearchValue(""); }} disabled={searchValue.length < 1 || disabled} />
)} {searchable && (
Type to filter options. Use arrow keys to navigate results.
)} {emptyIndicator ?? transNoResults} {/* First option for select all items */} {!hideSelectAll && !searchValue && multiple && ( !opt.disabled).length } aria-label={`Select all ${String( getAllOptions().length, )} options`} className="cursor-pointer gap-1.5 px-4 py-2 w-full h-auto min-h-10" >
!opt.disabled).length ? "bg-primary text-primary-foreground" : "opacity-50 [&_svg]:invisible", )} aria-hidden="true" >
({str(transSelectAll).ucFirst().value()} {getAllOptions().length > 20 ? ` - ${String(getAllOptions().length)} options` : ""} )
)} {/* List of items */} {isGroupedOptions(filteredOptions) ? ( filteredOptions.map((group) => ( {group.options.map((option) => { const isSelected = selectedValues.includes(option.value); return ( { toggleOption(option.value); }} role="option" aria-selected={isSelected} aria-disabled={option.disabled} aria-label={`${option.label}${ isSelected ? ", selected" : ", not selected" }${option.disabled ? ", disabled" : ""}`} className={cn( "cursor-pointer gap-1.5 px-4 py-2 w-full h-auto min-h-10", option.disabled && "opacity-50 cursor-not-allowed", )} disabled={option.disabled} > {option.icon && ( ); })} )) ) : ( {filteredOptions.map((option) => { const isSelected = selectedValues.includes(option.value); return ( { toggleOption(option.value); }} role="option" aria-selected={isSelected} aria-disabled={option.disabled} aria-label={`${option.label}${ isSelected ? ", selected" : ", not selected" }${option.disabled ? ", disabled" : ""}`} className={cn( "cursor-pointer gap-1.5 px-4 py-2 w-full h-auto min-h-10", option.disabled && "opacity-50 cursor-not-allowed", )} disabled={option.disabled} > {option.icon && ( ); })} )}
{(closeButtonVisible || selectedValues.length > 0) && (
{selectedValues.length > 0 && ( {str(transClearList).ucFirst().value()}

} className="w-full" type="button" variant="ghost" size="default" onClick={handleClear} disabled={disabled} /> )} {closeButtonVisible && selectedValues.length > 0 && ( )} {closeButtonVisible && ( {str(transClosePopover).ucFirst().value()}

} className="w-full" type="button" variant="ghost" size="default" onClick={() => { setIsPopoverOpen(false); }} disabled={disabled} /> )}
)}
{animation > 0 && selectedValues.length > 0 && ( { setIsAnimating(!isAnimating); }} /> )}
{helper && {helper}}
); }; MultiSelect.displayName = "MultiSelect"; export type { MultiSelectGroup, MultiSelectOption, MultiSelectProps };