@hilum/ui 3.3.4 → 3.5.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.
Files changed (66) hide show
  1. package/README.md +4 -0
  2. package/dist/{chunk-MAHPAP65.js → chunk-RI6QJR2L.js} +6 -3
  3. package/dist/chunk-RI6QJR2L.js.map +1 -0
  4. package/dist/index.d.ts +800 -122
  5. package/dist/index.js +10458 -1119
  6. package/dist/index.js.map +1 -1
  7. package/dist/tokens.css +17 -0
  8. package/dist/tokens.d.ts +3 -0
  9. package/dist/tokens.js +1 -1
  10. package/llms.txt +18 -4
  11. package/package.json +19 -1
  12. package/src/components/accordion.tsx +746 -58
  13. package/src/components/alert-dialog.tsx +11 -5
  14. package/src/components/ask-user-questions.tsx +1366 -0
  15. package/src/components/badge.tsx +105 -24
  16. package/src/components/button.tsx +207 -38
  17. package/src/components/card.tsx +3 -2
  18. package/src/components/chat-message.tsx +116 -0
  19. package/src/components/checkbox-group.tsx +403 -0
  20. package/src/components/checkbox.tsx +6 -3
  21. package/src/components/color-input.tsx +3 -1
  22. package/src/components/color-picker.tsx +1992 -80
  23. package/src/components/dialog.tsx +141 -81
  24. package/src/components/dropdown-menu.tsx +21 -13
  25. package/src/components/dropdown.tsx +245 -0
  26. package/src/components/file-thumbnail.tsx +156 -0
  27. package/src/components/hover-card.tsx +2 -0
  28. package/src/components/input-copy.tsx +307 -0
  29. package/src/components/input-group.tsx +303 -108
  30. package/src/components/input-message.tsx +883 -0
  31. package/src/components/input-number.tsx +62 -21
  32. package/src/components/input.tsx +6 -3
  33. package/src/components/menu-item.tsx +156 -0
  34. package/src/components/mobile-drawer.tsx +50 -0
  35. package/src/components/nav-item.tsx +43 -0
  36. package/src/components/nav-menu.tsx +47 -0
  37. package/src/components/popover.tsx +3 -1
  38. package/src/components/radio-group.tsx +381 -35
  39. package/src/components/scroll-area.tsx +9 -2
  40. package/src/components/select.tsx +708 -121
  41. package/src/components/sheet.tsx +15 -6
  42. package/src/components/slider.tsx +1583 -20
  43. package/src/components/status-badge.tsx +2 -0
  44. package/src/components/switch.tsx +245 -28
  45. package/src/components/table.tsx +191 -77
  46. package/src/components/tabs-subtle.tsx +411 -0
  47. package/src/components/tabs.tsx +459 -49
  48. package/src/components/textarea.tsx +7 -3
  49. package/src/components/thinking-indicator.tsx +106 -0
  50. package/src/components/thinking-steps.tsx +382 -0
  51. package/src/components/tooltip.tsx +158 -33
  52. package/src/hooks/use-merge-split.tsx +359 -0
  53. package/src/hooks/use-proximity-hover.ts +208 -0
  54. package/src/index.ts +14 -0
  55. package/src/lib/elevated.tsx +52 -0
  56. package/src/lib/font-weight.ts +18 -0
  57. package/src/lib/icon-context.tsx +98 -0
  58. package/src/lib/icon-map.tsx +669 -0
  59. package/src/lib/interaction.ts +102 -0
  60. package/src/lib/scroll-fade.tsx +241 -0
  61. package/src/lib/shape-context.tsx +121 -0
  62. package/src/lib/springs.ts +30 -0
  63. package/src/lib/surface-classes.ts +43 -0
  64. package/src/lib/surface-context.tsx +17 -0
  65. package/src/tokens/tokens.ts +3 -0
  66. package/dist/chunk-MAHPAP65.js.map +0 -1
@@ -1,110 +1,170 @@
1
1
  "use client";
2
2
 
