@octavian-tocan/react-dropdown 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,779 @@
1
+ import React2, { createContext, useContext, useRef, useState, useCallback, useEffect, useMemo, useLayoutEffect } from 'react';
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+ import { createPortal } from 'react-dom';
4
+ import { AnimatePresence, motion } from 'motion/react';
5
+
6
+ // src/DropdownRoot.tsx
7
+ var DropdownContext = createContext(null);
8
+ function useDropdownContext() {
9
+ const context = useContext(DropdownContext);
10
+ if (!context) {
11
+ throw new Error(
12
+ "useDropdownContext must be used within a DropdownProvider. Wrap your component with <DropdownRoot> to provide context."
13
+ );
14
+ }
15
+ return context;
16
+ }
17
+ function DropdownProvider({ children, value }) {
18
+ return /* @__PURE__ */ jsx(DropdownContext.Provider, { value, children });
19
+ }
20
+ function useKeyboardNavigation(items, getItemKey, onSelect, closeDropdown) {
21
+ const focusedIndexRef = useRef(-1);
22
+ const [focusedIndex, setFocusedIndex] = useState(-1);
23
+ const handleKeyDown = (event) => {
24
+ switch (event.key) {
25
+ case "ArrowDown":
26
+ event.preventDefault();
27
+ focusedIndexRef.current = Math.min(focusedIndexRef.current + 1, items.length - 1);
28
+ setFocusedIndex(focusedIndexRef.current);
29
+ break;
30
+ case "ArrowUp":
31
+ event.preventDefault();
32
+ focusedIndexRef.current = Math.max(focusedIndexRef.current - 1, 0);
33
+ setFocusedIndex(focusedIndexRef.current);
34
+ break;
35
+ case "Enter":
36
+ event.preventDefault();
37
+ if (focusedIndexRef.current >= 0 && focusedIndexRef.current < items.length) {
38
+ onSelect(items[focusedIndexRef.current]);
39
+ }
40
+ break;
41
+ case "Escape":
42
+ event.preventDefault();
43
+ closeDropdown();
44
+ break;
45
+ }
46
+ };
47
+ const resetFocus = () => {
48
+ focusedIndexRef.current = -1;
49
+ setFocusedIndex(-1);
50
+ };
51
+ return {
52
+ handleKeyDown,
53
+ resetFocus,
54
+ focusedIndex
55
+ };
56
+ }
57
+ function useClickOutside(dropdownRef, closeDropdown, isOpen) {
58
+ const closeDropdownRef = useRef(closeDropdown);
59
+ closeDropdownRef.current = closeDropdown;
60
+ const handleClickOutside = useCallback(
61
+ (event) => {
62
+ if (!dropdownRef.current) return;
63
+ const target = event.target;
64
+ if (!dropdownRef.current.contains(target)) {
65
+ closeDropdownRef.current();
66
+ }
67
+ },
68
+ [dropdownRef]
69
+ );
70
+ useEffect(() => {
71
+ if (!isOpen) return;
72
+ document.addEventListener("mousedown", handleClickOutside, { passive: true });
73
+ document.addEventListener("touchstart", handleClickOutside, { passive: true });
74
+ return () => {
75
+ document.removeEventListener("mousedown", handleClickOutside);
76
+ document.removeEventListener("touchstart", handleClickOutside);
77
+ };
78
+ }, [isOpen, handleClickOutside]);
79
+ }
80
+ function DropdownRoot({
81
+ children,
82
+ items,
83
+ selectedItem: initialSelectedItem = null,
84
+ onSelect,
85
+ getItemKey,
86
+ getItemDisplay,
87
+ filterItems,
88
+ disabled = false,
89
+ placeholder,
90
+ className = "",
91
+ dropdownPlacement = "bottom",
92
+ getItemDescription,
93
+ getItemIcon,
94
+ getItemSection,
95
+ getItemSeparator,
96
+ getItemDisabled,
97
+ getItemClassName,
98
+ closeOnSelect = true,
99
+ onOpenChange,
100
+ triggerRef,
101
+ usePortal = false,
102
+ "data-testid": testId = "dropdown-root"
103
+ }) {
104
+ const [isOpen, setIsOpen] = useState(false);
105
+ const [selectedItem, setSelectedItem] = useState(initialSelectedItem);
106
+ const [searchQuery, setSearchQuery] = useState("");
107
+ const dropdownRef = useRef(null);
108
+ const inputRef = useRef(null);
109
+ const defaultFilter = useCallback(
110
+ (items2, query) => {
111
+ if (!query.trim()) {
112
+ return items2;
113
+ }
114
+ const normalizedQuery = query.toLowerCase().trim();
115
+ return items2.filter((item) => getItemDisplay(item).toLowerCase().includes(normalizedQuery));
116
+ },
117
+ [getItemDisplay]
118
+ );
119
+ const filterFunction = filterItems || defaultFilter;
120
+ const filteredItems = useMemo(() => {
121
+ return filterFunction(items, searchQuery);
122
+ }, [items, searchQuery, filterFunction]);
123
+ const openDropdown = useCallback(() => {
124
+ if (disabled) return;
125
+ setIsOpen(true);
126
+ onOpenChange?.(true);
127
+ }, [disabled, onOpenChange]);
128
+ useLayoutEffect(() => {
129
+ if (isOpen && inputRef.current) {
130
+ inputRef.current.focus();
131
+ }
132
+ }, [isOpen]);
133
+ const closeDropdown = useCallback(() => {
134
+ setIsOpen(false);
135
+ setSearchQuery("");
136
+ onOpenChange?.(false);
137
+ }, [onOpenChange]);
138
+ const toggleDropdown = useCallback(() => {
139
+ isOpen ? closeDropdown() : openDropdown();
140
+ }, [isOpen, openDropdown, closeDropdown]);
141
+ const handleSelect = useCallback(
142
+ (item) => {
143
+ setSelectedItem(item);
144
+ onSelect(item);
145
+ if (closeOnSelect) {
146
+ closeDropdown();
147
+ }
148
+ },
149
+ [onSelect, closeDropdown, closeOnSelect]
150
+ );
151
+ const handleSearchChange = useCallback((query) => {
152
+ setSearchQuery(query);
153
+ }, []);
154
+ useClickOutside(dropdownRef, closeDropdown, isOpen);
155
+ const contextValue = useMemo(
156
+ () => ({
157
+ isOpen,
158
+ setIsOpen,
159
+ selectedItem,
160
+ setSelectedItem,
161
+ searchQuery,
162
+ setSearchQuery: handleSearchChange,
163
+ items,
164
+ filteredItems,
165
+ getItemKey,
166
+ getItemDisplay,
167
+ filterItems: filterFunction,
168
+ onSelect: handleSelect,
169
+ disabled,
170
+ closeOnSelect,
171
+ closeDropdown,
172
+ toggleDropdown,
173
+ dropdownPlacement,
174
+ getItemDescription,
175
+ getItemIcon,
176
+ getItemSection,
177
+ getItemSeparator,
178
+ getItemDisabled,
179
+ getItemClassName,
180
+ triggerRef,
181
+ usePortal
182
+ }),
183
+ [
184
+ isOpen,
185
+ setIsOpen,
186
+ selectedItem,
187
+ setSelectedItem,
188
+ searchQuery,
189
+ handleSearchChange,
190
+ items,
191
+ filteredItems,
192
+ getItemKey,
193
+ getItemDisplay,
194
+ filterFunction,
195
+ handleSelect,
196
+ disabled,
197
+ closeOnSelect,
198
+ closeDropdown,
199
+ toggleDropdown,
200
+ dropdownPlacement,
201
+ getItemDescription,
202
+ getItemIcon,
203
+ getItemSection,
204
+ getItemSeparator,
205
+ getItemDisabled,
206
+ getItemClassName,
207
+ triggerRef,
208
+ usePortal
209
+ ]
210
+ );
211
+ return /* @__PURE__ */ jsx(DropdownProvider, { value: contextValue, children: /* @__PURE__ */ jsx("div", { ref: dropdownRef, className: `relative ${className}`, "data-testid": testId, children }) });
212
+ }
213
+ function DropdownTrigger({
214
+ displayValue,
215
+ placeholder = "Select an option",
216
+ className = "",
217
+ "data-testid": testId = "dropdown-trigger"
218
+ }) {
219
+ const { isOpen, toggleDropdown, disabled } = useDropdownContext();
220
+ const buildTriggerClassName = () => {
221
+ const baseClasses = "w-full flex items-center justify-between px-3 py-2 text-left border border-gray-300 rounded-lg bg-white transition-colors";
222
+ const interactiveClasses = "focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-[#0B4F75] focus-visible:border-transparent hover:border-gray-400 cursor-pointer";
223
+ const disabledClasses = "opacity-50 cursor-not-allowed";
224
+ return `${baseClasses} ${disabled ? disabledClasses : interactiveClasses}`;
225
+ };
226
+ const buildTextClassName = () => {
227
+ return displayValue ? "text-gray-900" : "text-gray-400";
228
+ };
229
+ return /* @__PURE__ */ jsxs(
230
+ "button",
231
+ {
232
+ type: "button",
233
+ onClick: toggleDropdown,
234
+ disabled,
235
+ className: `${buildTriggerClassName()} ${className}`,
236
+ "aria-haspopup": "listbox",
237
+ "aria-expanded": isOpen,
238
+ "aria-label": displayValue ? void 0 : placeholder || "Select an option",
239
+ "data-testid": testId,
240
+ children: [
241
+ /* @__PURE__ */ jsx("span", { className: buildTextClassName(), children: displayValue || placeholder }),
242
+ /* @__PURE__ */ jsx(DropdownArrowIcon, { isOpen })
243
+ ]
244
+ }
245
+ );
246
+ }
247
+ function DropdownArrowIcon({ isOpen }) {
248
+ return /* @__PURE__ */ jsx(
249
+ "svg",
250
+ {
251
+ className: `size-4 transition-transform ${isOpen ? "rotate-180" : ""}`,
252
+ fill: "none",
253
+ stroke: "currentColor",
254
+ viewBox: "0 0 24 24",
255
+ "aria-hidden": "true",
256
+ children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" })
257
+ }
258
+ );
259
+ }
260
+
261
+ // src/design-tokens.ts
262
+ var ELEVATED_SHADOW = "0px 0px 0px 1px rgba(0,0,0,0.03), 0px 1px 1px 0px rgba(0,0,0,0.03), 0px 2px 2px 0px rgba(0,0,0,0.03), 0px 4px 4px 0px rgba(0,0,0,0.03), 0px 8px 8px 0px rgba(0,0,0,0.03), 0px 16px 16px 0px rgba(0,0,0,0.03)";
263
+ var EXIT_ANIMATION_DURATION_MS = 150;
264
+ function DropdownContent({
265
+ children,
266
+ className = "",
267
+ disableAnimation = false,
268
+ "data-testid": testId = "dropdown-content"
269
+ }) {
270
+ const { isOpen, dropdownPlacement, triggerRef, usePortal } = useDropdownContext();
271
+ const [shouldRender, setShouldRender] = useState(isOpen);
272
+ const [isClosing, setIsClosing] = useState(false);
273
+ const [portalPosition, setPortalPosition] = useState({ top: 0, right: 0 });
274
+ const prevIsOpen = useRef(isOpen);
275
+ useEffect(() => {
276
+ if (usePortal && isOpen && triggerRef?.current) {
277
+ const rect = triggerRef.current.getBoundingClientRect();
278
+ setPortalPosition({
279
+ top: rect.bottom + 4,
280
+ // 4px gap below trigger
281
+ right: window.innerWidth - rect.right
282
+ // Align dropdown's right edge with trigger's right edge
283
+ });
284
+ }
285
+ }, [isOpen, usePortal, triggerRef]);
286
+ useEffect(() => {
287
+ if (isOpen) {
288
+ setShouldRender(true);
289
+ setIsClosing(false);
290
+ } else if (prevIsOpen.current && !isOpen) {
291
+ if (disableAnimation) {
292
+ setShouldRender(false);
293
+ } else {
294
+ setIsClosing(true);
295
+ const timer = setTimeout(() => {
296
+ setShouldRender(false);
297
+ setIsClosing(false);
298
+ }, EXIT_ANIMATION_DURATION_MS);
299
+ return () => clearTimeout(timer);
300
+ }
301
+ }
302
+ prevIsOpen.current = isOpen;
303
+ }, [isOpen, disableAnimation]);
304
+ const placementClass = dropdownPlacement === "top" ? "bottom-full mb-1" : "mt-1";
305
+ const flexDirClass = dropdownPlacement === "top" ? "flex-col-reverse" : "flex-col";
306
+ const transformOrigin = dropdownPlacement === "top" ? "bottom center" : "top center";
307
+ const variants = {
308
+ initial: {
309
+ opacity: 0,
310
+ scale: 0.95,
311
+ y: dropdownPlacement === "top" ? 8 : -8
312
+ },
313
+ animate: {
314
+ opacity: 1,
315
+ scale: 1,
316
+ y: 0,
317
+ boxShadow: ELEVATED_SHADOW
318
+ },
319
+ exit: {
320
+ opacity: 0,
321
+ scale: 0.95,
322
+ y: dropdownPlacement === "top" ? 8 : -8
323
+ }
324
+ };
325
+ const content = /* @__PURE__ */ jsx(AnimatePresence, { children: shouldRender && /* @__PURE__ */ jsx(
326
+ motion.div,
327
+ {
328
+ className: `${usePortal ? "fixed" : "absolute"} z-50 ${usePortal ? "" : "w-full"} ${usePortal ? "" : "min-w-[320px]!"} ${usePortal ? "" : placementClass} bg-white border border-gray-200 rounded-lg flex ${flexDirClass} overflow-hidden ${className}`,
329
+ style: usePortal ? { top: portalPosition.top, right: portalPosition.right, transformOrigin } : { transformOrigin },
330
+ initial: disableAnimation ? false : "initial",
331
+ animate: "animate",
332
+ exit: disableAnimation ? void 0 : "exit",
333
+ variants,
334
+ transition: {
335
+ duration: 0.15,
336
+ ease: [0.16, 1, 0.3, 1]
337
+ // Custom ease for smooth feel
338
+ },
339
+ "data-testid": testId,
340
+ children
341
+ }
342
+ ) });
343
+ if (usePortal && typeof document !== "undefined") {
344
+ return createPortal(content, document.body);
345
+ }
346
+ return content;
347
+ }
348
+ var DropdownSearch = ({
349
+ value,
350
+ onChange,
351
+ inputRef,
352
+ placeholder = "Search...",
353
+ className = "",
354
+ "data-testid": testId = "dropdown-search"
355
+ }) => {
356
+ const { disabled } = useDropdownContext();
357
+ const handleKeyDown = (event) => {
358
+ if (event.key === "Tab") {
359
+ event.preventDefault();
360
+ }
361
+ };
362
+ return /* @__PURE__ */ jsx("div", { className: `px-2 py-1 border-b border-gray-200 shrink-0 ${className}`, children: /* @__PURE__ */ jsx(
363
+ "input",
364
+ {
365
+ ref: inputRef,
366
+ type: "text",
367
+ value,
368
+ onChange: (e) => onChange(e.target.value),
369
+ onKeyDown: handleKeyDown,
370
+ placeholder,
371
+ disabled,
372
+ className: "w-full px-2.5 py-1.5 text-sm border border-gray-300 rounded focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-[#0B4F75] focus-visible:border-transparent",
373
+ tabIndex: -1,
374
+ "data-testid": testId,
375
+ "aria-label": "Search options"
376
+ }
377
+ ) });
378
+ };
379
+ var groupItemsBySection = (items, resolveSection) => {
380
+ if (!resolveSection) {
381
+ return { sections: [], ungrouped: items };
382
+ }
383
+ const sectionIndex = /* @__PURE__ */ new Map();
384
+ const sections = [];
385
+ const ungrouped = [];
386
+ items.forEach((item) => {
387
+ const section = resolveSection(item);
388
+ if (!section) {
389
+ ungrouped.push(item);
390
+ return;
391
+ }
392
+ const existingIndex = sectionIndex.get(section.key);
393
+ if (existingIndex === void 0) {
394
+ sectionIndex.set(section.key, sections.length);
395
+ sections.push({ meta: section, items: [item] });
396
+ return;
397
+ }
398
+ sections[existingIndex].items.push(item);
399
+ });
400
+ return { sections, ungrouped };
401
+ };
402
+ function DropdownList({
403
+ items,
404
+ onSelect: customOnSelect,
405
+ hasResults,
406
+ selectedItem,
407
+ getItemKey,
408
+ getItemDisplay,
409
+ renderItem,
410
+ getItemDescription,
411
+ getItemIcon,
412
+ getItemSection,
413
+ getItemSeparator,
414
+ getItemDisabled,
415
+ getItemClassName,
416
+ className = "",
417
+ "data-testid": testId = "dropdown-list"
418
+ }) {
419
+ const {
420
+ isOpen,
421
+ getItemDescription: contextDescription,
422
+ getItemIcon: contextIcon,
423
+ getItemSection: contextSection,
424
+ getItemSeparator: contextSeparator,
425
+ getItemDisabled: contextDisabled,
426
+ getItemClassName: contextClassName,
427
+ onSelect: contextOnSelect,
428
+ closeDropdown,
429
+ closeOnSelect
430
+ } = useDropdownContext();
431
+ const listRef = useRef(null);
432
+ const prevIsOpenRef = useRef(isOpen);
433
+ useEffect(() => {
434
+ if (isOpen && !prevIsOpenRef.current && listRef.current && selectedItem) {
435
+ const selectedElement = listRef.current.querySelector(
436
+ `[data-key="${getItemKey(selectedItem)}"]`
437
+ );
438
+ if (selectedElement) {
439
+ selectedElement.scrollIntoView({
440
+ behavior: "auto",
441
+ block: "start"
442
+ });
443
+ }
444
+ }
445
+ prevIsOpenRef.current = isOpen;
446
+ }, [isOpen, selectedItem, getItemKey]);
447
+ const descriptionAccessor = getItemDescription ?? contextDescription;
448
+ const iconAccessor = getItemIcon ?? contextIcon;
449
+ const sectionAccessor = getItemSection ?? contextSection;
450
+ const separatorAccessor = getItemSeparator ?? contextSeparator;
451
+ const disabledAccessor = getItemDisabled ?? contextDisabled;
452
+ const classNameAccessor = getItemClassName ?? contextClassName;
453
+ const groupedItems = useMemo(
454
+ () => groupItemsBySection(items, sectionAccessor),
455
+ [items, sectionAccessor]
456
+ );
457
+ const resolveDescription = (item) => {
458
+ if (!descriptionAccessor) {
459
+ return null;
460
+ }
461
+ return descriptionAccessor(item) ?? null;
462
+ };
463
+ const resolveIcon = (item) => {
464
+ if (!iconAccessor) {
465
+ return void 0;
466
+ }
467
+ return iconAccessor(item);
468
+ };
469
+ const renderSectionHeader = (section) => {
470
+ return /* @__PURE__ */ jsx(
471
+ "li",
472
+ {
473
+ role: "presentation",
474
+ className: "px-3 py-2 bg-gray-50 text-xs font-semibold uppercase tracking-wide text-gray-500",
475
+ children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
476
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
477
+ section.meta.icon && /* @__PURE__ */ jsx("span", { className: "text-base text-gray-500", "aria-hidden": true, children: section.meta.icon }),
478
+ /* @__PURE__ */ jsx("span", { children: section.meta.label })
479
+ ] }),
480
+ section.meta.description && /* @__PURE__ */ jsx("span", { className: "text-[11px] font-normal normal-case text-gray-400", children: section.meta.description })
481
+ ] })
482
+ },
483
+ `section-${section.meta.key}`
484
+ );
485
+ };
486
+ const resolvedOnSelect = useMemo(() => {
487
+ const selectHandler = customOnSelect || contextOnSelect;
488
+ return (item) => {
489
+ selectHandler(item);
490
+ if (closeOnSelect) {
491
+ closeDropdown();
492
+ }
493
+ };
494
+ }, [customOnSelect, contextOnSelect, closeOnSelect, closeDropdown]);
495
+ const renderOption = (item) => {
496
+ const key = getItemKey(item);
497
+ const displayText = getItemDisplay(item);
498
+ const isSelected = selectedItem ? getItemKey(selectedItem) === key : false;
499
+ const isDisabled = disabledAccessor ? disabledAccessor(item) : false;
500
+ const customClassName = classNameAccessor ? classNameAccessor(item, isSelected, isDisabled) : "";
501
+ if (renderItem) {
502
+ return /* @__PURE__ */ jsx("li", { "data-key": key, children: renderItem(item, isSelected, resolvedOnSelect) }, key);
503
+ }
504
+ return /* @__PURE__ */ jsx("li", { "data-key": key, children: /* @__PURE__ */ jsx(
505
+ DropdownOption,
506
+ {
507
+ dataKey: key,
508
+ item,
509
+ onSelect: resolvedOnSelect,
510
+ isSelected,
511
+ displayText,
512
+ description: resolveDescription(item),
513
+ icon: resolveIcon(item),
514
+ isDisabled,
515
+ className: customClassName
516
+ }
517
+ ) }, key);
518
+ };
519
+ if (!hasResults) {
520
+ return /* @__PURE__ */ jsx("div", { className: `p-4 text-center text-sm text-gray-500 ${className}`, children: "No results found" });
521
+ }
522
+ const renderedItems = [];
523
+ groupedItems.ungrouped.forEach((item) => {
524
+ const originalIndex = items.findIndex((i) => getItemKey(i) === getItemKey(item));
525
+ renderedItems.push(renderOption(item));
526
+ if (separatorAccessor && originalIndex >= 0 && separatorAccessor(item, originalIndex)) {
527
+ renderedItems.push(
528
+ /* @__PURE__ */ jsx(
529
+ "li",
530
+ {
531
+ role: "separator",
532
+ className: "border-b border-gray-200 my-1"
533
+ },
534
+ `separator-${originalIndex}`
535
+ )
536
+ );
537
+ }
538
+ });
539
+ groupedItems.sections.forEach((section) => {
540
+ renderedItems.push(renderSectionHeader(section));
541
+ section.items.forEach((item) => {
542
+ const originalIndex = items.findIndex((i) => getItemKey(i) === getItemKey(item));
543
+ renderedItems.push(renderOption(item));
544
+ if (separatorAccessor && originalIndex >= 0 && separatorAccessor(item, originalIndex)) {
545
+ renderedItems.push(
546
+ /* @__PURE__ */ jsx(
547
+ "li",
548
+ {
549
+ role: "separator",
550
+ className: "border-b border-gray-200 my-1"
551
+ },
552
+ `separator-${originalIndex}`
553
+ )
554
+ );
555
+ }
556
+ });
557
+ });
558
+ return /* @__PURE__ */ jsx(
559
+ "ul",
560
+ {
561
+ ref: listRef,
562
+ className: `overflow-y-auto flex-1 min-h-0 flex flex-col no-scrollbar ${className}`,
563
+ role: "listbox",
564
+ "data-testid": testId,
565
+ children: renderedItems
566
+ }
567
+ );
568
+ }
569
+ function DropdownOption({
570
+ item,
571
+ onSelect,
572
+ isSelected,
573
+ displayText,
574
+ dataKey,
575
+ description,
576
+ icon,
577
+ isDisabled = false,
578
+ className = ""
579
+ }) {
580
+ const [isHovered, setIsHovered] = useState(false);
581
+ const handleClick = () => {
582
+ if (!isDisabled) {
583
+ onSelect(item);
584
+ }
585
+ };
586
+ const baseClasses = "px-3 py-1.5 text-sm transition-colors";
587
+ const selectedClasses = isSelected ? "bg-blue-50 text-blue-600 font-medium" : "";
588
+ const hasCustomHover = className.includes("hover:");
589
+ const disabledClasses = isDisabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer";
590
+ const classNameWithoutHover = className.replace(/hover:[^\s]+/g, "").trim();
591
+ const combinedClasses = `${baseClasses} ${selectedClasses} ${disabledClasses} ${classNameWithoutHover}`.trim();
592
+ const hoverBgMatch = className.match(/hover:!?bg-\[([^\]]+)\]/);
593
+ const hoverBgColor = hoverBgMatch ? hoverBgMatch[1] : hasCustomHover ? "#fee2e2" : "#f3f4f6";
594
+ return /* @__PURE__ */ jsx(
595
+ "div",
596
+ {
597
+ "data-key": dataKey,
598
+ onClick: handleClick,
599
+ className: combinedClasses,
600
+ style: isHovered && !isDisabled ? { backgroundColor: hoverBgColor } : void 0,
601
+ role: "option",
602
+ "aria-selected": isSelected,
603
+ "aria-disabled": isDisabled,
604
+ onMouseEnter: () => setIsHovered(true),
605
+ onMouseLeave: () => setIsHovered(false),
606
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
607
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col flex-1", children: [
608
+ /* @__PURE__ */ jsx("span", { children: displayText }),
609
+ description && /* @__PURE__ */ jsx("span", { className: "text-xs font-normal text-gray-500", children: description })
610
+ ] }),
611
+ icon && /* @__PURE__ */ jsx("span", { className: "text-base", "aria-hidden": true, children: icon })
612
+ ] })
613
+ }
614
+ );
615
+ }
616
+ function DropdownSearchable({
617
+ searchPlaceholder = "Search...",
618
+ className = "",
619
+ "data-testid": testId = "dropdown-searchable",
620
+ hideSearchThreshold
621
+ }) {
622
+ const {
623
+ searchQuery,
624
+ setSearchQuery,
625
+ filteredItems,
626
+ selectedItem,
627
+ getItemKey,
628
+ getItemDisplay,
629
+ getItemDescription,
630
+ getItemIcon,
631
+ getItemSection,
632
+ items
633
+ } = useDropdownContext();
634
+ const shouldShowSearch = hideSearchThreshold === void 0 || items.length > hideSearchThreshold;
635
+ return /* @__PURE__ */ jsxs(DropdownContent, { className, "data-testid": testId, children: [
636
+ shouldShowSearch && /* @__PURE__ */ jsx(
637
+ DropdownSearch,
638
+ {
639
+ value: searchQuery,
640
+ onChange: setSearchQuery,
641
+ placeholder: searchPlaceholder
642
+ }
643
+ ),
644
+ /* @__PURE__ */ jsx(
645
+ DropdownList,
646
+ {
647
+ items: filteredItems,
648
+ hasResults: filteredItems.length > 0,
649
+ selectedItem,
650
+ getItemKey,
651
+ getItemDisplay,
652
+ getItemDescription,
653
+ getItemIcon,
654
+ getItemSection
655
+ }
656
+ )
657
+ ] });
658
+ }
659
+ function DropdownSimple({
660
+ className = "",
661
+ "data-testid": testId = "dropdown-simple"
662
+ }) {
663
+ const {
664
+ filteredItems,
665
+ selectedItem,
666
+ getItemKey,
667
+ getItemDisplay,
668
+ getItemDescription,
669
+ getItemIcon,
670
+ getItemSection
671
+ } = useDropdownContext();
672
+ return /* @__PURE__ */ jsx(DropdownContent, { className, "data-testid": testId, children: /* @__PURE__ */ jsx(
673
+ DropdownList,
674
+ {
675
+ items: filteredItems,
676
+ hasResults: filteredItems.length > 0,
677
+ selectedItem,
678
+ getItemKey,
679
+ getItemDisplay,
680
+ getItemDescription,
681
+ getItemIcon,
682
+ getItemSection
683
+ }
684
+ ) });
685
+ }
686
+ var MenuTrigger = React2.forwardRef(
687
+ ({ children }, ref) => {
688
+ const { isOpen, toggleDropdown } = useDropdownContext();
689
+ return /* @__PURE__ */ jsx("div", { ref, onClick: toggleDropdown, "aria-expanded": isOpen, "aria-haspopup": "menu", children });
690
+ }
691
+ );
692
+ MenuTrigger.displayName = "MenuTrigger";
693
+ function DropdownMenu({
694
+ items,
695
+ onSelect,
696
+ getItemKey,
697
+ getItemDisplay,
698
+ getItemIcon,
699
+ getItemDescription,
700
+ getItemSeparator,
701
+ getItemDisabled,
702
+ getItemClassName,
703
+ getItemSection,
704
+ filterItems,
705
+ disabled = false,
706
+ placeholder,
707
+ className = "",
708
+ dropdownPlacement = "bottom",
709
+ closeOnSelect = true,
710
+ trigger,
711
+ contentClassName = "",
712
+ listClassName = "",
713
+ onOpenChange,
714
+ usePortal = false,
715
+ "data-testid": testId = "dropdown-menu"
716
+ }) {
717
+ const triggerRef = useRef(null);
718
+ return /* @__PURE__ */ jsxs(
719
+ DropdownRoot,
720
+ {
721
+ items,
722
+ selectedItem: null,
723
+ onSelect,
724
+ getItemKey,
725
+ getItemDisplay,
726
+ getItemIcon,
727
+ getItemDescription,
728
+ getItemSeparator,
729
+ getItemDisabled,
730
+ getItemClassName,
731
+ getItemSection,
732
+ filterItems,
733
+ disabled,
734
+ placeholder,
735
+ className,
736
+ dropdownPlacement,
737
+ closeOnSelect,
738
+ onOpenChange,
739
+ triggerRef,
740
+ usePortal,
741
+ "data-testid": testId,
742
+ children: [
743
+ /* @__PURE__ */ jsx(MenuTrigger, { ref: triggerRef, children: trigger }),
744
+ /* @__PURE__ */ jsx(DropdownContent, { className: contentClassName, children: /* @__PURE__ */ jsx(
745
+ DropdownList,
746
+ {
747
+ items,
748
+ hasResults: items.length > 0,
749
+ selectedItem: null,
750
+ getItemKey,
751
+ getItemDisplay,
752
+ getItemIcon,
753
+ getItemDescription,
754
+ getItemSeparator,
755
+ getItemDisabled,
756
+ getItemClassName,
757
+ getItemSection,
758
+ className: listClassName
759
+ }
760
+ ) })
761
+ ]
762
+ }
763
+ );
764
+ }
765
+
766
+ // src/index.ts
767
+ var Dropdown = {
768
+ Root: DropdownRoot,
769
+ Trigger: DropdownTrigger,
770
+ Content: DropdownContent,
771
+ Search: DropdownSearch,
772
+ List: DropdownList,
773
+ Simple: DropdownSimple,
774
+ Searchable: DropdownSearchable,
775
+ Menu: DropdownMenu
776
+ };
777
+ var index_default = Dropdown;
778
+
779
+ export { DropdownContent, DropdownList, DropdownMenu, DropdownProvider, DropdownRoot, DropdownSearch, DropdownSearchable, DropdownSimple, DropdownTrigger, index_default as default, useClickOutside, useDropdownContext, useKeyboardNavigation };