@gentleduck/registry-ui 0.2.5 → 0.2.8

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 (51) hide show
  1. package/.turbo/turbo-check-types.log +1 -0
  2. package/.turbo/turbo-test.log +23 -0
  3. package/CHANGELOG.md +21 -0
  4. package/package.json +9 -8
  5. package/src/_old/_table/index.ts +9 -0
  6. package/src/_old/_table/table.tsx +7 -7
  7. package/src/_old/_upload/index.ts +13 -0
  8. package/src/_old/_upload/upload-sonner.tsx +1 -1
  9. package/src/alert-dialog/alert-dialog.tsx +11 -4
  10. package/src/aspect-ratio/aspect-ratio.tsx +9 -11
  11. package/src/audio/audio-visualizer.tsx +28 -2
  12. package/src/audio/audio.types.ts +1 -2
  13. package/src/button/__test__/button.test.tsx +80 -0
  14. package/src/button/button.tsx +1 -1
  15. package/src/button-group/button-group.tsx +1 -0
  16. package/src/calendar/calendar.tsx +161 -141
  17. package/src/carousel/carousel.tsx +1 -0
  18. package/src/chart/__test__/chart.test.tsx +40 -0
  19. package/src/chart/chart.tsx +16 -7
  20. package/src/checkbox/checkbox.tsx +1 -0
  21. package/src/collapsible/collapsible.tsx +2 -1
  22. package/src/combobox/combobox.tsx +96 -69
  23. package/src/command/command.tsx +34 -37
  24. package/src/context-menu/context-menu.tsx +11 -3
  25. package/src/dialog/dialog-responsive.tsx +12 -1
  26. package/src/dialog/dialog.tsx +12 -4
  27. package/src/dropdown-menu/dropdown-menu.tsx +11 -3
  28. package/src/empty/empty.tsx +30 -17
  29. package/src/field/field.tsx +138 -109
  30. package/src/input-group/input-group.tsx +3 -0
  31. package/src/item/item.tsx +1 -0
  32. package/src/json-editor/json-editor.tsx +59 -60
  33. package/src/json-editor/json-editor.view.tsx +1 -0
  34. package/src/label/label.tsx +1 -0
  35. package/src/menubar/menubar.tsx +10 -3
  36. package/src/popover/popover.tsx +4 -0
  37. package/src/preview-panel/preview-panel-dialog.tsx +86 -80
  38. package/src/preview-panel/preview-panel.tsx +280 -273
  39. package/src/resizable/resizable.tsx +17 -15
  40. package/src/select/select.tsx +3 -0
  41. package/src/separator/separator.tsx +0 -1
  42. package/src/sheet/sheet.tsx +16 -4
  43. package/src/sidebar/sidebar.tsx +436 -378
  44. package/src/slider/slider.tsx +8 -10
  45. package/src/sonner/sonner.chunks.tsx +3 -0
  46. package/src/sonner/sonner.tsx +23 -20
  47. package/src/switch/switch.tsx +1 -0
  48. package/src/tabs/tabs.tsx +2 -2
  49. package/src/toggle/toggle.constants.ts +2 -2
  50. package/src/tooltip/tooltip.tsx +3 -0
  51. package/tsconfig.json +10 -1
@@ -7,154 +7,174 @@ import * as React from 'react'
7
7
  import { type DayButton, DayPicker, getDefaultClassNames } from 'react-day-picker'
8
8
  import { Button, buttonVariants } from '../button'
9
9
 