3
- import * as React from "react";
4
- import { Dialog } from "radix-ui";
5
- import { X } from "lucide-react";
6
- import { cn } from "../lib/utils";
7
3
  import {
8
- desktopDialogContentClassName,
9
- dialogSheetMotionClassName,
10
- mobileDialogSheetContentClassName,
11
- } from "../lib/mobile-popper-sheet";
12
-
13
- const DialogRoot = Dialog.Root;
14
- const DialogTrigger = Dialog.Trigger;
15
- const DialogClose = Dialog.Close;
16
-
17
- const DialogOverlay = React.forwardRef<
18
- React.ComponentRef<typeof Dialog.Overlay>,
19
- React.ComponentPropsWithoutRef<typeof Dialog.Overlay>
20
- >(({ className, ...props }, ref) => (
21
- <Dialog.Overlay
22
- ref={ref}
23
- className={cn(
24
- "fixed inset-0 z-50 bg-black/30 backdrop-blur-sm",
25
- "data-[state=open]:animate-in data-[state=closed]:animate-out",
26
- "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
27
- className,
28
- )}
29
- {...props}
30
- />
31
- ));
32
- DialogOverlay.displayName = "DialogOverlay";
33
-
34
- const DialogContent = React.forwardRef<
35
- React.ComponentRef<typeof Dialog.Content>,
36
- React.ComponentPropsWithoutRef<typeof Dialog.Content>
37
- >(({ className, children, ...props }, ref) => (
38
- <Dialog.Portal>
39
- <DialogOverlay />
40
- <Dialog.Content
41
- ref={ref}
42
- className={cn(
43
- mobileDialogSheetContentClassName,
44
- desktopDialogContentClassName,
45
- "sm:max-w-lg",
46
- dialogSheetMotionClassName,
47
- className,
48
- )}
49
- {...props}
50
- >
51
- {children}
52
- <Dialog.Close className="absolute right-3 top-3 flex size-9 items-center justify-center rounded-md text-muted-foreground opacity-70 transition-[opacity,scale] hover:opacity-100 active:scale-[0.96] focus:outline-none focus:ring-2 focus:ring-ground-900/20">
53
- <X size={16} />
54
- <span className="sr-only">Close</span>
55
- </Dialog.Close>
56
- </Dialog.Content>
57
- </Dialog.Portal>
58
- ));
4
+ createContext,
5
+ forwardRef,
6
+ useContext,
7
+ useEffect,
8
+ useState,
9
+ type ComponentPropsWithoutRef,
10
+ type HTMLAttributes,
11
+ } from "react";
12
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
13
+ import { motion } from "framer-motion";
14
+ import { cn } from "../lib/utils";
15
+ import { useIcon } from "../lib/icon-context";
16
+ import { spring } from "../lib/springs";
17
+ import { useShape } from "../lib/shape-context";
18
+ import { SurfaceProvider, useSurface } from "../lib/surface-context";
19
+ import { surfaceClasses } from "../lib/surface-classes";
20
+ import { Button } from "./button";
21
+
22
+ const DIALOG_OFFSET = 4;
23
+
24
+ const DialogOpenContext = createContext(false);
25
+
26
+ function Dialog({
27
+ children,
28
+ open: controlledOpen,
29
+ defaultOpen = false,
30
+ onOpenChange,
31
+ ...props
32
+ }: DialogPrimitive.DialogProps) {
33
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
34
+ const open = controlledOpen ?? uncontrolledOpen;
35
+ const handleOpenChange = onOpenChange ?? setUncontrolledOpen;
36
+
37
+ return (
38
+ <DialogOpenContext.Provider value={open}>
39
+ <DialogPrimitive.Root open={open} onOpenChange={handleOpenChange} {...props}>
40
+ {children}
41
+ </DialogPrimitive.Root>
42
+ </DialogOpenContext.Provider>
43
+ );
44
+ }
45
+
46
+ const DialogTrigger = DialogPrimitive.Trigger;
47
+ const DialogClose = DialogPrimitive.Close;
48
+
49
+ interface DialogContentProps extends ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
50
+ size?: "sm" | "lg";
51
+ /** Portal target. When set, the overlay and panel render inside this element
52
+ * (positioned `absolute`) instead of covering the viewport (`fixed`). Pair
53
+ * with a `position: relative; overflow: hidden` container — and usually
54
+ * `<Dialog modal={false}>` — to scope a dialog to a bounded region, e.g. a
55
+ * docs preview. Defaults to the document body / full-viewport behaviour. */
56
+ container?: HTMLElement | null;
57
+ }
58
+
59
+ const DialogContent = forwardRef<HTMLDivElement, DialogContentProps>(
60
+ ({ className, children, size = "sm", container, ...props }, ref) => {
61
+ const XIcon = useIcon("x");
62
+ const open = useContext(DialogOpenContext);
63
+ const shape = useShape();
64
+ const substrate = useSurface();
65
+ const dialogLevel = Math.min(substrate + DIALOG_OFFSET, 8);
66
+ const [mounted, setMounted] = useState(false);
67
+
68
+ useEffect(() => {
69
+ if (open) setMounted(true);
70
+ }, [open]);
71
+
72
+ const handleExitComplete = () => {
73
+ if (!open) setMounted(false);
74
+ };
75
+
76
+ if (!mounted) return null;
77
+
78
+ return (
79
+ <DialogPrimitive.Portal forceMount container={container ?? undefined}>
80
+ <DialogPrimitive.Overlay asChild forceMount>
81
+ <motion.div
82
+ className={cn(
83
+ container ? "absolute" : "fixed",
84
+ "inset-0 z-50 bg-black/40 dark:bg-black/80",
85
+ )}
86
+ initial={{ opacity: 0 }}
87
+ animate={{ opacity: open ? 1 : 0 }}
88
+ transition={open ? spring.slow : spring.slow.exit}
89
+ />
90
+ </DialogPrimitive.Overlay>
91
+ <DialogPrimitive.Content ref={ref} asChild forceMount {...props}>
92
+ <motion.div
93
+ className={cn(
94
+ container ? "absolute" : "fixed",
95
+ "left-1/2 top-1/2 z-50 w-[calc(100%-2rem)]",
96
+ surfaceClasses(dialogLevel),
97
+ "p-6 focus:outline-none",
98
+ "rounded-t-2xl max-sm:pb-[calc(1.5rem+env(safe-area-inset-bottom))] data-[state=open]:slide-in-from-bottom sm:data-[state=open]:zoom-in-95",
99
+ size === "sm" && "max-w-[400px]",
100
+ size === "lg" && "max-w-[540px]",
101
+ shape.container,
102
+ "rounded-t-2xl",
103
+ className,
104
+ )}
105
+ initial={{ opacity: 0, scale: 0.97, x: "-50%", y: "-50%" }}
106
+ animate={{
107
+ opacity: open ? 1 : 0,
108
+ scale: open ? 1 : 0.97,
109
+ x: "-50%",
110
+ y: "-50%",
111
+ }}
112
+ transition={open ? spring.slow : spring.slow.exit}
113
+ onAnimationComplete={handleExitComplete}
114
+ >
115
+ <SurfaceProvider value={dialogLevel}>
116
+ {children}
117
+ <DialogPrimitive.Close asChild>
118
+ <Button variant="ghost" size="icon-sm" className="absolute right-3 top-3">
119
+ <XIcon />
120
+ <span className="sr-only">Close</span>
121
+ </Button>
122
+ </DialogPrimitive.Close>
123
+ </SurfaceProvider>
124
+ </motion.div>
125
+ </DialogPrimitive.Content>
126
+ </DialogPrimitive.Portal>
127
+ );
128
+ },
129
+ );
59
130
  DialogContent.displayName = "DialogContent";
