@srcroot/ui 0.0.2 → 0.0.4

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.
@@ -4,6 +4,7 @@ import { cn } from "@/lib/utils"
4
4
  interface DropdownMenuContextValue {
5
5
  open: boolean
6
6
  onOpenChange: (open: boolean) => void
7
+ triggerRef: React.RefObject<HTMLButtonElement | null>
7
8
  }
8
9
 
9
10
  const DropdownMenuContext = React.createContext<DropdownMenuContextValue | null>(null)
@@ -16,7 +17,7 @@ interface DropdownMenuProps {
16
17
  }
17
18
 
18
19
  /**
19
- * DropdownMenu component with keyboard navigation
20
+ * DropdownMenu component with keyboard navigation and proper positioning
20
21
  *
21
22
  * @example
22
23
  * <DropdownMenu>
@@ -33,12 +34,13 @@ interface DropdownMenuProps {
33
34
  */
34
35
  function DropdownMenu({ children, open: controlledOpen, onOpenChange, defaultOpen = false }: DropdownMenuProps) {
35
36
  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
37
+ const triggerRef = React.useRef<HTMLButtonElement>(null)
36
38
 
37
39
  const open = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen
38
40
  const setOpen = onOpenChange || setUncontrolledOpen
39
41
 
40
42
  return (
41
- <DropdownMenuContext.Provider value={{ open, onOpenChange: setOpen }}>
43
+ <DropdownMenuContext.Provider value={{ open, onOpenChange: setOpen, triggerRef }}>
42
44
  <div className="relative inline-block text-left">
43
45
  {children}
44
46
  </div>
@@ -60,18 +62,25 @@ const DropdownMenuTrigger = React.forwardRef<HTMLButtonElement, DropdownMenuTrig
60
62
  context.onOpenChange(!context.open)
61
63
  }
62
64
 
65
+ // Combine refs
66
+ const combinedRef = (node: HTMLButtonElement | null) => {
67
+ (context.triggerRef as React.MutableRefObject<HTMLButtonElement | null>).current = node
68
+ if (typeof ref === 'function') ref(node)
69
+ else if (ref) ref.current = node
70
+ }
71
+
63
72
  if (asChild && React.isValidElement(children)) {
64
73
  return React.cloneElement(children as React.ReactElement<any>, {
65
74
  onClick: handleClick,
66
75
  "aria-expanded": context.open,
67
76
  "aria-haspopup": "menu",
68
- ref,
77
+ ref: combinedRef,
69
78
  })
70
79
  }
71
80
 
72
81
  return (
73
82
  <button
74
- ref={ref}
83
+ ref={combinedRef}
75
84
  aria-expanded={context.open}
76
85
  aria-haspopup="menu"
77
86
  onClick={handleClick}
@@ -84,53 +93,94 @@ const DropdownMenuTrigger = React.forwardRef<HTMLButtonElement, DropdownMenuTrig
84
93
  )
85
94
  DropdownMenuTrigger.displayName = "DropdownMenuTrigger"
86
95
 
87
- const DropdownMenuContent = React.forwardRef<
88
- HTMLDivElement,
89
- React.HTMLAttributes<HTMLDivElement>
90
- >(({ className, ...props }, ref) => {
91
- const context = React.useContext(DropdownMenuContext)
92
- if (!context) throw new Error("DropdownMenuContent must be used within DropdownMenu")
96
+ interface DropdownMenuContentProps extends React.HTMLAttributes<HTMLDivElement> {
97
+ /** Alignment relative to trigger: 'start' | 'center' | 'end' */
98
+ align?: 'start' | 'center' | 'end'
99
+ /** Side of trigger to open: 'bottom' | 'top' */
100
+ side?: 'bottom' | 'top'
101
+ /** Offset from trigger in pixels */
102
+ sideOffset?: number
103
+ }
93
104
 
94
- React.useEffect(() => {
95
- const handleClickOutside = () => {
96
- if (context.open) {
97
- context.onOpenChange(false)
105
+ const DropdownMenuContent = React.forwardRef<HTMLDivElement, DropdownMenuContentProps>(
106
+ ({ className, align = 'start', side = 'bottom', sideOffset = 4, ...props }, ref) => {
107
+ const context = React.useContext(DropdownMenuContext)
108
+ if (!context) throw new Error("DropdownMenuContent must be used within DropdownMenu")
109
+ const contentRef = React.useRef<HTMLDivElement>(null)
110
+
111
+ React.useEffect(() => {
112
+ const handleClickOutside = (e: MouseEvent) => {
113
+ if (context.open) {
114
+ const target = e.target as Node
115
+ const content = contentRef.current
116
+ const trigger = context.triggerRef.current
117
+
118
+ // Don't close if clicking inside content or trigger
119
+ if (content?.contains(target) || trigger?.contains(target)) {
120
+ return
121
+ }
122
+ context.onOpenChange(false)
123
+ }
98
124
  }
99
- }
100
125
 
101
- const handleEscape = (e: KeyboardEvent) => {
102
- if (e.key === "Escape" && context.open) {
103
- context.onOpenChange(false)
126
+ const handleEscape = (e: KeyboardEvent) => {
127
+ if (e.key === "Escape" && context.open) {
128
+ context.onOpenChange(false)
129
+ }
104
130
  }
105
- }
106
131
 
107
- const timer = setTimeout(() => {
108
- document.addEventListener("click", handleClickOutside)
109
- }, 0)
110
- document.addEventListener("keydown", handleEscape)
132
+ const timer = setTimeout(() => {
133
+ document.addEventListener("click", handleClickOutside)
134
+ }, 0)
135
+ document.addEventListener("keydown", handleEscape)
136
+
137
+ return () => {
138
+ clearTimeout(timer)
139
+ document.removeEventListener("click", handleClickOutside)
140
+ document.removeEventListener("keydown", handleEscape)
141
+ }
142
+ }, [context.open, context])
143
+
144
+ if (!context.open) return null
111
145
 
112
- return () => {
113
- clearTimeout(timer)
114
- document.removeEventListener("click", handleClickOutside)
115
- document.removeEventListener("keydown", handleEscape)
146
+ // Calculate alignment classes
147
+ const alignmentClasses = {
148
+ start: 'left-0',
149
+ center: 'left-1/2 -translate-x-1/2',
150
+ end: 'right-0',
116
151
  }
117
- }, [context.open, context])
118
152
 
119
- if (!context.open) return null
153
+ // Calculate side classes
154
+ const sideClasses = {
155
+ bottom: `top-full mt-${sideOffset}`,
156
+ top: `bottom-full mb-${sideOffset}`,
157
+ }
120
158
 
121
- return (
122
- <div
123
- ref={ref}
124
- role="menu"
125
- className={cn(
126
- "absolute right-0 z-50 mt-2 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
127
- className
128
- )}
129
- onClick={(e) => e.stopPropagation()}
130
- {...props}
131
- />
132
- )
133
- })
159
+ return (
160
+ <div
161
+ ref={(node) => {
162
+ (contentRef as React.MutableRefObject<HTMLDivElement | null>).current = node
163
+ if (typeof ref === 'function') ref(node)
164
+ else if (ref) ref.current = node
165
+ }}
166
+ role="menu"
167
+ aria-orientation="vertical"
168
+ className={cn(
169
+ "absolute z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
170
+ "animate-in fade-in-0 zoom-in-95",
171
+ alignmentClasses[align],
172
+ side === 'bottom' ? 'top-full' : 'bottom-full',
173
+ className
174
+ )}
175
+ style={{
176
+ marginTop: side === 'bottom' ? sideOffset : undefined,
177
+ marginBottom: side === 'top' ? sideOffset : undefined,
178
+ }}
179
+ {...props}
180
+ />
181
+ )
182
+ }
183
+ )
134
184
  DropdownMenuContent.displayName = "DropdownMenuContent"
135
185
 
136
186
  const DropdownMenuItem = React.forwardRef<
@@ -188,8 +238,6 @@ const DropdownMenuCheckboxItem = React.forwardRef<
188
238
  HTMLDivElement,
189
239
  React.HTMLAttributes<HTMLDivElement> & { checked?: boolean; disabled?: boolean }
190
240
  >(({ className, children, checked, disabled, onClick, ...props }, ref) => {
191
- const context = React.useContext(DropdownMenuContext)
192
-
193
241
  const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
194
242
  if (disabled) return
195
243
  onClick?.(e)
@@ -6,7 +6,7 @@ import { cn } from "@/lib/utils"
6
6
  // HoverCard Context
7
7
  interface HoverCardContextValue {
8
8
  open: boolean
9
- triggerRef: React.RefObject<HTMLDivElement>
9
+ triggerRef: React.RefObject<HTMLDivElement | null>
10
10
  }
11
11
 
12
12
  const HoverCardContext = React.createContext<HoverCardContextValue | null>(null)
@@ -69,8 +69,8 @@ const Image = React.forwardRef<HTMLImageElement, ImageProps>(
69
69
  }, [src])
70
70
 
71
71
  const containerStyle: React.CSSProperties = aspectRatio
72
- ? { paddingBottom: `${100 / aspectRatio}%`, ...style }
73
- : style
72
+ ? { paddingBottom: `${100 / aspectRatio}%`, ...(style || {}) }
73
+ : (style || {})
74
74
 
75
75
  // Render fallback
76
76
  if (status === "error" && fallback) {
@@ -22,7 +22,7 @@ function useMenubar() {
22
22
  // MenubarMenu Context
23
23
  interface MenubarMenuContextValue {
24
24
  menuId: string
25
- triggerRef: React.RefObject<HTMLButtonElement>
25
+ triggerRef: React.RefObject<HTMLButtonElement | null>
26
26
  }
27
27
 
28
28
  const MenubarMenuContext = React.createContext<MenubarMenuContextValue | null>(null)
@@ -5,10 +5,12 @@ import { cn } from "@/lib/utils"
5
5
 
6
6
  // Resizable Context
7
7
  interface ResizablePanelGroupContextValue {
8
+ groupId: string
8
9
  direction: "horizontal" | "vertical"
9
- sizes: number[]
10
- setSizes: React.Dispatch<React.SetStateAction<number[]>>
11
- registerPanel: () => number
10
+ registerPanel: (defaultSize: number, minSize: number, maxSize: number) => number
11
+ getSize: (index: number) => number
12
+ getTotalSize: () => number
13
+ onResize: (handleIndex: number, delta: number) => void
12
14
  }
13
15
 
14
16
  const ResizablePanelGroupContext = React.createContext<ResizablePanelGroupContextValue | null>(null)
@@ -27,29 +29,115 @@ interface ResizablePanelGroupProps extends React.HTMLAttributes<HTMLDivElement>
27
29
  onLayout?: (sizes: number[]) => void
28
30
  }
29
31
 
32
+ interface PanelConfig {
33
+ defaultSize: number
34
+ minSize: number
35
+ maxSize: number
36
+ }
37
+
30
38
  const ResizablePanelGroup = React.forwardRef<HTMLDivElement, ResizablePanelGroupProps>(
31
39
  ({ className, direction = "horizontal", children, onLayout, ...props }, ref) => {
32
- const [sizes, setSizes] = React.useState<number[]>([])
40
+ const groupId = React.useId()
41
+ const containerRef = React.useRef<HTMLDivElement>(null)
42
+ const [, forceUpdate] = React.useReducer(x => x + 1, 0)
43
+
44
+ // Store panel configs and sizes in refs for stable access
45
+ const panelConfigsRef = React.useRef<PanelConfig[]>([])
46
+ const sizesRef = React.useRef<number[]>([])
33
47
  const panelCountRef = React.useRef(0)
34
48
 
35
- const registerPanel = React.useCallback(() => {
36
- const index = panelCountRef.current
37
- panelCountRef.current += 1
49
+ // Reset panel count at start of each render
50
+ panelCountRef.current = 0
51
+
52
+ const registerPanel = React.useCallback((defaultSize: number, minSize: number, maxSize: number) => {
53
+ const index = panelCountRef.current++
54
+
55
+ // Only initialize if not already set
56
+ if (sizesRef.current[index] === undefined) {
57
+ sizesRef.current[index] = defaultSize
58
+ panelConfigsRef.current[index] = { defaultSize, minSize, maxSize }
59
+ }
60
+
38
61
  return index
39
62
  }, [])
40
63
 
41
- // Notify layout changes
42
- React.useEffect(() => {
43
- if (sizes.length > 0) {
44
- onLayout?.(sizes)
64
+ const getSize = React.useCallback((index: number) => {
65
+ return sizesRef.current[index] ?? 50
66
+ }, [])
67
+
68
+ const getTotalSize = React.useCallback(() => {
69
+ return sizesRef.current.reduce((sum, s) => sum + s, 0)
70
+ }, [])
71
+
72
+ const onResize = React.useCallback((handleIndex: number, delta: number) => {
73
+ const container = containerRef.current
74
+ if (!container) return
75
+
76
+ const containerSize = direction === "horizontal"
77
+ ? container.offsetWidth
78
+ : container.offsetHeight
79
+
80
+ if (containerSize === 0) return
81
+
82
+ const deltaPercent = (delta / containerSize) * 100
83
+
84
+ const leftIndex = handleIndex
85
+ const rightIndex = handleIndex + 1
86
+ const sizes = sizesRef.current
87
+ const configs = panelConfigsRef.current
88
+
89
+ if (leftIndex >= sizes.length || rightIndex >= sizes.length) return
90
+
91
+ const leftSize = sizes[leftIndex]
92
+ const rightSize = sizes[rightIndex]
93
+ const totalSize = leftSize + rightSize
94
+
95
+ let newLeftSize = leftSize + deltaPercent
96
+ let newRightSize = rightSize - deltaPercent
97
+
98
+ const leftMin = configs[leftIndex]?.minSize ?? 10
99
+ const leftMax = configs[leftIndex]?.maxSize ?? 90
100
+ const rightMin = configs[rightIndex]?.minSize ?? 10
101
+ const rightMax = configs[rightIndex]?.maxSize ?? 90
102
+
103
+ // Apply constraints
104
+ if (newLeftSize < leftMin) {
105
+ newLeftSize = leftMin
106
+ newRightSize = totalSize - leftMin
45
107
  }
46
- }, [sizes, onLayout])
108
+ if (newRightSize < rightMin) {
109
+ newRightSize = rightMin
110
+ newLeftSize = totalSize - rightMin
111
+ }
112
+ if (newLeftSize > leftMax) {
113
+ newLeftSize = leftMax
114
+ newRightSize = totalSize - leftMax
115
+ }
116
+ if (newRightSize > rightMax) {
117
+ newRightSize = rightMax
118
+ newLeftSize = totalSize - rightMax
119
+ }
120
+
121
+ sizesRef.current[leftIndex] = newLeftSize
122
+ sizesRef.current[rightIndex] = newRightSize
123
+
124
+ // Force re-render to update panel sizes
125
+ forceUpdate()
126
+
127
+ // Notify layout changes
128
+ onLayout?.([...sizesRef.current])
129
+ }, [direction, onLayout])
47
130
 
48
131
  return (
49
- <ResizablePanelGroupContext.Provider value={{ direction, sizes, setSizes, registerPanel }}>
132
+ <ResizablePanelGroupContext.Provider value={{ groupId, direction, registerPanel, getSize, getTotalSize, onResize }}>
50
133
  <div
51
- ref={ref}
134
+ ref={(node) => {
135
+ containerRef.current = node
136
+ if (typeof ref === "function") ref(node)
137
+ else if (ref) ref.current = node
138
+ }}
52
139
  data-panel-group
140
+ data-panel-group-id={groupId}
53
141
  data-direction={direction}
54
142
  className={cn(
55
143
  "flex h-full w-full",
@@ -75,33 +163,30 @@ interface ResizablePanelProps extends React.HTMLAttributes<HTMLDivElement> {
75
163
 
76
164
  const ResizablePanel = React.forwardRef<HTMLDivElement, ResizablePanelProps>(
77
165
  ({ className, defaultSize = 50, minSize = 10, maxSize = 90, children, style, ...props }, ref) => {
78
- const { direction, sizes, setSizes, registerPanel } = useResizablePanelGroup()
79
- const indexRef = React.useRef<number | null>(null)
80
-
81
- React.useEffect(() => {
82
- if (indexRef.current === null) {
83
- indexRef.current = registerPanel()
84
- setSizes(prev => {
85
- const newSizes = [...prev]
86
- newSizes[indexRef.current!] = defaultSize
87
- return newSizes
88
- })
89
- }
90
- }, [registerPanel, setSizes, defaultSize])
166
+ const { groupId, direction, registerPanel, getSize, getTotalSize } = useResizablePanelGroup()
167
+ const indexRef = React.useRef<number>(-1)
91
168
 
92
- const size = indexRef.current !== null ? sizes[indexRef.current] ?? defaultSize : defaultSize
169
+ // Register panel on first render only
170
+ if (indexRef.current === -1) {
171
+ indexRef.current = registerPanel(defaultSize, minSize, maxSize)
172
+ }
173
+
174
+ const size = getSize(indexRef.current)
175
+ const totalSize = getTotalSize()
176
+ // Convert to actual percentage of total
177
+ // const actualPercent = totalSize > 0 ? (size / totalSize) * 100 : size
93
178
 
94
179
  return (
95
180
  <div
96
181
  ref={ref}
97
182
  data-panel
183
+ data-panel-group-id={groupId}
98
184
  data-panel-index={indexRef.current}
99
185
  className={cn("overflow-hidden", className)}
100
186
  style={{
101
187
  ...style,
102
- [direction === "horizontal" ? "width" : "height"]: `${size}%`,
103
- flexShrink: 0,
104
- flexGrow: 0,
188
+ // Use flex-grow with the size as the ratio for smoother resizing
189
+ flex: `${size} 1 0`,
105
190
  }}
106
191
  {...props}
107
192
  >
@@ -119,62 +204,78 @@ interface ResizableHandleProps extends React.HTMLAttributes<HTMLDivElement> {
119
204
 
120
205
  const ResizableHandle = React.forwardRef<HTMLDivElement, ResizableHandleProps>(
121
206
  ({ className, withHandle = false, ...props }, ref) => {
122
- const { direction, sizes, setSizes } = useResizablePanelGroup()
207
+ const { direction, onResize } = useResizablePanelGroup()
123
208
  const [isDragging, setIsDragging] = React.useState(false)
209
+ const handleIndexRef = React.useRef<number>(-1)
210
+ const lastPosRef = React.useRef<number>(0)
124
211
  const handleRef = React.useRef<HTMLDivElement>(null)
125
212
 
126
- const handleMouseDown = (e: React.MouseEvent) => {
127
- e.preventDefault()
128
- setIsDragging(true)
213
+ // Determine handle index from DOM position
214
+ const getHandleIndex = React.useCallback(() => {
215
+ const handle = handleRef.current
216
+ if (!handle) return 0
129
217
 
130
- const startX = e.clientX
131
- const startY = e.clientY
132
- const startSizes = [...sizes]
133
- const container = handleRef.current?.parentElement
218
+ const parent = handle.parentElement
219
+ if (!parent) return 0
134
220
 
135
- if (!container) return
221
+ let handleCount = 0
222
+ for (const child of Array.from(parent.children)) {
223
+ if (child === handle) break
224
+ if (child.hasAttribute('data-panel-resize-handle')) {
225
+ handleCount++
226
+ }
227
+ }
228
+ return handleCount
229
+ }, [])
230
+
231
+ const handleMouseDown = React.useCallback((e: React.MouseEvent) => {
232
+ e.preventDefault()
233
+ e.stopPropagation()
234
+ setIsDragging(true)
136
235
 
137
- const containerRect = container.getBoundingClientRect()
138
- const containerSize = direction === "horizontal" ? containerRect.width : containerRect.height
236
+ const handleIndex = getHandleIndex()
237
+ const startPos = direction === "horizontal" ? e.clientX : e.clientY
238
+ lastPosRef.current = startPos
139
239
 
140
240
  const handleMouseMove = (moveEvent: MouseEvent) => {
141
- const delta = direction === "horizontal"
142
- ? moveEvent.clientX - startX
143
- : moveEvent.clientY - startY
144
- const deltaPercent = (delta / containerSize) * 100
145
-
146
- setSizes(prev => {
147
- const newSizes = [...prev]
148
- // Adjust first two panels (simple implementation)
149
- if (newSizes.length >= 2) {
150
- newSizes[0] = Math.max(10, Math.min(90, startSizes[0] + deltaPercent))
151
- newSizes[1] = Math.max(10, Math.min(90, startSizes[1] - deltaPercent))
152
- }
153
- return newSizes
154
- })
241
+ const currentPos = direction === "horizontal" ? moveEvent.clientX : moveEvent.clientY
242
+ const delta = currentPos - lastPosRef.current
243
+ lastPosRef.current = currentPos
244
+
245
+ if (delta !== 0) {
246
+ onResize(handleIndex, delta)
247
+ }
155
248
  }
156
249
 
157
250
  const handleMouseUp = () => {
158
251
  setIsDragging(false)
159
252
  document.removeEventListener("mousemove", handleMouseMove)
160
253
  document.removeEventListener("mouseup", handleMouseUp)
254
+ document.body.style.cursor = ""
255
+ document.body.style.userSelect = ""
161
256
  }
162
257
 
258
+ document.body.style.cursor = direction === "horizontal" ? "col-resize" : "row-resize"
259
+ document.body.style.userSelect = "none"
163
260
  document.addEventListener("mousemove", handleMouseMove)
164
261
  document.addEventListener("mouseup", handleMouseUp)
165
- }
262
+ }, [direction, onResize, getHandleIndex])
166
263
 
167
264
  return (
168
265
  <div
169
- ref={handleRef}
266
+ ref={(node) => {
267
+ handleRef.current = node
268
+ if (typeof ref === "function") ref(node)
269
+ else if (ref) ref.current = node
270
+ }}
170
271
  data-panel-resize-handle
272
+ data-dragging={isDragging}
171
273
  className={cn(
172
- "relative flex items-center justify-center bg-border",
274
+ "relative flex shrink-0 items-center justify-center bg-border",
173
275
  direction === "horizontal"
174
- ? "w-px cursor-col-resize hover:w-1 hover:bg-primary/50"
175
- : "h-px cursor-row-resize hover:h-1 hover:bg-primary/50",
176
- isDragging && (direction === "horizontal" ? "w-1 bg-primary" : "h-1 bg-primary"),
177
- "transition-all",
276
+ ? "w-1 cursor-col-resize hover:bg-primary/50 active:bg-primary"
277
+ : "h-1 cursor-row-resize hover:bg-primary/50 active:bg-primary",
278
+ isDragging && "bg-primary",
178
279
  className
179
280
  )}
180
281
  onMouseDown={handleMouseDown}
@@ -3,17 +3,47 @@ import { cn } from "@/lib/utils"
3
3
 
4
4
  interface ScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {
5
5
  /** Orientation of scrollbar */
6
- orientation?: "vertical" | "horizontal" | "both"
6
+ orientation?: "vertical" | "horizontal"
7
+ /** Scrollbar size: "thin" (4px), "default" (8px), "thick" (12px) */
8
+ scrollbarSize?: "thin" | "default" | "thick"
9
+ /** Hide scrollbar until hover */
10
+ hideScrollbar?: boolean
11
+ }
12
+
13
+ // CSS for custom scrollbar styling
14
+ const scrollbarStyles = {
15
+ thin: {
16
+ width: "4px",
17
+ height: "4px",
18
+ },
19
+ default: {
20
+ width: "8px",
21
+ height: "8px",
22
+ },
23
+ thick: {
24
+ width: "12px",
25
+ height: "12px",
26
+ },
7
27
  }
8
28
 
9
29
  /**
10
30
  * ScrollArea - Custom scrollbar container
11
31
  *
12
32
  * Provides a styled scrollbar that is thin and consistent across browsers.
13
- * Uses CSS scrollbar styling for webkit and Firefox.
33
+ * Supports customizable scrollbar size and orientation.
14
34
  */
15
35
  const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
16
- ({ className, children, orientation = "vertical", ...props }, ref) => {
36
+ ({ className, children, orientation = "vertical", scrollbarSize = "thin", hideScrollbar = false, style, ...props }, ref) => {
37
+ const sizes = scrollbarStyles[scrollbarSize]
38
+
39
+ const scrollbarCSS: React.CSSProperties = {
40
+ ...style,
41
+ // Webkit browsers (Chrome, Safari, Edge)
42
+ // @ts-ignore - CSS custom properties for scrollbar
43
+ "--scrollbar-width": sizes.width,
44
+ "--scrollbar-height": sizes.height,
45
+ }
46
+
17
47
  return (
18
48
  <div
19
49
  ref={ref}
@@ -22,13 +52,42 @@ const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
22
52
  // Container overflow based on orientation
23
53
  orientation === "vertical" && "overflow-y-auto overflow-x-hidden",
24
54
  orientation === "horizontal" && "overflow-x-auto overflow-y-hidden",
25
- orientation === "both" && "overflow-auto",
26
- // Custom scrollbar styling
27
- "scrollbar-thin",
55
+ // Custom scrollbar classes
56
+ "scrollbar-custom",
57
+ hideScrollbar && "scrollbar-hide hover:scrollbar-show",
28
58
  className
29
59
  )}
60
+ style={scrollbarCSS}
30
61
  {...props}
31
62
  >
63
+ <style>{`
64
+ .scrollbar-custom::-webkit-scrollbar {
65
+ width: var(--scrollbar-width, 4px);
66
+ height: var(--scrollbar-height, 4px);
67
+ }
68
+ .scrollbar-custom::-webkit-scrollbar-track {
69
+ background: transparent;
70
+ border-radius: 9999px;
71
+ }
72
+ .scrollbar-custom::-webkit-scrollbar-thumb {
73
+ background: hsl(var(--muted-foreground) / 0.3);
74
+ border-radius: 9999px;
75
+ }
76
+ .scrollbar-custom::-webkit-scrollbar-thumb:hover {
77
+ background: hsl(var(--muted-foreground) / 0.5);
78
+ }
79
+ .scrollbar-custom {
80
+ scrollbar-width: thin;
81
+ scrollbar-color: hsl(var(--muted-foreground) / 0.3) transparent;
82
+ }
83
+ .scrollbar-hide::-webkit-scrollbar {
84
+ opacity: 0;
85
+ }
86
+ .scrollbar-hide:hover::-webkit-scrollbar,
87
+ .scrollbar-show::-webkit-scrollbar {
88
+ opacity: 1;
89
+ }
90
+ `}</style>
32
91
  {children}
33
92
  </div>
34
93
  )
@@ -36,7 +95,7 @@ const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
36
95
  )
37
96
  ScrollArea.displayName = "ScrollArea"
38
97
 
39
- // ScrollBar component for explicit scrollbar styling reference
98
+ // ScrollBar component for explicit scrollbar styling reference (optional usage)
40
99
  interface ScrollBarProps extends React.HTMLAttributes<HTMLDivElement> {
41
100
  orientation?: "vertical" | "horizontal"
42
101
  }