10
- function Calendar({
11
- className,
12
- classNames,
13
- showOutsideDays = true,
14
- captionLayout = 'label',
15
- buttonVariant = 'ghost',
16
- formatters,
17
- components,
18
- dir,
19
- ...props
20
- }: React.ComponentProps<typeof DayPicker> & {
21
- buttonVariant?: React.ComponentProps<typeof Button>['variant']
22
- }) {
23
- const direction = useDirection(dir as Direction)
24
- const defaultClassNames = getDefaultClassNames()
25
- const localeTag = React.useMemo(() => {
26
- const code = props.locale?.code
27
- if (!code) return undefined
28
- return code.startsWith('ar') ? `${code}-u-nu-arab` : code
29
- }, [props.locale])
10
+ function mergeRefs<T>(...refs: (React.Ref<T> | undefined)[]): React.RefCallback<T> {
11
+ return (node) => {
12
+ for (const ref of refs) {
13
+ if (typeof ref === 'function') {
14
+ ref(node)
15
+ } else if (ref != null) {
16
+ ;(ref as React.MutableRefObject<T | null>).current = node
17
+ }
18
+ }
19
+ }
20
+ }
30
21
 
31
- const monthFormatter = React.useMemo(() => {
32
- return new Intl.DateTimeFormat(localeTag, { month: 'short' })
33
- }, [localeTag])
22
+ const Calendar = React.forwardRef<
23
+ HTMLDivElement,
24
+ React.ComponentProps<typeof DayPicker> & {
25
+ buttonVariant?: React.ComponentProps<typeof Button>['variant']
26
+ }
27
+ >(
28
+ (
29
+ {
30
+ className,
31
+ classNames,
32
+ showOutsideDays = true,
33
+ captionLayout = 'label',
34
+ buttonVariant = 'ghost',
35
+ formatters,
36
+ components,
37
+ dir,
38
+ ...props
39
+ },
40
+ ref,
41
+ ) => {
42
+ const direction = useDirection(dir as Direction)
43
+ const defaultClassNames = getDefaultClassNames()
44
+ const localeTag = React.useMemo(() => {
45
+ const code = props.locale?.code
46
+ if (!code) return undefined
47
+ return code.startsWith('ar') ? `${code}-u-nu-arab` : code
48
+ }, [props.locale])
34
49
 
35
- const captionFormatter = React.useMemo(() => {
36
- return new Intl.DateTimeFormat(localeTag, { month: 'long', year: 'numeric' })
37
- }, [localeTag])
50
+ const monthFormatter = React.useMemo(() => {
51
+ return new Intl.DateTimeFormat(localeTag, { month: 'short' })
52
+ }, [localeTag])
38
53
 
39
- const numberFormatter = React.useMemo(() => {
40
- return new Intl.NumberFormat(localeTag)
41
- }, [localeTag])
54
+ const captionFormatter = React.useMemo(() => {
55
+ return new Intl.DateTimeFormat(localeTag, { month: 'long', year: 'numeric' })
56
+ }, [localeTag])
42
57
 
43
- const formatLocalizedNumber = React.useCallback(
44
- (value: number) => {
45
- return numberFormatter.format(value)
46
- },
47
- [numberFormatter],
48
- )
58
+ const numberFormatter = React.useMemo(() => {
59
+ return new Intl.NumberFormat(localeTag)
60
+ }, [localeTag])
49
61
 
50
- return (
51
- <DayPicker
52
- dir={direction}
53
- captionLayout={captionLayout}
54
- className={cn(
55
- 'group/calendar bg-background in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent p-3 [--cell-size:--spacing(8)]',
56
- String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
57
- String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
58
- className,
59
- )}
60
- classNames={{
61
- button_next: cn(
62
- buttonVariants({ variant: buttonVariant }),
63
- 'size-(--cell-size) select-none p-0 aria-disabled:opacity-50',
64
- defaultClassNames.button_next,
65
- ),
66
- button_previous: cn(
67
- buttonVariants({ variant: buttonVariant }),
68
- 'size-(--cell-size) select-none p-0 aria-disabled:opacity-50',
69
- defaultClassNames.button_previous,
70
- ),
71
- caption_label: cn(
72
- 'select-none font-medium',
73
- captionLayout === 'label'
74
- ? 'text-sm'
75
- : 'flex h-8 items-center gap-1 rounded-md ps-2 pe-1 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground',
76
- defaultClassNames.caption_label,
77
- ),
78
- day: cn(
79
- 'group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-s-md [&:last-child[data-selected=true]_button]:rounded-e-md',
80
- defaultClassNames.day,
81
- ),
82
- disabled: cn('text-muted-foreground opacity-50', defaultClassNames.disabled),
83
- dropdown: cn('absolute inset-0 bg-popover opacity-0', defaultClassNames.dropdown),
84
- dropdown_root: cn(
85
- 'relative rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50',
86
- defaultClassNames.dropdown_root,
87
- ),
88
- dropdowns: cn(
89
- 'flex h-(--cell-size) w-full items-center justify-center gap-1.5 font-medium text-sm',
90
- defaultClassNames.dropdowns,
91
- ),
92
- hidden: cn('invisible', defaultClassNames.hidden),
93
- month: cn('flex w-full flex-col gap-4', defaultClassNames.month),
94
- month_caption: cn(
95
- 'flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)',
96
- defaultClassNames.month_caption,
97
- ),
98
- months: cn('relative flex flex-col gap-4 md:flex-row', defaultClassNames.months),
99
- nav: cn('absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1', defaultClassNames.nav),
100
- outside: cn('text-muted-foreground aria-selected:text-muted-foreground', defaultClassNames.outside),
101
- range_end: cn('rounded-e-md bg-accent', defaultClassNames.range_end),
102
- range_middle: cn('rounded-none', defaultClassNames.range_middle),
103
- range_start: cn('rounded-s-md bg-accent', defaultClassNames.range_start),
104
- root: cn('w-fit', defaultClassNames.root),
105
- table: 'w-full border-collapse',
106
- today: cn(
107
- 'rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none',
108
- defaultClassNames.today,
109
- ),
110
- week: cn('mt-2 flex w-full', defaultClassNames.week),
111
- week_number: cn('select-none text-[0.8rem] text-muted-foreground', defaultClassNames.week_number),
112
- week_number_header: cn('w-(--cell-size) select-none', defaultClassNames.week_number_header),
113
- weekday: cn(
114
- 'flex-1 select-none rounded-md font-normal text-[0.8rem] text-muted-foreground',
115
- defaultClassNames.weekday,
116
- ),
117
- weekdays: cn('flex', defaultClassNames.weekdays),
118
- ...classNames,
119
- }}
120
- components={{
121
- Chevron: ({ className, orientation, ...props }) => {
122
- if (orientation === 'left') {
123
- return <ChevronLeftIcon className={cn('size-4', className)} {...props} />
124
- }
62
+ const formatLocalizedNumber = React.useCallback(
63
+ (value: number) => {
64
+ return numberFormatter.format(value)
65
+ },
66
+ [numberFormatter],
67
+ )
125
68
 
126
- if (orientation === 'right') {
127
- return <ChevronRightIcon className={cn('size-4', className)} {...props} />
128
- }
69
+ return (
70
+ <DayPicker
71
+ dir={direction}
72
+ captionLayout={captionLayout}
73
+ className={cn(
74
+ 'group/calendar bg-background in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent p-3 [--cell-size:--spacing(8)]',
75
+ String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
76
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
77
+ className,
78
+ )}
79
+ classNames={{
80
+ button_next: cn(
81
+ buttonVariants({ variant: buttonVariant }),
82
+ 'size-(--cell-size) select-none p-0 aria-disabled:opacity-50',
83
+ defaultClassNames.button_next,
84
+ ),
85
+ button_previous: cn(
86
+ buttonVariants({ variant: buttonVariant }),
87
+ 'size-(--cell-size) select-none p-0 aria-disabled:opacity-50',
88
+ defaultClassNames.button_previous,
89
+ ),
90
+ caption_label: cn(
91
+ 'select-none font-medium',
92
+ captionLayout === 'label'
93
+ ? 'text-sm'
94
+ : 'flex h-8 items-center gap-1 rounded-md ps-2 pe-1 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground',
95
+ defaultClassNames.caption_label,
96
+ ),
97
+ day: cn(
98
+ 'group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-s-md [&:last-child[data-selected=true]_button]:rounded-e-md',
99
+ defaultClassNames.day,
100
+ ),
101
+ disabled: cn('text-muted-foreground opacity-50', defaultClassNames.disabled),
102
+ dropdown: cn('absolute inset-0 bg-popover opacity-0', defaultClassNames.dropdown),
103
+ dropdown_root: cn(
104
+ 'relative rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50',
105
+ defaultClassNames.dropdown_root,
106
+ ),
107
+ dropdowns: cn(
108
+ 'flex h-(--cell-size) w-full items-center justify-center gap-1.5 font-medium text-sm',
109
+ defaultClassNames.dropdowns,
110
+ ),
111
+ hidden: cn('invisible', defaultClassNames.hidden),
112
+ month: cn('flex w-full flex-col gap-4', defaultClassNames.month),
113
+ month_caption: cn(
114
+ 'flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)',
115
+ defaultClassNames.month_caption,
116
+ ),
117
+ months: cn('relative flex flex-col gap-4 md:flex-row', defaultClassNames.months),
118
+ nav: cn('absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1', defaultClassNames.nav),
119
+ outside: cn('text-muted-foreground aria-selected:text-muted-foreground', defaultClassNames.outside),
120
+ range_end: cn('rounded-e-md bg-accent', defaultClassNames.range_end),
121
+ range_middle: cn('rounded-none', defaultClassNames.range_middle),
122
+ range_start: cn('rounded-s-md bg-accent', defaultClassNames.range_start),
123
+ root: cn('w-fit', defaultClassNames.root),
124
+ table: 'w-full border-collapse',
125
+ today: cn(
126
+ 'rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none',
127
+ defaultClassNames.today,
128
+ ),
129
+ week: cn('mt-2 flex w-full', defaultClassNames.week),
130
+ week_number: cn('select-none text-[0.8rem] text-muted-foreground', defaultClassNames.week_number),
131
+ week_number_header: cn('w-(--cell-size) select-none', defaultClassNames.week_number_header),
132
+ weekday: cn(
133
+ 'flex-1 select-none rounded-md font-normal text-[0.8rem] text-muted-foreground',
134
+ defaultClassNames.weekday,
135
+ ),
136
+ weekdays: cn('flex', defaultClassNames.weekdays),
137
+ ...classNames,
138
+ }}
139
+ components={{
140
+ Chevron: ({ className, orientation, ...props }) => {
141
+ if (orientation === 'left') {
142
+ return <ChevronLeftIcon className={cn('size-4', className)} {...props} />
143
+ }
129
144
 
130
- return <ChevronDownIcon className={cn('size-4', className)} {...props} />
131
- },
132
- DayButton: CalendarDayButton,
133
- Root: ({ className, rootRef, ...props }) => {
134
- return <div className={cn(className)} data-slot="calendar" ref={rootRef} {...props} />
135
- },
136
- WeekNumber: ({ children, ...props }) => {
137
- return (
138
- <td {...props}>
139
- <div className="flex size-(--cell-size) items-center justify-center text-center">{children}</div>
140
- </td>
141
- )
142
- },
143
- ...components,
144
- }}
145
- formatters={{
146
- formatCaption: (date) => captionFormatter.format(date),
147
- formatDay: (date) => formatLocalizedNumber(date.getDate()),
148
- formatMonthDropdown: (date) => monthFormatter.format(date),
149
- formatWeekNumber: (weekNumber) => formatLocalizedNumber(weekNumber),
150
- formatYearDropdown: (date) => String(date.getFullYear()),
151
- ...formatters,
152
- }}
153
- showOutsideDays={showOutsideDays}
154
- {...props}
155
- />
156
- )
157
- }
145
+ if (orientation === 'right') {
146
+ return <ChevronRightIcon className={cn('size-4', className)} {...props} />
147
+ }
148
+
149
+ return <ChevronDownIcon className={cn('size-4', className)} {...props} />
150
+ },
151
+ DayButton: CalendarDayButton,
152
+ Root: ({ className, rootRef, ...props }) => {
153
+ return <div className={cn(className)} data-slot="calendar" ref={mergeRefs(ref, rootRef)} {...props} />
154
+ },
155
+ WeekNumber: ({ children, ...props }) => {
156
+ return (
157
+ <td {...props}>
158
+ <div className="flex size-(--cell-size) items-center justify-center text-center">{children}</div>
159
+ </td>
160
+ )
161
+ },
162
+ ...components,
163
+ }}
164
+ formatters={{
165
+ formatCaption: (date) => captionFormatter.format(date),
166
+ formatDay: (date) => formatLocalizedNumber(date.getDate()),
167
+ formatMonthDropdown: (date) => monthFormatter.format(date),
168
+ formatWeekNumber: (weekNumber) => formatLocalizedNumber(weekNumber),
169
+ formatYearDropdown: (date) => String(date.getFullYear()),
170
+ ...formatters,
171
+ }}
172
+ showOutsideDays={showOutsideDays}
173
+ {...props}
174
+ />
175
+ )
176
+ },
177
+ )
158
178
  Calendar.displayName = 'Calendar'