60
131
 
61
- function DialogHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
132
+ function DialogHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
62
133
  return <div className={cn("flex flex-col gap-1.5 mb-4", className)} {...props} />;
63
134
  }
64
- DialogHeader.displayName = "DialogHeader";
65
135
 
66
- function DialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
67
- return (
68
- <div
69
- className={cn(
70
- "mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:items-center sm:justify-end",
71
- "[&>*]:w-full sm:[&>*]:w-auto",
72
- className,
73
- )}
74
- {...props}
75
- />
76
- );
136
+ function DialogFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
137
+ return <div className={cn("flex justify-end gap-2 mt-6", className)} {...props} />;
77
138
  }
78
- DialogFooter.displayName = "DialogFooter";
79
139
 
80
- const DialogTitle = React.forwardRef<
81
- React.ComponentRef<typeof Dialog.Title>,
82
- React.ComponentPropsWithoutRef<typeof Dialog.Title>
140
+ const DialogTitle = forwardRef<
141
+ HTMLHeadingElement,
142
+ ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
83
143
  >(({ className, ...props }, ref) => (
84
- <Dialog.Title
144
+ <DialogPrimitive.Title
85
145
  ref={ref}
86
- className={cn("body-lg font-semibold text-balance text-foreground", className)}
146
+ className={cn("text-[16px] text-foreground leading-tight", className)}
147
+ style={{ fontVariationSettings: "'wght' 700" }}
87
148
  {...props}
88
149
  />
89
150
  ));
