import * as React from 'react'; import { cva, type VariantProps } from 'class-variance-authority'; import { CheckIcon, ChevronDown, XIcon } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Separator } from '@/components/ui/separator'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from '@/components/ui/command'; /** * 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('m-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300', { variants: { variant: { inverted: 'inverted', }, }, defaultVariants: { variant: 'inverted', }, }); /** * Props for MultiSelect component */ interface MultiSelectProps extends React.ButtonHTMLAttributes, VariantProps { /** * An array of option objects to be displayed in the multi-select component. * Each option object has a label, value, and an optional icon. */ options: { /** 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?: React.ComponentType<{ className?: string }>; }[]; /** * 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; /** * 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; } export const MultiSelect = React.forwardRef( ( { options, onValueChange, variant, defaultValue = [], placeholder = 'Select options', animation = 0, maxCount = 3, modalPopover = false, className, ...props }, ref, ) => { const [selectedValues, setSelectedValues] = React.useState(defaultValue); const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); const [isAnimating] = React.useState(false); const handleInputKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { setIsPopoverOpen(true); } else if (event.key === 'Backspace' && !event.currentTarget.value) { const newSelectedValues = [...selectedValues]; newSelectedValues.pop(); setSelectedValues(newSelectedValues); onValueChange(newSelectedValues); } }; const toggleOption = (option: string) => { const newSelectedValues = selectedValues.includes(option) ? selectedValues.filter((value) => value !== option) : [...selectedValues, option]; setSelectedValues(newSelectedValues); onValueChange(newSelectedValues); }; const handleClear = () => { setSelectedValues([]); onValueChange([]); }; const handleTogglePopover = () => { setIsPopoverOpen((prev) => !prev); }; const toggleAll = () => { if (selectedValues.length === options.length) { handleClear(); } else { const allValues = options.map((option) => option.value); setSelectedValues(allValues); onValueChange(allValues); } }; return ( setIsPopoverOpen(false)}> No results found.
(Select All)
{options.map((option) => { const isSelected = selectedValues.includes(option.value); return ( toggleOption(option.value)} className="cursor-pointer">
{option.icon && } {option.label}
); })}
{selectedValues.length > 0 && ( <> Clear )} setIsPopoverOpen(false)} className="max-w-full flex-1 cursor-pointer justify-center"> Close
); }, ); MultiSelect.displayName = 'MultiSelect';