159
179
 
160
180
  function CalendarDayButton({ className, day, modifiers, ...props }: React.ComponentProps<typeof DayButton>) {
@@ -100,6 +100,7 @@ const Carousel = React.forwardRef<HTMLElement, React.HTMLAttributes<HTMLDivEleme
100
100
  scrollNext,
101
101
  scrollPrev,
102
102
  }}>
103
+ {/* biome-ignore lint/a11y/useAriaPropsSupportedByRole: carousel is a custom widget needing roledescription */}
103
104
  <section
104
105
  aria-roledescription="carousel"
105
106
  className={cn('relative', className)}
@@ -0,0 +1,40 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import * as React from 'react'
3
+ import { renderToStaticMarkup } from 'react-dom/server'
4
+ import { Bar, BarChart, XAxis } from 'recharts'
5
+ import { ChartContainer } from '../chart'
6
+
7
+ const data = [{ name: 'alpha', value: 12 }]
8
+
9
+ describe('registry-ui chart', () => {
10
+ test('ChartContainer server render does not emit invalid size warnings', () => {
11
+ const originalWarn = console.warn
12
+ const warnings: string[] = []
13
+
14
+ console.warn = (...args: unknown[]) => {
15
+ warnings.push(args.map((value) => String(value)).join(' '))
16
+ }
17
+
18
+ try {
19
+ const html = renderToStaticMarkup(
20
+ <ChartContainer
21
+ config={{
22
+ value: {
23
+ color: 'hsl(var(--chart-1))',
24
+ label: 'Value',
25
+ },
26
+ }}>
27
+ <BarChart accessibilityLayer data={data}>
28
+ <XAxis dataKey="name" hide />
29
+ <Bar dataKey="value" fill="var(--color-value)" radius={8} />
30
+ </BarChart>
31
+ </ChartContainer>,
32
+ )
33
+
34
+ expect(html).toContain('data-slot="chart-container"')
35
+ expect(warnings.some((message) => message.includes('The width(') && message.includes('height('))).toBe(false)
36
+ } finally {
37
+ console.warn = originalWarn
38
+ }
39
+ })
40
+ })
@@ -15,6 +15,7 @@ import type {
15
15
 
16
16
  // Format: { THEME_NAME: CSS_SELECTOR }
17
17
  export const THEMES = { dark: '.dark', light: '' } as const
18
+ const DEFAULT_CHART_INITIAL_DIMENSION = { width: 640, height: 360 } as const
18
19
 
19
20
  const ChartContext = React.createContext<ChartContextProps | null>(null)
20
21
 
@@ -28,7 +29,7 @@ function useChart() {
28
29
  return context
29
30
  }
30
31
 
31
- const ChartContainer = ({ id, className, children, config, ref, dir, ...props }: ChartContainerProps) => {
32
+ function ChartContainer({ id, className, children, config, ref, dir, ...props }: ChartContainerProps) {
32
33
  const uniqueId = React.useId()
33
34
  const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`
34
35
  const direction = useDirection(dir as Direction)
@@ -46,13 +47,16 @@ const ChartContainer = ({ id, className, children, config, ref, dir, ...props }:
46
47
  dir={direction}
47
48
  ref={ref}>
48
49
  <ChartStyle config={config} id={chartId} />
49
- <RechartsPrimitive.ResponsiveContainer minWidth={0}>{children}</RechartsPrimitive.ResponsiveContainer>
50
+ <RechartsPrimitive.ResponsiveContainer initialDimension={DEFAULT_CHART_INITIAL_DIMENSION} minWidth={0}>
51
+ {children}
52
+ </RechartsPrimitive.ResponsiveContainer>
50
53
  </div>
51
54
  </ChartContext.Provider>
52
55
  )
53
56
  }
57
+ ChartContainer.displayName = 'ChartContainer'
54
58
 
55
- const ChartStyle = ({ id, config }: ChartStyleProps) => {
59
+ function ChartStyle({ id, config }: ChartStyleProps) {
56
60
  const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color)
57
61
 
58
62
  if (!colorConfig.length) {
@@ -61,6 +65,7 @@ const ChartStyle = ({ id, config }: ChartStyleProps) => {
61
65
 
62
66
  return (
63
67
  <style
68
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: controlled CSS injection for chart color themes
64
69
  dangerouslySetInnerHTML={{
65
70
  __html: Object.entries(THEMES)
66
71
  .map(
@@ -80,10 +85,11 @@ ${colorConfig
80
85
  />
81
86
  )
82
87
  }
88
+ ChartStyle.displayName = 'ChartStyle'
83
89
 
84
90
  const ChartTooltip = RechartsPrimitive.Tooltip
85
91
 
86
- const ChartTooltipContent = ({
92
+ function ChartTooltipContent({
87
93
  active,
88
94
  payload,
89
95
  className,
@@ -98,7 +104,7 @@ const ChartTooltipContent = ({
98
104
  color,
99
105
  nameKey,
100
106
  labelKey,
101
- }: ChartTooltipContentProps) => {
107
+ }: ChartTooltipContentProps) {
102
108
  const { config } = useChart()
103
109
 
104
110
  const tooltipLabel = React.useMemo(() => {
@@ -208,17 +214,19 @@ const ChartTooltipContent = ({
208
214
  </div>
209
215
  )
210
216
  }
217
+ ChartTooltipContent.displayName = 'ChartTooltipContent'
211
218
 
212
219
  const ChartLegend = RechartsPrimitive.Legend
220
+ ChartLegend.displayName = 'ChartLegend'
213
221
 
214
- const ChartLegendContent = ({
222
+ function ChartLegendContent({
215
223
  className,
216
224
  hideIcon = false,
217
225
  payload,
218
226
  verticalAlign = 'bottom',
219
227
  ref,
220
228
  nameKey,
221
- }: ChartLegendContentProps) => {
229
+ }: ChartLegendContentProps) {
222
230
  const { config } = useChart()
223
231
 
224
232
  if (!payload?.length) {
@@ -256,5 +264,6 @@ const ChartLegendContent = ({
256
264
  </div>
257
265
  )
258
266
  }
267
+ ChartLegendContent.displayName = 'ChartLegendContent'
259
268
 
260
269
  export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle }
@@ -37,6 +37,7 @@ const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
37
37
  onCheckedChange?.(next)
38
38
  }
39
39
 
40
+ // biome-ignore lint/correctness/useExhaustiveDependencies: changeCheckedState is stable and defined in render scope
40
41
  React.useEffect(() => {
41
42
  if (ref && typeof ref !== 'function' && checked === 'indeterminate' && ref.current) {
42
43
  ref.current.indeterminate = true
@@ -44,6 +44,7 @@ const Collapsible = React.forwardRef<
44
44
  onOpenChange?.(state)
45
45
  }
46
46
 
47
+ // biome-ignore lint/correctness/useExhaustiveDependencies: handleOpenChange and triggerRef are stable refs
47
48
  React.useEffect(() => {
48
49
  if (open) {
49
50
  handleOpenChange(open)
@@ -56,7 +57,7 @@ const Collapsible = React.forwardRef<
56
57
 
57
58
  triggerRef.current?.addEventListener('click', handleClick)
58
59
  return () => triggerRef.current?.removeEventListener('click', handleClick)
59
- }, [open])
60
+ }, [open, onOpenChange])
60
61
 
61
62
  return (
62
63
  <CollapsibleContext.Provider