90
151
  DialogTitle.displayName = "DialogTitle";
91
152
 
92
- const DialogDescription = React.forwardRef<
93
- React.ComponentRef<typeof Dialog.Description>,
94
- React.ComponentPropsWithoutRef<typeof Dialog.Description>
153
+ const DialogDescription = forwardRef<
154
+ HTMLParagraphElement,
155
+ ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
95
156
  >(({ className, ...props }, ref) => (
96
- <Dialog.Description
157
+ <DialogPrimitive.Description
97
158
  ref={ref}
98
- className={cn("body text-pretty text-muted-foreground", className)}
159
+ className={cn("text-[13px] text-muted-foreground", className)}
99
160
  {...props}
100
161
  />
101
162
  ));
102
163
  DialogDescription.displayName = "DialogDescription";
103
164
 
104
165
  export {
105
- DialogRoot as Dialog,
166
+ Dialog,
106
167
  DialogTrigger,
107
- DialogOverlay,
108
168
  DialogContent,
109
169
  DialogHeader,
110
170
  DialogFooter,
@@ -4,6 +4,12 @@ import * as React from "react";
4
4
  import { DropdownMenu } from "radix-ui";
5
5
  import { Check, ChevronRight, Circle } from "lucide-react";
6
6
  import { cn } from "../lib/utils";
7
+ import {
8
+ menuItemActiveClasses,
9
+ menuItemClasses,
10
+ motionClasses,
11
+ pressClasses,
12
+ } from "../lib/interaction";
7
13
  import {
8
14
  mobilePopperSheetMotionClassName,
9
15
  mobilePopperSheetPositionClassName,
@@ -30,7 +36,7 @@ const DropdownMenuContent = React.forwardRef<
30
36
  data-hilum-mobile-sheet="true"
31
37
  sideOffset={sideOffset}
32
38
  className={cn(
33
- "z-50 min-w-[8rem] bg-card rounded-xl shadow-natural p-1",
39
+ "z-50 min-w-[8rem] rounded-xl border border-border bg-card p-1 shadow-elevated",
34
40
  mobilePopperSheetPositionClassName,
35
41
  mobilePopperSheetSurfaceClassName,
36
42
  "max-sm:overflow-y-auto max-sm:px-2 max-sm:pb-2 max-sm:pt-5",
@@ -59,11 +65,13 @@ const DropdownMenuItem = React.forwardRef<
59
65
  <DropdownMenu.Item
60
66
  ref={ref}
61
67
  className={cn(
62
- "relative flex min-h-10 cursor-default select-none items-center gap-2 rounded-md px-2.5 py-2",
63
- "body outline-none transition-colors",
68
+ menuItemClasses,
69
+ motionClasses,
70
+ pressClasses,
64
71
  destructive
65
72
  ? "text-destructive focus:bg-destructive/10 focus:text-destructive"
66
73
  : "text-muted-foreground focus:bg-muted focus:text-foreground",
74
+ !destructive && menuItemActiveClasses,
67
75
  "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
68
76
  inset && "pl-8",
69
77
  className,
@@ -80,9 +88,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
80
88
  <DropdownMenu.CheckboxItem
81
89
  ref={ref}
82
90
  className={cn(
83
- "relative flex min-h-10 cursor-default select-none items-center gap-2 rounded-md py-2 pl-8 pr-2.5",
84
- "body text-muted-foreground outline-none transition-colors",
85
- "focus:bg-muted focus:text-foreground",
91
+ menuItemClasses,
92
+ menuItemActiveClasses,
93
+ "py-2 pl-8 pr-2.5 text-muted-foreground",
86
94
  "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
87
95
  className,
88
96
  )}
@@ -105,9 +113,9 @@ const DropdownMenuRadioItem = React.forwardRef<
105
113
  <DropdownMenu.RadioItem
106
114
  ref={ref}
107
115
  className={cn(
108
- "relative flex min-h-10 cursor-default select-none items-center gap-2 rounded-md py-2 pl-8 pr-2.5",
109
- "body text-muted-foreground outline-none transition-colors",
110
- "focus:bg-muted focus:text-foreground",
116
+ menuItemClasses,
117
+ menuItemActiveClasses,
118
+ "py-2 pl-8 pr-2.5 text-muted-foreground",
111
119
  "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
112
120
  className,
113
121
  )}
@@ -158,9 +166,9 @@ const DropdownMenuSubTrigger = React.forwardRef<
158
166
  <DropdownMenu.SubTrigger
159
167
  ref={ref}
160
168
  className={cn(
161
- "relative flex min-h-10 cursor-default select-none items-center gap-2 rounded-md px-2.5 py-2",
162
- "body text-muted-foreground outline-none transition-colors",
163
- "focus:bg-muted focus:text-foreground data-[state=open]:bg-muted",
169
+ menuItemClasses,
170
+ menuItemActiveClasses,
171
+ "text-muted-foreground data-[state=open]:bg-muted",
164
172
  inset && "pl-8",
165
173
  className,
166
174
  )}
@@ -182,7 +190,7 @@ const DropdownMenuSubContent = React.forwardRef<
182
190
  ref={ref}
183
191
  data-hilum-mobile-sheet="true"
184
192
  className={cn(
185
- "z-50 min-w-[8rem] bg-card rounded-xl border border-border shadow-natural p-1",
193
+ "z-50 min-w-[8rem] rounded-xl border border-border bg-card p-1 shadow-elevated",
186
194
  mobilePopperSheetPositionClassName,
187
195
  mobilePopperSheetSurfaceClassName,
188
196
  "max-sm:overflow-y-auto max-sm:px-2 max-sm:pb-2 max-sm:pt-5",
@@ -0,0 +1,245 @@
1
+ "use client";
2
+
3
+ import {
4
+ useRef,
5
+ useState,
6
+ useEffect,
7
+ createContext,
8
+ useContext,
9
+ forwardRef,
10
+ type ReactNode,
11
+ type HTMLAttributes,
12
+ } from "react";
13
+ import { motion, AnimatePresence } from "framer-motion";
14
+ import { cn } from "../lib/utils";
15
+ import { spring } from "../lib/springs";
16
+ import { useProximityHover } from "../hooks/use-proximity-hover";
17
+ import { shapeMap } from "../lib/shape-context";
18
+ import { Elevated } from "../lib/elevated";
19
+
20
+ // Dropdown opts out of the global pill/rounded shape context — popover surfaces
21
+ // look cleaner with the smaller "rounded" radii regardless of how the rest of
22
+ // the UI is shaped (the heavy pill bubbling distorts perceived padding at this
23
+ // scale and produces the corner-shadow asymmetry).
24
+ const shape = shapeMap.rounded;
25
+
26
+ interface DropdownContextValue {
27
+ registerItem: (index: number, element: HTMLElement | null) => void;
28
+ activeIndex: number | null;
29
+ checkedIndex?: number;
30
+ }
31
+
32
+ const DropdownContext = createContext<DropdownContextValue | null>(null);
33
+
34
+ export function useDropdown() {
35
+ const ctx = useContext(DropdownContext);
36
+ return ctx ?? { registerItem: () => undefined, activeIndex: null };
37
+ }
38
+
39
+ interface DropdownProps extends HTMLAttributes<HTMLDivElement> {
40
+ children: ReactNode;
41
+ checkedIndex?: number;
42
+ }
43
+
44
+ const Dropdown = forwardRef<HTMLDivElement, DropdownProps>(
45
+ ({ children, checkedIndex, className, ...props }, ref) => {
46
+ const containerRef = useRef<HTMLDivElement>(null);
47
+ const {
48
+ activeIndex,
49
+ setActiveIndex,
50
+ itemRects,
51
+ sessionRef,
52
+ handlers,
53
+ registerItem,
54
+ measureItems,
55
+ } = useProximityHover(containerRef);
56
+
57
+ useEffect(() => {
58
+ measureItems();
59
+ }, [measureItems, children]);
60
+
61
+ const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
62
+
63
+ const activeRect = activeIndex !== null ? itemRects[activeIndex] : null;
64
+ const checkedRect = checkedIndex != null ? itemRects[checkedIndex] : null;
65
+ const focusRect = focusedIndex !== null ? itemRects[focusedIndex] : null;
66
+ const isHoveringOther = activeIndex !== null && activeIndex !== checkedIndex;
67
+
68
+ return (
69
+ <DropdownContext.Provider
70
+ value={
71
+ checkedIndex === undefined
72
+ ? { registerItem, activeIndex }
73
+ : { registerItem, activeIndex, checkedIndex }
74
+ }
75
+ >
76
+ <Elevated
77
+ offset={2}
78
+ shadowLevel={3}
79
+ ref={(node) => {
80
+ (containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
81
+ if (typeof ref === "function") ref(node);
82
+ else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
83
+ }}
84
+ onMouseEnter={handlers.onMouseEnter}
85
+ onMouseMove={handlers.onMouseMove}
86
+ onMouseLeave={handlers.onMouseLeave}
87
+ onFocus={(e) => {
88
+ const indexAttr = (e.target as HTMLElement)
89
+ .closest("[data-proximity-index]")
90
+ ?.getAttribute("data-proximity-index");
91
+ if (indexAttr != null) {
92
+ const idx = Number(indexAttr);
93
+ setActiveIndex(idx);
94
+ setFocusedIndex((e.target as HTMLElement).matches(":focus-visible") ? idx : null);
95
+ }
96
+ }}
97
+ onBlur={(e) => {
98
+ if (containerRef.current?.contains(e.relatedTarget as Node)) return;
99
+ setFocusedIndex(null);
100
+ setActiveIndex(null);
101
+ }}
102
+ onKeyDown={(e) => {
103
+ const items = Array.from(
104
+ containerRef.current?.querySelectorAll('[role="menuitemradio"]') ?? [],
105
+ ) as HTMLElement[];
106
+ const currentIdx = items.indexOf(e.target as HTMLElement);
107
+ if (currentIdx === -1) return;
108
+
109
+ if (["ArrowDown", "ArrowUp", "ArrowRight", "ArrowLeft"].includes(e.key)) {
110
+ e.preventDefault();
111
+ const next = ["ArrowDown", "ArrowRight"].includes(e.key)
112
+ ? (currentIdx + 1) % items.length
113
+ : (currentIdx - 1 + items.length) % items.length;
114
+ items[next].focus();
115
+ } else if (e.key === "Home") {
116
+ e.preventDefault();
117
+ items[0]?.focus();
118
+ } else if (e.key === "End") {
119
+ e.preventDefault();
120
+ items[items.length - 1]?.focus();
121
+ }
122
+ }}
123
+ role="menu"
124
+ className={cn(
125
+ `relative flex flex-col gap-0.5 w-72 max-w-full ${shape.container} p-1 select-none`,
126
+ className,
127
+ )}
128
+ {...props}
129
+ >
130
+ {/* Selected background */}
131
+ <AnimatePresence>
132
+ {checkedRect && (
133
+ <motion.div
134
+ className={`absolute ${shape.bg} bg-active pointer-events-none`}
135
+ initial={false}
136
+ animate={{
137
+ top: checkedRect.top,
138
+ left: checkedRect.left,
139
+ width: checkedRect.width,
140
+ height: checkedRect.height,
141
+ opacity: isHoveringOther ? 0.8 : 1,
142
+ }}
143
+ exit={{ opacity: 0, transition: spring.moderate.exit }}
144
+ transition={{
145
+ ...spring.moderate,
146
+ opacity: { duration: 0.08 },
147
+ }}
148
+ />
149
+ )}
150
+ </AnimatePresence>
151
+
152
+ {/* Hover background */}
153
+ <AnimatePresence>
154
+ {activeRect && (
155
+ <motion.div
156
+ key={sessionRef.current}
157
+ className={`absolute ${shape.bg} bg-hover pointer-events-none`}
158
+ initial={{
159
+ opacity: 0,
160
+ top: checkedRect?.top ?? activeRect.top,
161
+ left: checkedRect?.left ?? activeRect.left,
162
+ width: checkedRect?.width ?? activeRect.width,
163
+ height: checkedRect?.height ?? activeRect.height,
164
+ }}
165
+ animate={{
166
+ opacity: 1,
167
+ top: activeRect.top,
168
+ left: activeRect.left,
169
+ width: activeRect.width,
170
+ height: activeRect.height,
171
+ }}
172
+ exit={{ opacity: 0, transition: spring.fast.exit }}
173
+ transition={{
174
+ ...spring.fast,
175
+ opacity: { duration: 0.08 },
176
+ }}
177
+ />
178
+ )}
179
+ </AnimatePresence>
180
+
181
+ {/* Focus ring */}
182
+ <AnimatePresence>
183
+ {focusRect && (
184
+ <motion.div
185
+ className={`absolute ${shape.focusRing} pointer-events-none z-20 border border-[#6B97FF]`}
186
+ initial={false}
187
+ animate={{
188
+ left: focusRect.left - 2,
189
+ top: focusRect.top - 2,
190
+ width: focusRect.width + 4,
191
+ height: focusRect.height + 4,
192
+ }}
193
+ exit={{ opacity: 0, transition: spring.fast.exit }}
194
+ transition={{
195
+ ...spring.fast,
196
+ opacity: { duration: 0.08 },
197
+ }}
198
+ />
199
+ )}
200
+ </AnimatePresence>
201
+
202
+ {children}
203
+ </Elevated>
204
+ </DropdownContext.Provider>
205
+ );
206
+ },
207
+ );
208
+
209
+ Dropdown.displayName = "Dropdown";
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // DropdownLabel
213
+ // ---------------------------------------------------------------------------
214
+
215
+ const DropdownLabel = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
216
+ ({ className, ...props }, ref) => (
217
+ <div
218
+ ref={ref}
219
+ className={cn("px-2 py-1.5 text-[11px] text-muted-foreground", className)}
220
+ {...props}
221
+ />
222
+ ),
223
+ );
224
+
225
+ DropdownLabel.displayName = "DropdownLabel";
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // DropdownSeparator
229
+ // ---------------------------------------------------------------------------
230
+
231
+ const DropdownSeparator = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
232
+ ({ className, ...props }, ref) => (
233
+ <div
234
+ ref={ref}
235
+ role="separator"
236
+ className={cn("my-1 -mx-1 h-px bg-border/60", className)}
237
+ {...props}
238
+ />
239
+ ),
240
+ );
241
+
242
+ DropdownSeparator.displayName = "DropdownSeparator";
243
+
244
+ export { Dropdown, DropdownLabel, DropdownSeparator };
245
+ export default Dropdown;