@tinybigui/react 0.1.1 → 0.3.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 CHANGED
@@ -1,15 +1,49 @@
1
1
  import { clsx } from 'clsx';
2
- import { twMerge } from 'tailwind-merge';
2
+ import { extendTailwindMerge } from 'tailwind-merge';
3
3
  import { argbFromHex, themeFromSourceColor } from '@material/material-color-utilities';
4
4
  export { argbFromHex, hexFromArgb } from '@material/material-color-utilities';
5
- import { forwardRef, useRef, useEffect, createContext, useContext, useState, useCallback } from 'react';
6
- import { useButton, useTextField, useFocusRing, useCheckbox, VisuallyHidden, mergeProps as mergeProps$1, useSwitch, useRadioGroup, useRadio } from 'react-aria';
7
- import { mergeProps, filterDOMProps } from '@react-aria/utils';
8
- import { jsx, jsxs } from 'react/jsx-runtime';
5
+ import { forwardRef, useRef, useEffect, createContext, useContext, useMemo, useCallback, useState, useLayoutEffect, useId, Children, isValidElement, cloneElement } from 'react';
6
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
9
7
  import { cva } from 'class-variance-authority';
10
- import { useToggleState, useRadioGroupState } from 'react-stately';
8
+ import { useButton, useTextField, useFocusRing, useCheckbox, VisuallyHidden, mergeProps as mergeProps$1, useSwitch, useRadioGroup, useRadio, useTabList, useTab, useTabPanel, FocusScope, usePreventScroll, useDialog, useOverlay, useLink, useProgressBar } from 'react-aria';
9
+ import { mergeProps, filterDOMProps } from '@react-aria/utils';
10
+ import { useToggleState, useRadioGroupState, useTabListState, Item, useOverlayTriggerState } from 'react-stately';
11
+ import { MenuItem as MenuItem$1, Menu as Menu$1, MenuTrigger as MenuTrigger$1, Popover, MenuSection as MenuSection$1, Separator, Header, useSlottedContext, ButtonContext } from 'react-aria-components';
12
+ import { createPortal } from 'react-dom';
11
13
 
12
14
  // src/utils/cn.ts
15
+ var twMerge = extendTailwindMerge({
16
+ extend: {
17
+ classGroups: {
18
+ "font-size": [
19
+ {
20
+ text: [
21
+ // MD3 Display scale
22
+ "display-large",
23
+ "display-medium",
24
+ "display-small",
25
+ // MD3 Headline scale
26
+ "headline-large",
27
+ "headline-medium",
28
+ "headline-small",
29
+ // MD3 Title scale
30
+ "title-large",
31
+ "title-medium",
32
+ "title-small",
33
+ // MD3 Body scale
34
+ "body-large",
35
+ "body-medium",
36
+ "body-small",
37
+ // MD3 Label scale
38
+ "label-large",
39
+ "label-medium",
40
+ "label-small"
41
+ ]
42
+ }
43
+ ]
44
+ }
45
+ }
46
+ });
13
47
  function cn(...inputs) {
14
48
  return twMerge(clsx(inputs));
15
49
  }
@@ -130,6 +164,162 @@ function truncateText(lines = 1) {
130
164
  textOverflow: "ellipsis"
131
165
  };
132
166
  }
167
+ function useScrollElevation(options = {}) {
168
+ const { scrolled: controlledScrolled, onScrollStateChange, threshold = 0 } = options;
169
+ const isControlled = controlledScrolled !== void 0;
170
+ const [internalScrolled, setInternalScrolled] = useState(false);
171
+ const handleScroll = useCallback(() => {
172
+ const currentlyScrolled = window.scrollY > threshold;
173
+ setInternalScrolled((prev) => {
174
+ if (prev !== currentlyScrolled) {
175
+ onScrollStateChange?.(currentlyScrolled);
176
+ return currentlyScrolled;
177
+ }
178
+ return prev;
179
+ });
180
+ }, [threshold, onScrollStateChange]);
181
+ useEffect(() => {
182
+ if (isControlled) return;
183
+ window.addEventListener("scroll", handleScroll, { passive: true });
184
+ handleScroll();
185
+ return () => {
186
+ window.removeEventListener("scroll", handleScroll);
187
+ };
188
+ }, [isControlled, handleScroll]);
189
+ return {
190
+ isScrolled: isControlled ? controlledScrolled : internalScrolled
191
+ };
192
+ }
193
+ var AppBarHeadless = forwardRef(
194
+ ({ className, children, scrolled: scrolledProp, onScrollStateChange }, ref) => {
195
+ const { isScrolled } = useScrollElevation({
196
+ scrolled: scrolledProp,
197
+ onScrollStateChange
198
+ });
199
+ return /* @__PURE__ */ jsx("header", { ref, role: "banner", className, "data-scrolled": isScrolled, children });
200
+ }
201
+ );
202
+ AppBarHeadless.displayName = "AppBarHeadless";
203
+ var appBarVariants = cva(
204
+ [
205
+ // Base classes (always applied)
206
+ "w-full",
207
+ "bg-surface text-on-surface",
208
+ "flex flex-col",
209
+ // Elevation transition using MD3 motion tokens
210
+ "transition-shadow duration-medium2 ease-standard"
211
+ ],
212
+ {
213
+ variants: {
214
+ /**
215
+ * Size variant (MD3 specification)
216
+ * Controls bar height, title placement, and type scale
217
+ */
218
+ variant: {
219
+ /** 64dp, title left-aligned, title-large */
220
+ small: "h-appbar-small",
221
+ /** 64dp, title centered, title-large */
222
+ "center-aligned": "h-appbar-small variant-center-aligned",
223
+ /** 112dp, title bottom-left, headline-small */
224
+ medium: "h-appbar-medium",
225
+ /** 152dp, title bottom-left, display-small */
226
+ large: "h-appbar-large"
227
+ },
228
+ /**
229
+ * Scroll state — controls surface elevation
230
+ * MD3: flat at rest, elevated on scroll
231
+ */
232
+ scrolled: {
233
+ false: "shadow-elevation-0",
234
+ true: "shadow-elevation-2"
235
+ }
236
+ },
237
+ defaultVariants: {
238
+ variant: "small",
239
+ scrolled: false
240
+ }
241
+ }
242
+ );
243
+ var appBarTitleVariants = cva("text-on-surface font-normal truncate", {
244
+ variants: {
245
+ variant: {
246
+ small: "text-title-large",
247
+ "center-aligned": "text-title-large",
248
+ medium: "text-headline-small",
249
+ large: "text-display-small"
250
+ }
251
+ },
252
+ defaultVariants: {
253
+ variant: "small"
254
+ }
255
+ });
256
+ var AppBar = forwardRef(
257
+ ({
258
+ variant = "small",
259
+ title,
260
+ navigationIcon,
261
+ actions,
262
+ scrolled: scrolledProp,
263
+ onScrollStateChange,
264
+ className
265
+ }, ref) => {
266
+ const { isScrolled } = useScrollElevation({
267
+ scrolled: scrolledProp,
268
+ onScrollStateChange
269
+ });
270
+ const isExpandedVariant = variant === "medium" || variant === "large";
271
+ return /* @__PURE__ */ jsxs(
272
+ AppBarHeadless,
273
+ {
274
+ ref,
275
+ scrolled: isScrolled,
276
+ className: cn(appBarVariants({ variant, scrolled: isScrolled }), className),
277
+ children: [
278
+ /* @__PURE__ */ jsxs(
279
+ "div",
280
+ {
281
+ "data-slot": "top-row",
282
+ className: cn(
283
+ "flex items-center",
284
+ "px-1",
285
+ // Small and center-aligned: fill the full bar height
286
+ !isExpandedVariant && "flex-1",
287
+ // Expanded variants: fixed height for the top row (64dp)
288
+ isExpandedVariant && "h-16 shrink-0"
289
+ ),
290
+ children: [
291
+ navigationIcon != null && /* @__PURE__ */ jsx("div", { "data-slot": "navigation", className: "flex shrink-0 items-center", children: navigationIcon }),
292
+ !isExpandedVariant && /* @__PURE__ */ jsx(
293
+ "span",
294
+ {
295
+ "data-testid": "appbar-title",
296
+ className: cn(
297
+ "min-w-0 flex-1 px-1",
298
+ appBarTitleVariants({ variant }),
299
+ // Center-aligned: center the title text
300
+ variant === "center-aligned" && "text-center"
301
+ ),
302
+ children: title
303
+ }
304
+ ),
305
+ actions != null && /* @__PURE__ */ jsx("div", { "data-slot": "actions", className: "flex shrink-0 items-center gap-0.5", children: actions })
306
+ ]
307
+ }
308
+ ),
309
+ isExpandedVariant && /* @__PURE__ */ jsx("div", { "data-slot": "expanded-title", className: cn("flex flex-1 items-end", "px-4 pb-4"), children: /* @__PURE__ */ jsx(
310
+ "span",
311
+ {
312
+ "data-testid": "appbar-title",
313
+ className: cn("min-w-0 truncate", appBarTitleVariants({ variant })),
314
+ children: title
315
+ }
316
+ ) })
317
+ ]
318
+ }
319
+ );
320
+ }
321
+ );
322
+ AppBar.displayName = "AppBar";
133
323
  var ButtonHeadless = forwardRef(
134
324
  ({ className, children, tabIndex = 0, onMouseDown, type, ...restProps }, forwardedRef) => {
135
325
  const internalRef = useRef(null);
@@ -303,7 +493,7 @@ var buttonVariants = cva(
303
493
  {
304
494
  variant: "tonal",
305
495
  color: "primary",
306
- className: "bg-secondary-container text-on-secondary-container"
496
+ className: "bg-primary-container text-on-primary-container"
307
497
  },
308
498
  {
309
499
  variant: "tonal",
@@ -574,7 +764,7 @@ IconButtonHeadless.displayName = "IconButtonHeadless";
574
764
  var iconButtonVariants = cva(
575
765
  [
576
766
  // Base classes (always applied)
577
- "relative inline-flex items-center justify-center",
767
+ "relative inline-flex items-center justify-center cursor-pointer",
578
768
  "overflow-hidden rounded-full",
579
769
  // Circular shape
580
770
  "transition-all duration-200",
@@ -888,7 +1078,7 @@ FABHeadless.displayName = "FABHeadless";
888
1078
  var fabVariants = cva(
889
1079
  [
890
1080
  // Base classes (always applied)
891
- "relative inline-flex items-center justify-center",
1081
+ "relative inline-flex items-center justify-center cursor-pointer",
892
1082
  "overflow-hidden",
893
1083
  "transition-all duration-200",
894
1084
  "focus-visible:outline-primary focus-visible:outline-2 focus-visible:outline-offset-2",
@@ -1329,8 +1519,8 @@ var textFieldLabelVariants = cva(
1329
1519
  {
1330
1520
  variants: {
1331
1521
  variant: {
1332
- filled: "top-4",
1333
- outlined: "top-3 bg-surface px-1"
1522
+ filled: "top-2.5",
1523
+ outlined: "top-2.5 bg-surface px-1"
1334
1524
  },
1335
1525
  size: {
1336
1526
  small: "text-sm",
@@ -1338,7 +1528,7 @@ var textFieldLabelVariants = cva(
1338
1528
  large: "text-lg"
1339
1529
  },
1340
1530
  floating: {
1341
- true: "-translate-y-6 scale-75",
1531
+ true: "-translate-y-5 scale-75",
1342
1532
  false: "scale-100"
1343
1533
  },
1344
1534
  focused: {
@@ -1363,7 +1553,7 @@ var textFieldLabelVariants = cva(
1363
1553
  {
1364
1554
  variant: "outlined",
1365
1555
  floating: true,
1366
- className: "-top-2"
1556
+ className: "top-2.5"
1367
1557
  }
1368
1558
  ],
1369
1559
  defaultVariants: {
@@ -1676,7 +1866,8 @@ var TextField = forwardRef(
1676
1866
  hasLeadingIcon: !!leadingIcon,
1677
1867
  hasTrailingIcon: !!trailingIcon,
1678
1868
  multiline: true
1679
- })
1869
+ }),
1870
+ label && "placeholder:opacity-0"
1680
1871
  ),
1681
1872
  rows,
1682
1873
  spellCheck: spellCheckProp
@@ -1694,7 +1885,9 @@ var TextField = forwardRef(
1694
1885
  hasLeadingIcon: !!leadingIcon,
1695
1886
  hasTrailingIcon: !!trailingIcon,
1696
1887
  multiline: false
1697
- })
1888
+ }),
1889
+ label && "placeholder:opacity-0"
1890
+ // Hide placeholder when there's a value to prevent overlap with floating label
1698
1891
  ),
1699
1892
  spellCheck: spellCheckProp
1700
1893
  }
@@ -1928,8 +2121,7 @@ var checkboxLabelVariants = cva(
1928
2121
  // MD3: Body Medium (14px)
1929
2122
  "text-on-surface",
1930
2123
  "select-none",
1931
- "ml-4"
1932
- // 16px spacing between checkbox and label (MD3 standard)
2124
+ "ml-1.5"
1933
2125
  ],
1934
2126
  {
1935
2127
  variants: {
@@ -2191,7 +2383,7 @@ var switchHandleContainerVariants = cva(
2191
2383
  */
2192
2384
  selected: {
2193
2385
  true: [
2194
- "left-[28px]",
2386
+ "left-[24px]",
2195
2387
  // Position when ON (52px - 24px = 28px)
2196
2388
  "text-primary"
2197
2389
  // State layer color
@@ -2731,8 +2923,7 @@ var radioLabelVariants = cva(
2731
2923
  // MD3: Body Medium (14px)
2732
2924
  "text-on-surface",
2733
2925
  "select-none",
2734
- "ml-4"
2735
- // 16px spacing between radio and label (MD3 standard)
2926
+ "ml-1.5"
2736
2927
  ],
2737
2928
  {
2738
2929
  variants: {
@@ -2984,7 +3175,2965 @@ var RadioHeadless = forwardRef(
2984
3175
  }
2985
3176
  );
2986
3177
  RadioHeadless.displayName = "RadioHeadless";
3178
+ var HeadlessTabsContext = createContext(null);
3179
+ function useHeadlessTabsContext(componentName) {
3180
+ const context = useContext(HeadlessTabsContext);
3181
+ if (!context) {
3182
+ throw new Error(`${componentName} must be used within a Tabs component`);
3183
+ }
3184
+ return context;
3185
+ }
3186
+ var HeadlessTabList = forwardRef(
3187
+ ({ children, className }, forwardedRef) => {
3188
+ const { state } = useHeadlessTabsContext("HeadlessTabList");
3189
+ const internalRef = useRef(null);
3190
+ const ref = forwardedRef ?? internalRef;
3191
+ const { tabListProps } = useTabList({}, state, ref);
3192
+ return /* @__PURE__ */ jsx("div", { ...tabListProps, ref, className, children });
3193
+ }
3194
+ );
3195
+ HeadlessTabList.displayName = "HeadlessTabList";
3196
+ var HeadlessTab = forwardRef(
3197
+ ({ item, className, onMouseDown, children, ...props }, forwardedRef) => {
3198
+ const { state } = useHeadlessTabsContext("HeadlessTab");
3199
+ const internalRef = useRef(null);
3200
+ const ref = forwardedRef ?? internalRef;
3201
+ const { tabProps, isSelected, isDisabled, isPressed } = useTab(
3202
+ {
3203
+ key: item.key,
3204
+ ...item.isDisabled !== void 0 && { isDisabled: item.isDisabled }
3205
+ },
3206
+ state,
3207
+ ref
3208
+ );
3209
+ const { isFocusVisible, focusProps } = useFocusRing();
3210
+ const mergedProps = mergeProps(tabProps, focusProps, {
3211
+ className,
3212
+ onMouseDown
3213
+ });
3214
+ return /* @__PURE__ */ jsx("button", { ...mergedProps, ref, type: "button", "data-key": String(item.key), ...props, children: typeof children === "function" ? children({ isSelected, isDisabled, isFocusVisible, isPressed }) : children });
3215
+ }
3216
+ );
3217
+ HeadlessTab.displayName = "HeadlessTab";
3218
+ var HeadlessTabPanel = forwardRef(
3219
+ ({ children, className, ...props }, forwardedRef) => {
3220
+ const { state } = useHeadlessTabsContext("HeadlessTabPanel");
3221
+ const internalRef = useRef(null);
3222
+ const ref = forwardedRef ?? internalRef;
3223
+ const { tabPanelProps } = useTabPanel(props, state, ref);
3224
+ return /* @__PURE__ */ jsx("div", { ...tabPanelProps, ref, className, children });
3225
+ }
3226
+ );
3227
+ HeadlessTabPanel.displayName = "HeadlessTabPanel";
3228
+ var TabsContext = createContext(null);
3229
+ function useTabsContext() {
3230
+ const context = useContext(TabsContext);
3231
+ if (!context) {
3232
+ throw new Error("Component must be used within a Tabs component");
3233
+ }
3234
+ return context;
3235
+ }
3236
+ function getComponentName(type) {
3237
+ if (typeof type === "string") return type;
3238
+ const exotic = type;
3239
+ return exotic.displayName ?? exotic.name ?? "";
3240
+ }
3241
+ function extractTabItemsFromChildren(children) {
3242
+ const items = [];
3243
+ Children.forEach(children, (child) => {
3244
+ if (!isValidElement(child)) return;
3245
+ if (getComponentName(child.type) === "TabList") {
3246
+ const tabListProps = child.props;
3247
+ Children.forEach(tabListProps.children, (tabChild) => {
3248
+ if (!isValidElement(tabChild)) return;
3249
+ if (getComponentName(tabChild.type) === "Tab") {
3250
+ const tabProps = tabChild.props;
3251
+ items.push({
3252
+ key: tabProps.id,
3253
+ ...tabProps.label !== void 0 && { label: tabProps.label },
3254
+ ...tabProps.icon !== void 0 && { icon: tabProps.icon },
3255
+ ...tabProps.isDisabled !== void 0 && { isDisabled: tabProps.isDisabled },
3256
+ ...tabProps["aria-label"] !== void 0 && { "aria-label": tabProps["aria-label"] }
3257
+ });
3258
+ }
3259
+ });
3260
+ }
3261
+ });
3262
+ return items;
3263
+ }
3264
+ var Tabs = forwardRef(
3265
+ ({
3266
+ selectedKey,
3267
+ defaultSelectedKey,
3268
+ onSelectionChange,
3269
+ variant = "primary",
3270
+ layout = "fixed",
3271
+ children,
3272
+ className,
3273
+ "aria-label": ariaLabel,
3274
+ "aria-labelledby": ariaLabelledBy
3275
+ }, ref) => {
3276
+ const tabItems = useMemo(() => extractTabItemsFromChildren(children), [children]);
3277
+ const state = useTabListState({
3278
+ ...selectedKey !== void 0 && { selectedKey },
3279
+ ...defaultSelectedKey !== void 0 && { defaultSelectedKey },
3280
+ ...onSelectionChange !== void 0 && { onSelectionChange },
3281
+ disabledKeys: tabItems.filter((t) => t.isDisabled).map((t) => t.key),
3282
+ children: tabItems.map((item) => /* @__PURE__ */ jsx(Item, { textValue: item.label ?? item["aria-label"] ?? String(item.key), children: item.label ?? item["aria-label"] ?? "" }, item.key))
3283
+ });
3284
+ const tabsContextValue = useMemo(
3285
+ () => ({
3286
+ selectedKey: state.selectedKey,
3287
+ variant,
3288
+ layout,
3289
+ disabledKeys: tabItems.filter((t) => t.isDisabled).map((t) => t.key),
3290
+ ...ariaLabel !== void 0 && { "aria-label": ariaLabel },
3291
+ ...ariaLabelledBy !== void 0 && { "aria-labelledby": ariaLabelledBy }
3292
+ }),
3293
+ [state.selectedKey, variant, layout, tabItems, ariaLabel, ariaLabelledBy]
3294
+ );
3295
+ const headlessContextValue = useMemo(() => ({ state }), [state]);
3296
+ return /* @__PURE__ */ jsx(HeadlessTabsContext.Provider, { value: headlessContextValue, children: /* @__PURE__ */ jsx(TabsContext.Provider, { value: tabsContextValue, children: /* @__PURE__ */ jsx("div", { ref, className: cn("flex flex-col", className), children }) }) });
3297
+ }
3298
+ );
3299
+ Tabs.displayName = "Tabs";
3300
+ var tabListVariants = cva(
3301
+ [
3302
+ // Base classes
3303
+ "relative flex",
3304
+ "bg-surface",
3305
+ // Bottom divider line (MD3 spec)
3306
+ "border-b border-outline-variant"
3307
+ ],
3308
+ {
3309
+ variants: {
3310
+ layout: {
3311
+ fixed: "w-full",
3312
+ scrollable: "overflow-x-auto scrollbar-none"
3313
+ }
3314
+ },
3315
+ defaultVariants: {
3316
+ layout: "fixed"
3317
+ }
3318
+ }
3319
+ );
3320
+ var tabVariants = cva(
3321
+ [
3322
+ // Base layout
3323
+ "relative flex flex-col items-center justify-center",
3324
+ "min-h-12 px-4",
3325
+ "cursor-pointer select-none",
3326
+ "overflow-hidden",
3327
+ // Typography: MD3 Title Small
3328
+ "text-sm font-medium tracking-[0.1px]",
3329
+ // Transition
3330
+ "transition-colors duration-200",
3331
+ // Focus visible
3332
+ "focus-visible:outline-none",
3333
+ // State layer via before pseudo-element
3334
+ "before:absolute before:inset-0 before:transition-opacity before:duration-200",
3335
+ "before:bg-current before:opacity-0",
3336
+ "hover:before:opacity-8",
3337
+ "active:before:opacity-12",
3338
+ "focus-visible:before:opacity-12"
3339
+ ],
3340
+ {
3341
+ variants: {
3342
+ /**
3343
+ * Tab variant (Primary or Secondary)
3344
+ */
3345
+ variant: {
3346
+ primary: "",
3347
+ secondary: ""
3348
+ },
3349
+ /**
3350
+ * Selected state
3351
+ */
3352
+ selected: {
3353
+ true: "",
3354
+ false: ""
3355
+ },
3356
+ /**
3357
+ * Disabled state
3358
+ */
3359
+ disabled: {
3360
+ true: "opacity-38 cursor-not-allowed pointer-events-none",
3361
+ false: ""
3362
+ },
3363
+ /**
3364
+ * Layout determines min-width behavior
3365
+ */
3366
+ layout: {
3367
+ fixed: "flex-1",
3368
+ scrollable: "min-w-[90px] shrink-0"
3369
+ }
3370
+ },
3371
+ compoundVariants: [
3372
+ // Primary + selected
3373
+ {
3374
+ variant: "primary",
3375
+ selected: true,
3376
+ disabled: false,
3377
+ className: "text-primary"
3378
+ },
3379
+ // Primary + unselected
3380
+ {
3381
+ variant: "primary",
3382
+ selected: false,
3383
+ disabled: false,
3384
+ className: "text-on-surface-variant"
3385
+ },
3386
+ // Secondary + selected
3387
+ {
3388
+ variant: "secondary",
3389
+ selected: true,
3390
+ disabled: false,
3391
+ className: "text-on-surface"
3392
+ },
3393
+ // Secondary + unselected
3394
+ {
3395
+ variant: "secondary",
3396
+ selected: false,
3397
+ disabled: false,
3398
+ className: "text-on-surface-variant"
3399
+ }
3400
+ ],
3401
+ defaultVariants: {
3402
+ variant: "primary",
3403
+ selected: false,
3404
+ disabled: false,
3405
+ layout: "fixed"
3406
+ }
3407
+ }
3408
+ );
3409
+ var tabIndicatorVariants = cva(
3410
+ [
3411
+ // Base: absolutely positioned at bottom
3412
+ "absolute bottom-0 left-0",
3413
+ "pointer-events-none",
3414
+ // Transition using MD3 motion tokens (medium2 duration, emphasized easing)
3415
+ "transition-[left,width]",
3416
+ "duration-medium2",
3417
+ "ease-emphasized"
3418
+ ],
3419
+ {
3420
+ variants: {
3421
+ variant: {
3422
+ primary: ["h-[3px]", "bg-primary", "rounded-t-sm"],
3423
+ secondary: ["h-[2px]", "bg-on-surface-variant"]
3424
+ }
3425
+ },
3426
+ defaultVariants: {
3427
+ variant: "primary"
3428
+ }
3429
+ }
3430
+ );
3431
+ var tabPanelVariants = cva(
3432
+ [
3433
+ // Base panel styles
3434
+ "outline-none",
3435
+ "focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
3436
+ ],
3437
+ {
3438
+ variants: {},
3439
+ defaultVariants: {}
3440
+ }
3441
+ );
3442
+ var tabBadgeVariants = cva(
3443
+ [
3444
+ // Base badge
3445
+ "absolute",
3446
+ "inline-flex items-center justify-center",
3447
+ "bg-error text-on-error",
3448
+ "font-medium leading-none",
3449
+ "pointer-events-none"
3450
+ ],
3451
+ {
3452
+ variants: {
3453
+ type: {
3454
+ dot: ["top-1 right-1", "w-1.5 h-1.5", "rounded-full"],
3455
+ count: ["-top-1 -right-1", "min-w-[16px] h-4", "px-1", "rounded-full", "text-[11px]"]
3456
+ }
3457
+ },
3458
+ defaultVariants: {
3459
+ type: "count"
3460
+ }
3461
+ }
3462
+ );
3463
+ var tabIconVariants = cva(
3464
+ ["relative", "inline-flex items-center justify-center", "w-6 h-6"],
3465
+ {
3466
+ variants: {
3467
+ hasLabel: {
3468
+ true: "mb-1",
3469
+ false: ""
3470
+ }
3471
+ },
3472
+ defaultVariants: {
3473
+ hasLabel: false
3474
+ }
3475
+ }
3476
+ );
3477
+ var TabList = forwardRef(
3478
+ ({ children, className }, forwardedRef) => {
3479
+ const { state } = useHeadlessTabsContext("TabList");
3480
+ const {
3481
+ variant,
3482
+ layout,
3483
+ "aria-label": ariaLabel,
3484
+ "aria-labelledby": ariaLabelledBy
3485
+ } = useTabsContext();
3486
+ const internalRef = useRef(null);
3487
+ const ref = forwardedRef ?? internalRef;
3488
+ const { tabListProps } = useTabList(
3489
+ {
3490
+ ...ariaLabel !== void 0 && { "aria-label": ariaLabel },
3491
+ ...ariaLabelledBy !== void 0 && { "aria-labelledby": ariaLabelledBy }
3492
+ },
3493
+ state,
3494
+ ref
3495
+ );
3496
+ const handleNavKeyDown = useCallback(
3497
+ (e) => {
3498
+ if (!["ArrowRight", "ArrowLeft", "Home", "End"].includes(e.key)) return;
3499
+ const container = ref.current;
3500
+ if (!container) return;
3501
+ const focusedEl = document.activeElement;
3502
+ const currentKey = focusedEl?.dataset?.key;
3503
+ if (!currentKey) return;
3504
+ const allTabEls = [...container.querySelectorAll("[data-key]")];
3505
+ const allKeys = allTabEls.map((el) => el.dataset.key).filter(Boolean);
3506
+ const enabledKeys = allKeys.filter(
3507
+ (k) => allTabEls.find((el) => el.dataset.key === k)?.getAttribute("aria-disabled") !== "true"
3508
+ );
3509
+ const currentIndex = enabledKeys.indexOf(currentKey);
3510
+ if (currentIndex === -1) return;
3511
+ let nextKey = null;
3512
+ switch (e.key) {
3513
+ case "ArrowRight":
3514
+ nextKey = enabledKeys[(currentIndex + 1) % enabledKeys.length] ?? null;
3515
+ break;
3516
+ case "ArrowLeft":
3517
+ nextKey = enabledKeys[(currentIndex - 1 + enabledKeys.length) % enabledKeys.length] ?? null;
3518
+ break;
3519
+ case "Home":
3520
+ nextKey = enabledKeys[0] ?? null;
3521
+ break;
3522
+ case "End":
3523
+ nextKey = enabledKeys[enabledKeys.length - 1] ?? null;
3524
+ break;
3525
+ }
3526
+ if (nextKey != null) {
3527
+ e.preventDefault();
3528
+ state.setSelectedKey(nextKey);
3529
+ const nextEl = container.querySelector(
3530
+ `[data-key="${CSS.escape(nextKey)}"]`
3531
+ );
3532
+ nextEl?.focus();
3533
+ }
3534
+ },
3535
+ [state, ref]
3536
+ );
3537
+ const { onKeyDown: tabListKeyDown } = tabListProps;
3538
+ const handleKeyDown = useCallback(
3539
+ (e) => {
3540
+ handleNavKeyDown(e);
3541
+ if (!e.defaultPrevented) {
3542
+ tabListKeyDown?.(e);
3543
+ }
3544
+ },
3545
+ [handleNavKeyDown, tabListKeyDown]
3546
+ );
3547
+ const [indicatorStyle, setIndicatorStyle] = useState({ left: 0, width: 0 });
3548
+ const [indicatorReady, setIndicatorReady] = useState(false);
3549
+ const updateIndicator = useCallback(() => {
3550
+ const container = ref.current;
3551
+ if (!container) return;
3552
+ const selectedTab = container.querySelector('[aria-selected="true"]');
3553
+ if (!selectedTab) return;
3554
+ const containerRect = container.getBoundingClientRect();
3555
+ const tabRect = selectedTab.getBoundingClientRect();
3556
+ const newLeft = tabRect.left - containerRect.left + container.scrollLeft;
3557
+ const newWidth = tabRect.width;
3558
+ setIndicatorStyle((prev) => {
3559
+ if (prev.left === newLeft && prev.width === newWidth) return prev;
3560
+ return { left: newLeft, width: newWidth };
3561
+ });
3562
+ setIndicatorReady(true);
3563
+ }, [ref]);
3564
+ useLayoutEffect(() => {
3565
+ updateIndicator();
3566
+ }, [state.selectedKey, updateIndicator]);
3567
+ const mergedTabListProps = { ...tabListProps, onKeyDown: handleKeyDown };
3568
+ return /* @__PURE__ */ jsxs("div", { ...mergedTabListProps, ref, className: cn(tabListVariants({ layout }), className), children: [
3569
+ children,
3570
+ /* @__PURE__ */ jsx(
3571
+ "span",
3572
+ {
3573
+ "data-tab-indicator": true,
3574
+ "aria-hidden": "true",
3575
+ className: cn(
3576
+ tabIndicatorVariants({ variant }),
3577
+ // Hide until first position is calculated to avoid flash
3578
+ !indicatorReady && "opacity-0"
3579
+ ),
3580
+ style: {
3581
+ // Dynamic left/width values from DOM measurements
3582
+ left: `${indicatorStyle.left}px`,
3583
+ width: `${indicatorStyle.width}px`
3584
+ }
3585
+ }
3586
+ )
3587
+ ] });
3588
+ }
3589
+ );
3590
+ TabList.displayName = "TabList";
3591
+ function resolveBadgeDisplay(badge) {
3592
+ if (badge === void 0 || badge === false) return null;
3593
+ if (badge === true) return "dot";
3594
+ if (typeof badge === "number") {
3595
+ if (badge === 0) return null;
3596
+ return badge > 999 ? "999+" : String(badge);
3597
+ }
3598
+ return null;
3599
+ }
3600
+ var Tab = forwardRef(
3601
+ ({ id, icon, label, badge, isDisabled = false, disableRipple = false, className, ...htmlProps }, forwardedRef) => {
3602
+ const { state } = useHeadlessTabsContext("Tab");
3603
+ const { variant, layout } = useTabsContext();
3604
+ const internalRef = useRef(null);
3605
+ const ref = forwardedRef ?? internalRef;
3606
+ const {
3607
+ tabProps,
3608
+ isSelected,
3609
+ isDisabled: ariaIsDisabled
3610
+ } = useTab({ key: id, isDisabled }, state, ref);
3611
+ const { isFocusVisible, focusProps } = useFocusRing();
3612
+ const finalIsDisabled = isDisabled || ariaIsDisabled;
3613
+ const { onMouseDown: handleRipple, ripples } = useRipple({
3614
+ disabled: finalIsDisabled || disableRipple
3615
+ });
3616
+ const handleClick = useCallback(() => {
3617
+ if (!finalIsDisabled) {
3618
+ state.setSelectedKey(id);
3619
+ }
3620
+ }, [state, id, finalIsDisabled]);
3621
+ const handleFocus = useCallback(() => {
3622
+ if (!finalIsDisabled) {
3623
+ state.selectionManager.setFocusedKey(id);
3624
+ }
3625
+ }, [state.selectionManager, id, finalIsDisabled]);
3626
+ const mergedProps = mergeProps(tabProps, focusProps, {
3627
+ onMouseDown: disableRipple ? void 0 : handleRipple,
3628
+ onClick: handleClick,
3629
+ onFocus: handleFocus
3630
+ });
3631
+ const badgeDisplay = resolveBadgeDisplay(badge);
3632
+ const isDotBadge = badgeDisplay === "dot";
3633
+ const hasIcon = Boolean(icon);
3634
+ const hasLabel = Boolean(label);
3635
+ return /* @__PURE__ */ jsxs(
3636
+ "button",
3637
+ {
3638
+ ...mergedProps,
3639
+ ref,
3640
+ type: "button",
3641
+ "data-key": String(id),
3642
+ tabIndex: finalIsDisabled ? -1 : isSelected ? 0 : -1,
3643
+ className: cn(
3644
+ tabVariants({
3645
+ variant,
3646
+ selected: isSelected,
3647
+ disabled: finalIsDisabled,
3648
+ layout
3649
+ }),
3650
+ isFocusVisible && "outline-primary outline-2 outline-offset-2",
3651
+ hasLabel && hasIcon && "min-h-16",
3652
+ className
3653
+ ),
3654
+ ...htmlProps,
3655
+ children: [
3656
+ !disableRipple && ripples,
3657
+ hasIcon && /* @__PURE__ */ jsxs("span", { className: cn(tabIconVariants({ hasLabel })), children: [
3658
+ icon,
3659
+ badgeDisplay && /* @__PURE__ */ jsx(
3660
+ "span",
3661
+ {
3662
+ "data-badge-type": isDotBadge ? "dot" : "count",
3663
+ "aria-hidden": "true",
3664
+ className: cn(tabBadgeVariants({ type: isDotBadge ? "dot" : "count" })),
3665
+ children: !isDotBadge && badgeDisplay
3666
+ }
3667
+ )
3668
+ ] }),
3669
+ hasLabel && /* @__PURE__ */ jsxs("span", { className: "relative z-10 truncate", children: [
3670
+ label,
3671
+ !hasIcon && badgeDisplay && /* @__PURE__ */ jsx(
3672
+ "span",
3673
+ {
3674
+ "data-badge-type": isDotBadge ? "dot" : "count",
3675
+ "aria-hidden": "true",
3676
+ className: cn(
3677
+ "relative ml-1",
3678
+ tabBadgeVariants({ type: isDotBadge ? "dot" : "count" })
3679
+ ),
3680
+ children: !isDotBadge && badgeDisplay
3681
+ }
3682
+ )
3683
+ ] })
3684
+ ]
3685
+ }
3686
+ );
3687
+ }
3688
+ );
3689
+ Tab.displayName = "Tab";
3690
+ var TabPanel = forwardRef(
3691
+ ({ id, children, className }, forwardedRef) => {
3692
+ const { state } = useHeadlessTabsContext("TabPanel");
3693
+ useTabsContext();
3694
+ const internalRef = useRef(null);
3695
+ const ref = forwardedRef ?? internalRef;
3696
+ const { tabPanelProps } = useTabPanel({}, state, ref);
3697
+ if (state.selectedKey !== id) {
3698
+ return null;
3699
+ }
3700
+ return /* @__PURE__ */ jsx("div", { ...tabPanelProps, ref, className: cn(tabPanelVariants(), className), children });
3701
+ }
3702
+ );
3703
+ TabPanel.displayName = "TabPanel";
3704
+ var NavigationBarContext = createContext(null);
3705
+ function useNavigationBarContext() {
3706
+ const ctx = useContext(NavigationBarContext);
3707
+ if (ctx === null) {
3708
+ throw new Error("HeadlessNavigationBarItem must be rendered inside HeadlessNavigationBar.");
3709
+ }
3710
+ return ctx;
3711
+ }
3712
+ var HeadlessNavigationBar = forwardRef(
3713
+ ({
3714
+ items,
3715
+ selectedKey,
3716
+ defaultSelectedKey,
3717
+ onSelectionChange,
3718
+ "aria-label": ariaLabel,
3719
+ className,
3720
+ renderItem
3721
+ }, ref) => {
3722
+ const collectionChildren = items.map((item) => /* @__PURE__ */ jsx(Item, { textValue: item.label ?? item["aria-label"] ?? item.key, children: item.key }, item.key));
3723
+ const disabledKeys = items.filter((item) => item.isDisabled).map((item) => item.key);
3724
+ const state = useTabListState({
3725
+ children: collectionChildren,
3726
+ ...selectedKey !== void 0 ? { selectedKey } : {},
3727
+ ...defaultSelectedKey !== void 0 ? { defaultSelectedKey } : {},
3728
+ ...onSelectionChange ? { onSelectionChange } : {},
3729
+ disabledKeys
3730
+ });
3731
+ const tabListRef = useRef(null);
3732
+ const { tabListProps } = useTabList({ "aria-label": ariaLabel }, state, tabListRef);
3733
+ return /* @__PURE__ */ jsx(
3734
+ "nav",
3735
+ {
3736
+ ref,
3737
+ role: "navigation",
3738
+ "aria-label": ariaLabel,
3739
+ className,
3740
+ children: /* @__PURE__ */ jsx(NavigationBarContext.Provider, { value: { state, hideLabels: false, disableRipple: false }, children: /* @__PURE__ */ jsx("div", { ...tabListProps, ref: tabListRef, className: "flex h-full w-full items-stretch", children: [...state.collection].map((collectionItem) => {
3741
+ const itemConfig = items.find((i) => String(i.key) === String(collectionItem.key));
3742
+ if (!itemConfig) return null;
3743
+ return renderItem(itemConfig);
3744
+ }) }) })
3745
+ }
3746
+ );
3747
+ }
3748
+ );
3749
+ HeadlessNavigationBar.displayName = "HeadlessNavigationBar";
3750
+ var HeadlessNavigationBarItem = forwardRef(({ itemKey, children, className, "aria-label": ariaLabel }, ref) => {
3751
+ const { state } = useNavigationBarContext();
3752
+ const internalRef = useRef(null);
3753
+ const resolvedRef = ref ?? internalRef;
3754
+ const { tabProps, isSelected } = useTab({ key: itemKey }, state, resolvedRef);
3755
+ const { "aria-controls": _controls, ...tabPropsWithoutControls } = tabProps;
3756
+ const { focusProps, isFocusVisible } = useFocusRing();
3757
+ const content = typeof children === "function" ? children({ isSelected, isFocusVisible }) : children;
3758
+ return /* @__PURE__ */ jsx(
3759
+ "button",
3760
+ {
3761
+ type: "button",
3762
+ ...mergeProps(tabPropsWithoutControls, focusProps),
3763
+ ref: resolvedRef,
3764
+ className,
3765
+ "aria-label": ariaLabel,
3766
+ "data-focus-visible": isFocusVisible || void 0,
3767
+ "data-selected": isSelected,
3768
+ children: content
3769
+ }
3770
+ );
3771
+ });
3772
+ HeadlessNavigationBarItem.displayName = "HeadlessNavigationBarItem";
3773
+ var navigationBarVariants = cva([
3774
+ // Layout
3775
+ "fixed bottom-0 left-0 right-0 z-10",
3776
+ "w-full",
3777
+ "flex flex-row items-stretch",
3778
+ // MD3 surface
3779
+ "bg-surface-container",
3780
+ // MD3 height: 80dp
3781
+ "h-20",
3782
+ // Safe-area bottom (for mobile devices)
3783
+ "pb-safe"
3784
+ ]);
3785
+ var navigationBarItemVariants = cva(
3786
+ [
3787
+ // Layout
3788
+ "relative flex flex-1 flex-col items-center justify-center",
3789
+ "cursor-pointer select-none outline-none",
3790
+ // State layer pseudo-element (covers the full item area)
3791
+ "before:absolute before:inset-0 before:rounded-none before:transition-opacity before:duration-short2 before:ease-standard",
3792
+ // Focus-visible ring
3793
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary",
3794
+ // Transition for color changes
3795
+ "transition-colors duration-short2 ease-standard"
3796
+ ],
3797
+ {
3798
+ variants: {
3799
+ /**
3800
+ * Whether this item is the currently selected destination.
3801
+ * Controls icon and label colors per MD3 spec.
3802
+ */
3803
+ isActive: {
3804
+ true: [
3805
+ // Active icon color applied via text-color on the icon wrapper
3806
+ "[&>[data-icon]]:text-on-secondary-container",
3807
+ // Active label color
3808
+ "[&>[data-label]]:text-on-surface",
3809
+ // State layer color for active item
3810
+ "before:bg-on-surface-variant"
3811
+ ],
3812
+ false: [
3813
+ // Inactive icon color
3814
+ "[&>[data-icon]]:text-on-surface-variant",
3815
+ // Inactive label color
3816
+ "[&>[data-label]]:text-on-surface-variant",
3817
+ // State layer color for inactive item
3818
+ "before:bg-on-surface-variant"
3819
+ ]
3820
+ },
3821
+ /**
3822
+ * Whether the item is disabled.
3823
+ * Applies `opacity-38` per MD3 disabled state.
3824
+ */
3825
+ isDisabled: {
3826
+ true: ["cursor-not-allowed opacity-38 pointer-events-none"],
3827
+ false: []
3828
+ },
3829
+ /**
3830
+ * Hover and press state layer opacities.
3831
+ * Applied via compound variants on hover/active pseudo-classes.
3832
+ */
3833
+ isHovered: {
3834
+ true: ["before:opacity-8"],
3835
+ false: ["before:opacity-0"]
3836
+ },
3837
+ isPressed: {
3838
+ true: ["before:opacity-12"],
3839
+ false: []
3840
+ }
3841
+ },
3842
+ compoundVariants: [
3843
+ // When not hovered or pressed, state layer is invisible
3844
+ {
3845
+ isHovered: false,
3846
+ isPressed: false,
3847
+ className: "before:opacity-0"
3848
+ }
3849
+ ],
3850
+ defaultVariants: {
3851
+ isActive: false,
3852
+ isDisabled: false,
3853
+ isHovered: false,
3854
+ isPressed: false
3855
+ }
3856
+ }
3857
+ );
3858
+ var indicatorPillVariants = cva(
3859
+ [
3860
+ "absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2",
3861
+ "w-16 h-8",
3862
+ "rounded-full bg-secondary-container",
3863
+ "transition-[transform,opacity] duration-medium2 ease-emphasized",
3864
+ "origin-center"
3865
+ ],
3866
+ {
3867
+ variants: {
3868
+ isActive: {
3869
+ true: ["scale-x-100 opacity-100"],
3870
+ false: ["scale-x-0 opacity-0"]
3871
+ }
3872
+ },
3873
+ defaultVariants: {
3874
+ isActive: false
3875
+ }
3876
+ }
3877
+ );
3878
+ var badgeVariants = cva(
3879
+ [
3880
+ "absolute",
3881
+ "flex items-center justify-center",
3882
+ "bg-error text-on-error",
3883
+ "font-medium leading-none",
3884
+ // MD3 label-small
3885
+ "text-[0.6875rem]"
3886
+ ],
3887
+ {
3888
+ variants: {
3889
+ isDot: {
3890
+ true: [
3891
+ // Dot: 6dp diameter, top-right of icon
3892
+ "top-0 right-0.5 z-10",
3893
+ "w-1.5 h-1.5 min-w-0 rounded-full"
3894
+ ],
3895
+ false: [
3896
+ // Numeric: pill shape, top-right of icon
3897
+ "-top-1 left-3 z-10",
3898
+ "min-w-[1rem] h-4 px-1 rounded-full"
3899
+ ]
3900
+ }
3901
+ },
3902
+ defaultVariants: {
3903
+ isDot: false
3904
+ }
3905
+ }
3906
+ );
3907
+ var iconWrapperVariants = cva([
3908
+ "relative z-10 flex items-center justify-center",
3909
+ "w-6 h-6"
3910
+ ]);
3911
+ var labelVariants = cva([
3912
+ "mt-1 select-none",
3913
+ "text-label-medium",
3914
+ "transition-colors duration-short2 ease-standard",
3915
+ "truncate max-w-full"
3916
+ ]);
3917
+ var MIN_ITEMS = 3;
3918
+ var MAX_ITEMS = 5;
3919
+ function validateItemCount(count) {
3920
+ if (process.env.NODE_ENV !== "production" && (count < MIN_ITEMS || count > MAX_ITEMS)) {
3921
+ console.warn(
3922
+ `[NavigationBar] MD3 Navigation Bar requires between ${MIN_ITEMS} and ${MAX_ITEMS} destination items. Received ${count}. See: https://m3.material.io/components/navigation-bar/overview`
3923
+ );
3924
+ }
3925
+ }
3926
+ function getBadgeText(badge) {
3927
+ if (badge === void 0 || badge === 0) return null;
3928
+ if (badge === true) return null;
3929
+ if (badge > 999) return "999+";
3930
+ return String(badge);
3931
+ }
3932
+ function isBadgeVisible(badge) {
3933
+ if (badge === void 0) return false;
3934
+ if (badge === 0) return false;
3935
+ return true;
3936
+ }
3937
+ function ItemVisual({ config, isActive, hideLabels, disableRipple }) {
3938
+ const isItemDisabled = config.isDisabled === true;
3939
+ const { onMouseDown, ripples } = useRipple({
3940
+ ...disableRipple || isItemDisabled ? { disabled: true } : {}
3941
+ });
3942
+ const showBadge = isBadgeVisible(config.badge);
3943
+ const isDot = config.badge === true;
3944
+ const badgeText = getBadgeText(config.badge);
3945
+ return (
3946
+ // Overflow-hidden wrapper required for ripple containment.
3947
+ // pointer-events-none is intentional: the parent <button> handles all interaction.
3948
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions
3949
+ /* @__PURE__ */ jsxs(
3950
+ "span",
3951
+ {
3952
+ onMouseDown,
3953
+ className: "pointer-events-none relative flex h-full w-full flex-col items-center justify-center overflow-hidden",
3954
+ children: [
3955
+ ripples,
3956
+ /* @__PURE__ */ jsx(
3957
+ "span",
3958
+ {
3959
+ "data-indicator-pill": true,
3960
+ "data-active": isActive,
3961
+ "aria-hidden": "true",
3962
+ className: cn(indicatorPillVariants({ isActive }), !hideLabels && "-mt-3.5")
3963
+ }
3964
+ ),
3965
+ /* @__PURE__ */ jsxs(
3966
+ "span",
3967
+ {
3968
+ "data-icon": true,
3969
+ className: cn(
3970
+ iconWrapperVariants(),
3971
+ isActive ? "text-on-secondary-container" : "text-on-surface-variant"
3972
+ ),
3973
+ children: [
3974
+ /* @__PURE__ */ jsx("span", { className: "relative z-10 flex h-6 w-6 items-center justify-center", children: config.icon }),
3975
+ showBadge && /* @__PURE__ */ jsx(
3976
+ "span",
3977
+ {
3978
+ "data-badge": true,
3979
+ "data-badge-dot": isDot || void 0,
3980
+ "aria-label": isDot ? "notification" : badgeText ? `${badgeText} notifications` : void 0,
3981
+ "aria-live": "polite",
3982
+ className: cn(badgeVariants({ isDot })),
3983
+ children: isDot ? null : badgeText
3984
+ }
3985
+ )
3986
+ ]
3987
+ }
3988
+ ),
3989
+ !hideLabels && config.label && /* @__PURE__ */ jsx(
3990
+ "span",
3991
+ {
3992
+ "data-label": true,
3993
+ className: cn(labelVariants(), isActive ? "text-on-surface" : "text-on-surface-variant"),
3994
+ children: config.label
3995
+ }
3996
+ )
3997
+ ]
3998
+ }
3999
+ )
4000
+ );
4001
+ }
4002
+ var NavigationBar = forwardRef(
4003
+ ({
4004
+ items,
4005
+ activeKey,
4006
+ defaultActiveKey,
4007
+ onActiveChange,
4008
+ hideLabels = false,
4009
+ "aria-label": ariaLabel,
4010
+ disableRipple = false,
4011
+ className
4012
+ }, ref) => {
4013
+ validateItemCount(items.length);
4014
+ return /* @__PURE__ */ jsx(
4015
+ HeadlessNavigationBar,
4016
+ {
4017
+ ref,
4018
+ items,
4019
+ ...activeKey !== void 0 ? { selectedKey: activeKey } : {},
4020
+ ...defaultActiveKey !== void 0 ? { defaultSelectedKey: defaultActiveKey } : {},
4021
+ ...onActiveChange ? { onSelectionChange: onActiveChange } : {},
4022
+ "aria-label": ariaLabel,
4023
+ className: cn(navigationBarVariants(), className),
4024
+ renderItem: (config) => (
4025
+ // HeadlessNavigationBarItem renders the <button role="tab"> with all
4026
+ // ARIA semantics. ItemVisual renders the icon/pill/badge/label inside
4027
+ // — no nested <button> elements.
4028
+ /* @__PURE__ */ jsx(
4029
+ HeadlessNavigationBarItem,
4030
+ {
4031
+ itemKey: config.key,
4032
+ ...config["aria-label"] !== void 0 ? { "aria-label": config["aria-label"] } : {},
4033
+ className: cn(
4034
+ "relative flex flex-1 flex-col items-center justify-center",
4035
+ "cursor-pointer outline-none select-none",
4036
+ "duration-short2 ease-standard transition-colors",
4037
+ // State layer pseudo-element
4038
+ "before:absolute before:inset-0 before:rounded-none",
4039
+ "before:bg-on-surface-variant before:opacity-0",
4040
+ "before:duration-short2 before:ease-standard before:transition-opacity",
4041
+ "hover:before:opacity-8",
4042
+ "active:before:opacity-12",
4043
+ // Focus ring
4044
+ "focus-visible:outline-primary focus-visible:outline-2 focus-visible:outline-offset-2",
4045
+ // Disabled styling
4046
+ config.isDisabled && "pointer-events-none cursor-not-allowed opacity-38"
4047
+ ),
4048
+ children: ({ isSelected }) => /* @__PURE__ */ jsx(
4049
+ ItemVisual,
4050
+ {
4051
+ config,
4052
+ isActive: isSelected,
4053
+ hideLabels,
4054
+ disableRipple
4055
+ }
4056
+ )
4057
+ },
4058
+ config.key
4059
+ )
4060
+ )
4061
+ }
4062
+ );
4063
+ }
4064
+ );
4065
+ NavigationBar.displayName = "NavigationBar";
4066
+ function getBadgeContent(badge) {
4067
+ if (badge === true) return null;
4068
+ if (badge === 0) return "";
4069
+ if (badge > 999) return "999+";
4070
+ return String(badge);
4071
+ }
4072
+ function isBadgeVisible2(badge) {
4073
+ if (badge === void 0) return false;
4074
+ if (badge === 0) return false;
4075
+ return true;
4076
+ }
4077
+ var NavigationBarItem = forwardRef(
4078
+ ({
4079
+ itemKey: _itemKey,
4080
+ icon,
4081
+ label,
4082
+ badge,
4083
+ isActive = false,
4084
+ hideLabels = false,
4085
+ isDisabled = false,
4086
+ disableRipple = false,
4087
+ className,
4088
+ "aria-label": ariaLabel,
4089
+ // Spread remaining props (e.g. tabProps from useTab)
4090
+ ...rest
4091
+ }, ref) => {
4092
+ const { onMouseDown, ripples } = useRipple({
4093
+ ...disableRipple || isDisabled ? { disabled: true } : {}
4094
+ });
4095
+ const showBadge = isBadgeVisible2(badge);
4096
+ const isDot = badge === true;
4097
+ const badgeContent = badge !== void 0 ? getBadgeContent(badge) : null;
4098
+ return /* @__PURE__ */ jsx(
4099
+ "button",
4100
+ {
4101
+ type: "button",
4102
+ ref,
4103
+ onMouseDown,
4104
+ disabled: isDisabled,
4105
+ "aria-label": ariaLabel,
4106
+ className: cn(
4107
+ navigationBarItemVariants({
4108
+ isActive,
4109
+ isDisabled
4110
+ }),
4111
+ className
4112
+ ),
4113
+ ...rest,
4114
+ children: /* @__PURE__ */ jsxs("span", { className: "relative flex h-full w-full flex-col items-center justify-center overflow-hidden rounded-none", children: [
4115
+ ripples,
4116
+ /* @__PURE__ */ jsx(
4117
+ "span",
4118
+ {
4119
+ "data-indicator-pill": true,
4120
+ "data-active": isActive,
4121
+ className: cn(indicatorPillVariants({ isActive }), !hideLabels && "-mt-3.5"),
4122
+ "aria-hidden": "true"
4123
+ }
4124
+ ),
4125
+ /* @__PURE__ */ jsxs("span", { "data-icon": true, className: cn(iconWrapperVariants()), children: [
4126
+ /* @__PURE__ */ jsx("span", { className: "relative z-10 flex h-6 w-6 items-center justify-center", children: icon }),
4127
+ showBadge && /* @__PURE__ */ jsx(
4128
+ "span",
4129
+ {
4130
+ "data-badge": true,
4131
+ "data-badge-dot": isDot || void 0,
4132
+ "aria-label": isDot ? "notification" : badgeContent ? `${badgeContent} notifications` : void 0,
4133
+ "aria-live": "polite",
4134
+ className: cn(badgeVariants({ isDot })),
4135
+ children: isDot ? null : badgeContent
4136
+ }
4137
+ )
4138
+ ] }),
4139
+ !hideLabels && label && /* @__PURE__ */ jsx("span", { "data-label": true, className: cn(labelVariants()), children: label })
4140
+ ] })
4141
+ }
4142
+ );
4143
+ }
4144
+ );
4145
+ NavigationBarItem.displayName = "NavigationBarItem";
4146
+ var DrawerContext = createContext(null);
4147
+ var HeadlessDrawer = forwardRef(
4148
+ ({
4149
+ variant = "standard",
4150
+ open,
4151
+ defaultOpen = false,
4152
+ onOpenChange,
4153
+ "aria-label": ariaLabel,
4154
+ children,
4155
+ className,
4156
+ scrimClassName,
4157
+ disableRipple = false
4158
+ }, ref) => {
4159
+ const state = useOverlayTriggerState({
4160
+ ...open !== void 0 ? { isOpen: open } : {},
4161
+ ...defaultOpen !== void 0 ? { defaultOpen } : {},
4162
+ ...onOpenChange !== void 0 ? { onOpenChange } : {}
4163
+ });
4164
+ const isOpen = state.isOpen;
4165
+ const close = useCallback(() => {
4166
+ state.close();
4167
+ }, [state]);
4168
+ const contextValue = {
4169
+ isOpen,
4170
+ close,
4171
+ disableRipple
4172
+ };
4173
+ if (variant === "modal") {
4174
+ return /* @__PURE__ */ jsx(DrawerContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx("nav", { ref, role: "navigation", "aria-label": ariaLabel, children: isOpen && /* @__PURE__ */ jsxs(Fragment, { children: [
4175
+ /* @__PURE__ */ jsx(
4176
+ "div",
4177
+ {
4178
+ "data-testid": "drawer-scrim",
4179
+ className: scrimClassName,
4180
+ onClick: () => state.close(),
4181
+ "aria-hidden": "true"
4182
+ }
4183
+ ),
4184
+ /* @__PURE__ */ jsx(FocusScope, { contain: true, restoreFocus: true, autoFocus: true, children: /* @__PURE__ */ jsx(
4185
+ ModalDrawerPanel,
4186
+ {
4187
+ ariaLabel,
4188
+ onClose: () => state.close(),
4189
+ className,
4190
+ children
4191
+ }
4192
+ ) })
4193
+ ] }) }) });
4194
+ }
4195
+ return /* @__PURE__ */ jsx(DrawerContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
4196
+ "nav",
4197
+ {
4198
+ ref,
4199
+ role: "navigation",
4200
+ "aria-label": ariaLabel,
4201
+ className,
4202
+ children
4203
+ }
4204
+ ) });
4205
+ }
4206
+ );
4207
+ HeadlessDrawer.displayName = "HeadlessDrawer";
4208
+ var ModalDrawerPanel = ({
4209
+ ariaLabel,
4210
+ onClose,
4211
+ className,
4212
+ children
4213
+ }) => {
4214
+ const panelRef = useRef(null);
4215
+ usePreventScroll();
4216
+ const { dialogProps } = useDialog({ "aria-label": ariaLabel }, panelRef);
4217
+ const { overlayProps } = useOverlay(
4218
+ {
4219
+ isOpen: true,
4220
+ onClose,
4221
+ isDismissable: true,
4222
+ shouldCloseOnBlur: false
4223
+ },
4224
+ panelRef
4225
+ );
4226
+ return /* @__PURE__ */ jsx(
4227
+ "div",
4228
+ {
4229
+ ...mergeProps(overlayProps, dialogProps),
4230
+ ref: panelRef,
4231
+ className,
4232
+ "aria-modal": "true",
4233
+ children
4234
+ }
4235
+ );
4236
+ };
4237
+ ModalDrawerPanel.displayName = "ModalDrawerPanel";
4238
+ var HeadlessDrawerItem = forwardRef(
4239
+ ({
4240
+ href,
4241
+ isActive = false,
4242
+ children,
4243
+ className,
4244
+ isDisabled,
4245
+ onMouseDown,
4246
+ onPress,
4247
+ onPressStart,
4248
+ onPressEnd,
4249
+ onPressChange,
4250
+ onPressUp,
4251
+ ...restProps
4252
+ }, forwardedRef) => {
4253
+ const internalRef = useRef(null);
4254
+ const { isFocusVisible, focusProps } = useFocusRing();
4255
+ if (href) {
4256
+ const linkRef = forwardedRef ?? internalRef;
4257
+ const { linkProps } = useLink(
4258
+ {
4259
+ href,
4260
+ ...isDisabled !== void 0 ? { isDisabled } : {},
4261
+ ...onPress !== void 0 ? { onPress } : {}
4262
+ },
4263
+ linkRef
4264
+ );
4265
+ return /* @__PURE__ */ jsx(
4266
+ "a",
4267
+ {
4268
+ ...mergeProps(linkProps, focusProps, { onMouseDown }),
4269
+ ref: linkRef,
4270
+ href,
4271
+ className,
4272
+ "aria-current": isActive ? "page" : void 0,
4273
+ "data-focus-visible": isFocusVisible || void 0,
4274
+ "data-active": isActive || void 0,
4275
+ children
4276
+ }
4277
+ );
4278
+ }
4279
+ const buttonRef = forwardedRef ?? internalRef;
4280
+ const { buttonProps } = useButton(
4281
+ {
4282
+ ...restProps,
4283
+ ...isDisabled !== void 0 ? { isDisabled } : {},
4284
+ ...onPress !== void 0 ? { onPress } : {},
4285
+ ...onPressStart !== void 0 ? { onPressStart } : {},
4286
+ ...onPressEnd !== void 0 ? { onPressEnd } : {},
4287
+ ...onPressChange !== void 0 ? { onPressChange } : {},
4288
+ ...onPressUp !== void 0 ? { onPressUp } : {},
4289
+ elementType: "button"
4290
+ },
4291
+ buttonRef
4292
+ );
4293
+ return /* @__PURE__ */ jsx(
4294
+ "button",
4295
+ {
4296
+ type: "button",
4297
+ ...mergeProps(buttonProps, focusProps, { onMouseDown }),
4298
+ ref: buttonRef,
4299
+ className,
4300
+ "aria-current": isActive ? "page" : void 0,
4301
+ "data-focus-visible": isFocusVisible || void 0,
4302
+ "data-active": isActive || void 0,
4303
+ children
4304
+ }
4305
+ );
4306
+ }
4307
+ );
4308
+ HeadlessDrawerItem.displayName = "HeadlessDrawerItem";
4309
+ var drawerVariants = cva(
4310
+ [
4311
+ // Layout
4312
+ "fixed top-0 left-0 h-full w-drawer",
4313
+ "flex flex-col overflow-y-auto",
4314
+ // Stacking and shape
4315
+ "z-50",
4316
+ "rounded-r-xl",
4317
+ // Slide animation (transition applies to all open/closed state changes)
4318
+ "transition-transform duration-medium4 ease-emphasized-decelerate",
4319
+ // Focus outline removal (focus management handled by FocusScope / React Aria)
4320
+ "outline-none",
4321
+ // Padding for content spacing
4322
+ "px-3"
4323
+ ],
4324
+ {
4325
+ variants: {
4326
+ /**
4327
+ * Structural variant — drives surface color and elevation.
4328
+ * - `standard`: inline nav panel, lower-elevation surface
4329
+ * - `modal`: overlay dialog with elevation shadow
4330
+ */
4331
+ variant: {
4332
+ standard: ["bg-surface-container-low"],
4333
+ modal: ["bg-surface-container", "shadow-elevation-1"]
4334
+ },
4335
+ /**
4336
+ * Open/closed state — drives translation.
4337
+ * - `true`: drawer visible (`translate-x-0`)
4338
+ * - `false`: drawer off-screen (`-translate-x-full`)
4339
+ */
4340
+ open: {
4341
+ true: ["translate-x-0"],
4342
+ false: ["-translate-x-full"]
4343
+ }
4344
+ },
4345
+ defaultVariants: {
4346
+ variant: "standard",
4347
+ open: false
4348
+ }
4349
+ }
4350
+ );
4351
+ var drawerItemVariants = cva(
4352
+ [
4353
+ // Layout
4354
+ "relative flex w-full items-center gap-3",
4355
+ "h-14 px-4",
4356
+ "rounded-full",
4357
+ // Typography
4358
+ "text-label-large",
4359
+ // Interaction
4360
+ "cursor-pointer select-none outline-none",
4361
+ // State layer pseudo-element
4362
+ "before:absolute before:inset-0 before:rounded-full",
4363
+ "before:transition-opacity before:duration-short2 before:ease-standard",
4364
+ "before:opacity-0",
4365
+ // Hover and focus visible state layers
4366
+ "hover:before:opacity-8",
4367
+ "focus-visible:before:opacity-12",
4368
+ // Active pressed state
4369
+ "active:before:opacity-12",
4370
+ // Transition for color changes
4371
+ "transition-colors duration-short2 ease-standard"
4372
+ ],
4373
+ {
4374
+ variants: {
4375
+ /**
4376
+ * Whether this item is the currently active destination.
4377
+ * Controls background, text color, and icon color per MD3 spec.
4378
+ */
4379
+ isActive: {
4380
+ true: [
4381
+ "bg-secondary-container",
4382
+ "text-on-secondary-container",
4383
+ "before:bg-on-secondary-container"
4384
+ ],
4385
+ false: ["bg-transparent", "text-on-surface-variant", "before:bg-on-surface-variant"]
4386
+ },
4387
+ /**
4388
+ * Whether the item is disabled.
4389
+ * Applies `opacity-38` per MD3 disabled state spec.
4390
+ */
4391
+ isDisabled: {
4392
+ true: ["opacity-38 cursor-not-allowed pointer-events-none"],
4393
+ false: []
4394
+ }
4395
+ },
4396
+ defaultVariants: {
4397
+ isActive: false,
4398
+ isDisabled: false
4399
+ }
4400
+ }
4401
+ );
4402
+ var scrimVariants = cva([
4403
+ "fixed inset-0 z-40",
4404
+ "bg-scrim opacity-32",
4405
+ "transition-opacity duration-medium2 ease-standard"
4406
+ ]);
4407
+ var drawerSectionVariants = cva(["flex flex-col w-full"]);
4408
+ var drawerSectionHeaderVariants = cva([
4409
+ "px-4 pt-4 pb-2",
4410
+ "text-title-small text-on-surface-variant",
4411
+ "select-none"
4412
+ ]);
4413
+ var drawerDividerVariants = cva(["border-t border-outline-variant", "mx-4 my-2"]);
4414
+ var Drawer = forwardRef(
4415
+ ({
4416
+ variant = "standard",
4417
+ open,
4418
+ defaultOpen = false,
4419
+ onOpenChange,
4420
+ "aria-label": ariaLabel,
4421
+ children,
4422
+ className,
4423
+ disableRipple = false,
4424
+ ...restProps
4425
+ }, ref) => {
4426
+ const isOpen = open ?? defaultOpen;
4427
+ const drawerPanelClass = cn(
4428
+ drawerVariants({
4429
+ variant,
4430
+ open: isOpen
4431
+ }),
4432
+ className
4433
+ );
4434
+ const scrimClass = scrimVariants();
4435
+ return /* @__PURE__ */ jsx(
4436
+ HeadlessDrawer,
4437
+ {
4438
+ ref,
4439
+ variant,
4440
+ ...open !== void 0 ? { open } : {},
4441
+ ...defaultOpen !== void 0 ? { defaultOpen } : {},
4442
+ ...onOpenChange !== void 0 ? { onOpenChange } : {},
4443
+ "aria-label": ariaLabel,
4444
+ className: drawerPanelClass,
4445
+ scrimClassName: scrimClass,
4446
+ disableRipple,
4447
+ ...restProps,
4448
+ children
4449
+ }
4450
+ );
4451
+ }
4452
+ );
4453
+ Drawer.displayName = "Drawer";
4454
+ var DrawerItem = forwardRef(
4455
+ ({
4456
+ href,
4457
+ icon,
4458
+ label,
4459
+ badge,
4460
+ secondaryText,
4461
+ isActive = false,
4462
+ isDisabled = false,
4463
+ disableRipple = false,
4464
+ className,
4465
+ onPress,
4466
+ onPressStart,
4467
+ onPressEnd,
4468
+ onPressChange,
4469
+ onPressUp,
4470
+ ...restProps
4471
+ }, ref) => {
4472
+ const isItemDisabled = isDisabled;
4473
+ const { onMouseDown: handleRipple, ripples } = useRipple({
4474
+ disabled: isItemDisabled || disableRipple
4475
+ });
4476
+ return /* @__PURE__ */ jsxs(
4477
+ HeadlessDrawerItem,
4478
+ {
4479
+ ...restProps,
4480
+ ref,
4481
+ ...href !== void 0 ? { href } : {},
4482
+ isActive,
4483
+ ...isItemDisabled !== void 0 ? { isDisabled: isItemDisabled } : {},
4484
+ ...onPress !== void 0 ? { onPress } : {},
4485
+ ...onPressStart !== void 0 ? { onPressStart } : {},
4486
+ ...onPressEnd !== void 0 ? { onPressEnd } : {},
4487
+ ...onPressChange !== void 0 ? { onPressChange } : {},
4488
+ ...onPressUp !== void 0 ? { onPressUp } : {},
4489
+ onMouseDown: handleRipple,
4490
+ className: cn(
4491
+ drawerItemVariants({
4492
+ isActive,
4493
+ isDisabled: isItemDisabled
4494
+ }),
4495
+ className
4496
+ ),
4497
+ children: [
4498
+ ripples,
4499
+ icon && /* @__PURE__ */ jsx(
4500
+ "span",
4501
+ {
4502
+ className: "relative z-10 flex shrink-0 items-center justify-center",
4503
+ "aria-hidden": "true",
4504
+ children: icon
4505
+ }
4506
+ ),
4507
+ /* @__PURE__ */ jsxs("span", { className: "relative z-10 flex min-w-0 flex-1 flex-col text-left", children: [
4508
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: label }),
4509
+ secondaryText && /* @__PURE__ */ jsx("span", { className: "text-body-small truncate opacity-70", children: secondaryText })
4510
+ ] }),
4511
+ badge && /* @__PURE__ */ jsx(
4512
+ "span",
4513
+ {
4514
+ className: "relative z-10 ml-auto flex shrink-0 items-center pr-2",
4515
+ "aria-hidden": "true",
4516
+ children: badge
4517
+ }
4518
+ )
4519
+ ]
4520
+ }
4521
+ );
4522
+ }
4523
+ );
4524
+ DrawerItem.displayName = "DrawerItem";
4525
+ var DrawerSection = forwardRef(
4526
+ ({ header, children, showDivider = false, className }, ref) => {
4527
+ return /* @__PURE__ */ jsxs("div", { ref, className: cn(drawerSectionVariants(), className), children: [
4528
+ showDivider && /* @__PURE__ */ jsx("hr", { role: "separator", "aria-hidden": "true", className: drawerDividerVariants() }),
4529
+ header && /* @__PURE__ */ jsx("span", { className: drawerSectionHeaderVariants(), children: header }),
4530
+ children
4531
+ ] });
4532
+ }
4533
+ );
4534
+ DrawerSection.displayName = "DrawerSection";
4535
+ var progressContainerVariants = cva(["inline-flex", "flex-col", "gap-1"], {
4536
+ variants: {
4537
+ /**
4538
+ * The visual type of the indicator.
4539
+ */
4540
+ type: {
4541
+ linear: "w-full",
4542
+ circular: "items-center justify-center w-auto"
4543
+ }
4544
+ },
4545
+ defaultVariants: {
4546
+ type: "linear"
4547
+ }
4548
+ });
4549
+ var progressTrackVariants = cva([
4550
+ "relative",
4551
+ "w-full",
4552
+ "h-1",
4553
+ // MD3: 4dp track height
4554
+ "rounded-full",
4555
+ // MD3: full corner radius
4556
+ "overflow-hidden",
4557
+ "bg-surface-container-highest"
4558
+ // MD3: inactive track color
4559
+ ]);
4560
+ var progressIndicatorVariants = cva([
4561
+ "absolute",
4562
+ "left-0",
4563
+ "top-0",
4564
+ "h-full",
4565
+ "rounded-full",
4566
+ "bg-primary",
4567
+ // MD3: active track color
4568
+ "transition-[width]",
4569
+ "duration-medium4",
4570
+ // MD3: 400ms for value transitions
4571
+ "ease-standard"
4572
+ // MD3: cubic-bezier(0.2, 0, 0, 1)
4573
+ ]);
4574
+ var progressStopIndicatorVariants = cva([
4575
+ "absolute",
4576
+ "right-0",
4577
+ "top-1/2",
4578
+ "-translate-y-1/2",
4579
+ "w-1",
4580
+ "h-1",
4581
+ "rounded-full",
4582
+ "bg-primary"
4583
+ // MD3: stop indicator uses primary color
4584
+ ]);
4585
+ var progressCircularSizeVariants = cva(
4586
+ ["relative", "flex", "items-center", "justify-center", "flex-shrink-0"],
4587
+ {
4588
+ variants: {
4589
+ size: {
4590
+ small: "h-6 w-6",
4591
+ // MD3: 24dp
4592
+ medium: "h-12 w-12",
4593
+ // MD3: 48dp (default)
4594
+ large: "h-16 w-16"
4595
+ // MD3: 64dp
4596
+ }
4597
+ },
4598
+ defaultVariants: {
4599
+ size: "medium"
4600
+ }
4601
+ }
4602
+ );
4603
+ var progressLabelVariants = cva([
4604
+ "text-body-small",
4605
+ // MD3: body-small type scale (12px)
4606
+ "text-on-surface",
4607
+ // MD3: on-surface color role
4608
+ "select-none"
4609
+ ]);
4610
+ var STROKE_WIDTH = 4;
4611
+ var CIRCULAR_SIZE_PX = {
4612
+ small: 24,
4613
+ medium: 48,
4614
+ large: 64
4615
+ };
4616
+ function getCircularGeometry(size) {
4617
+ const diameter = CIRCULAR_SIZE_PX[size];
4618
+ const radius = (diameter - STROKE_WIDTH) / 2;
4619
+ const circumference = 2 * Math.PI * radius;
4620
+ const viewBox = `0 0 ${diameter} ${diameter}`;
4621
+ const cx = diameter / 2;
4622
+ const cy = diameter / 2;
4623
+ return { diameter, radius, circumference, viewBox, cx, cy };
4624
+ }
4625
+ var Progress = forwardRef(
4626
+ ({
4627
+ type = "linear",
4628
+ indeterminate = false,
4629
+ size = "medium",
4630
+ className,
4631
+ label,
4632
+ value = 0,
4633
+ minValue = 0,
4634
+ maxValue = 100,
4635
+ ...restProps
4636
+ }, forwardedRef) => {
4637
+ const internalRef = useRef(null);
4638
+ const ref = forwardedRef ?? internalRef;
4639
+ const { progressBarProps, labelProps } = useProgressBar({
4640
+ label,
4641
+ value,
4642
+ minValue,
4643
+ maxValue,
4644
+ isIndeterminate: indeterminate,
4645
+ ...restProps
4646
+ });
4647
+ const percentage = indeterminate ? 0 : Math.min(100, Math.max(0, (value - minValue) / (maxValue - minValue) * 100));
4648
+ if (process.env.NODE_ENV !== "production") {
4649
+ const ariaProps = restProps;
4650
+ if (!label && !ariaProps["aria-label"] && !ariaProps["aria-labelledby"]) {
4651
+ console.warn(
4652
+ "[Progress] Progress indicator should have a visible label prop or aria-label for accessibility."
4653
+ );
4654
+ }
4655
+ }
4656
+ return /* @__PURE__ */ jsxs(
4657
+ "div",
4658
+ {
4659
+ ...progressBarProps,
4660
+ ref,
4661
+ className: cn(progressContainerVariants({ type }), className),
4662
+ children: [
4663
+ label && /* @__PURE__ */ jsx("span", { ...labelProps, className: cn(progressLabelVariants()), children: label }),
4664
+ type === "linear" ? /* @__PURE__ */ jsx(LinearProgress, { percentage, indeterminate }) : /* @__PURE__ */ jsx(CircularProgress, { percentage, indeterminate, size })
4665
+ ]
4666
+ }
4667
+ );
4668
+ }
4669
+ );
4670
+ Progress.displayName = "Progress";
4671
+ function LinearProgress({ percentage, indeterminate }) {
4672
+ if (indeterminate) {
4673
+ return /* @__PURE__ */ jsx("div", { "data-progress-track": "", className: cn(progressTrackVariants()), children: /* @__PURE__ */ jsxs(
4674
+ "div",
4675
+ {
4676
+ "data-progress-indeterminate": "",
4677
+ className: "absolute inset-0 overflow-hidden rounded-full",
4678
+ children: [
4679
+ /* @__PURE__ */ jsx(
4680
+ "div",
4681
+ {
4682
+ className: cn(
4683
+ "bg-primary absolute top-0 h-full rounded-full",
4684
+ "animate-progress-linear-indeterminate-1"
4685
+ )
4686
+ }
4687
+ ),
4688
+ /* @__PURE__ */ jsx(
4689
+ "div",
4690
+ {
4691
+ className: cn(
4692
+ "bg-primary absolute top-0 h-full rounded-full",
4693
+ "animate-progress-linear-indeterminate-2"
4694
+ )
4695
+ }
4696
+ )
4697
+ ]
4698
+ }
4699
+ ) });
4700
+ }
4701
+ return /* @__PURE__ */ jsxs("div", { "data-progress-track": "", className: cn(progressTrackVariants()), children: [
4702
+ /* @__PURE__ */ jsx(
4703
+ "div",
4704
+ {
4705
+ "data-progress-indicator": "",
4706
+ className: cn(progressIndicatorVariants()),
4707
+ style: { width: `${percentage}%` }
4708
+ }
4709
+ ),
4710
+ /* @__PURE__ */ jsx(
4711
+ "div",
4712
+ {
4713
+ "data-stop-indicator": "",
4714
+ className: cn(progressStopIndicatorVariants()),
4715
+ "aria-hidden": "true"
4716
+ }
4717
+ )
4718
+ ] });
4719
+ }
4720
+ function CircularProgress({
4721
+ percentage,
4722
+ indeterminate,
4723
+ size
4724
+ }) {
4725
+ const { radius, circumference, viewBox, cx, cy } = getCircularGeometry(size);
4726
+ const strokeDashoffset = (1 - percentage / 100) * circumference;
4727
+ if (indeterminate) {
4728
+ return /* @__PURE__ */ jsx(
4729
+ "div",
4730
+ {
4731
+ "data-progress-size": size,
4732
+ "data-progress-indeterminate": "",
4733
+ className: cn(progressCircularSizeVariants({ size })),
4734
+ children: /* @__PURE__ */ jsxs(
4735
+ "svg",
4736
+ {
4737
+ viewBox,
4738
+ className: "animate-progress-circular-rotate h-full w-full",
4739
+ "aria-hidden": "true",
4740
+ children: [
4741
+ /* @__PURE__ */ jsx(
4742
+ "circle",
4743
+ {
4744
+ cx,
4745
+ cy,
4746
+ r: radius,
4747
+ fill: "none",
4748
+ stroke: "currentColor",
4749
+ strokeWidth: STROKE_WIDTH,
4750
+ className: "text-surface-container-highest"
4751
+ }
4752
+ ),
4753
+ /* @__PURE__ */ jsx(
4754
+ "circle",
4755
+ {
4756
+ cx,
4757
+ cy,
4758
+ r: radius,
4759
+ fill: "none",
4760
+ stroke: "currentColor",
4761
+ strokeWidth: STROKE_WIDTH,
4762
+ className: "animate-progress-circular-dash text-primary",
4763
+ strokeLinecap: "round"
4764
+ }
4765
+ )
4766
+ ]
4767
+ }
4768
+ )
4769
+ }
4770
+ );
4771
+ }
4772
+ return /* @__PURE__ */ jsx("div", { "data-progress-size": size, className: cn(progressCircularSizeVariants({ size })), children: /* @__PURE__ */ jsxs("svg", { viewBox, className: "h-full w-full -rotate-90", "aria-hidden": "true", children: [
4773
+ /* @__PURE__ */ jsx(
4774
+ "circle",
4775
+ {
4776
+ cx,
4777
+ cy,
4778
+ r: radius,
4779
+ fill: "none",
4780
+ stroke: "currentColor",
4781
+ strokeWidth: STROKE_WIDTH,
4782
+ className: "text-surface-container-highest"
4783
+ }
4784
+ ),
4785
+ /* @__PURE__ */ jsx(
4786
+ "circle",
4787
+ {
4788
+ cx,
4789
+ cy,
4790
+ r: radius,
4791
+ fill: "none",
4792
+ stroke: "currentColor",
4793
+ strokeWidth: STROKE_WIDTH,
4794
+ strokeLinecap: "round",
4795
+ className: "text-primary duration-medium4 ease-standard transition-[stroke-dashoffset]",
4796
+ strokeDasharray: circumference,
4797
+ strokeDashoffset
4798
+ }
4799
+ )
4800
+ ] }) });
4801
+ }
4802
+ var ProgressHeadless = forwardRef(
4803
+ ({
4804
+ type = "linear",
4805
+ indeterminate = false,
4806
+ size = "medium",
4807
+ className,
4808
+ children,
4809
+ renderProgress,
4810
+ label,
4811
+ value = 0,
4812
+ minValue = 0,
4813
+ maxValue = 100,
4814
+ ...restProps
4815
+ }, forwardedRef) => {
4816
+ const internalRef = useRef(null);
4817
+ const ref = forwardedRef ?? internalRef;
4818
+ const { progressBarProps, labelProps } = useProgressBar({
4819
+ label,
4820
+ value,
4821
+ minValue,
4822
+ maxValue,
4823
+ isIndeterminate: indeterminate,
4824
+ ...restProps
4825
+ });
4826
+ const percentage = indeterminate ? 0 : (value - minValue) / (maxValue - minValue) * 100;
4827
+ return /* @__PURE__ */ jsxs("div", { ...progressBarProps, ref, className, children: [
4828
+ label && /* @__PURE__ */ jsx("span", { ...labelProps, children: label }),
4829
+ renderProgress?.({
4830
+ percentage,
4831
+ isIndeterminate: indeterminate,
4832
+ type,
4833
+ size
4834
+ }),
4835
+ children
4836
+ ] });
4837
+ }
4838
+ );
4839
+ ProgressHeadless.displayName = "ProgressHeadless";
4840
+ var MenuContext = createContext(null);
4841
+ function useMenuContext() {
4842
+ return useContext(MenuContext);
4843
+ }
4844
+ function TriggerBridge({ children }) {
4845
+ const ctx = useSlottedContext(ButtonContext);
4846
+ const localRef = useRef(null);
4847
+ const { ref: contextRef, ...ctxProps } = ctx ?? {};
4848
+ const mergedCallbackRef = useCallback(
4849
+ (node) => {
4850
+ localRef.current = node;
4851
+ if (!contextRef) return;
4852
+ if (typeof contextRef === "function") {
4853
+ contextRef(node);
4854
+ } else {
4855
+ contextRef.current = node;
4856
+ }
4857
+ },
4858
+ [contextRef]
4859
+ );
4860
+ const { buttonProps } = useButton({ ...ctxProps, elementType: "button" }, localRef);
4861
+ if (!isValidElement(children)) return /* @__PURE__ */ jsx(Fragment, { children });
4862
+ return cloneElement(
4863
+ children,
4864
+ { ...buttonProps, ref: mergedCallbackRef }
4865
+ );
4866
+ }
4867
+ function HeadlessMenuTrigger({
4868
+ children,
4869
+ placement = "bottom start",
4870
+ shouldFlip = true,
4871
+ ...rest
4872
+ }) {
4873
+ const childrenArray = Array.isArray(children) ? children : [children];
4874
+ const [triggerChild, menuChild] = childrenArray;
4875
+ return /* @__PURE__ */ jsxs(MenuTrigger$1, { ...rest, children: [
4876
+ /* @__PURE__ */ jsx(TriggerBridge, { children: triggerChild }),
4877
+ /* @__PURE__ */ jsx(Popover, { placement, shouldFlip, offset: 4, children: menuChild })
4878
+ ] });
4879
+ }
4880
+ function HeadlessMenu({
4881
+ className,
4882
+ children,
4883
+ "aria-label": ariaLabel,
4884
+ ...props
4885
+ }) {
4886
+ const menuRef = useRef(null);
4887
+ useLayoutEffect(() => {
4888
+ if (ariaLabel && menuRef.current) {
4889
+ menuRef.current.removeAttribute("aria-labelledby");
4890
+ }
4891
+ });
4892
+ return /* @__PURE__ */ jsx(
4893
+ Menu$1,
4894
+ {
4895
+ ...props,
4896
+ ref: menuRef,
4897
+ ...ariaLabel !== void 0 ? { "aria-label": ariaLabel } : {},
4898
+ className: className ?? "",
4899
+ children
4900
+ }
4901
+ );
4902
+ }
4903
+ HeadlessMenuTrigger.Menu = HeadlessMenu;
4904
+ var HeadlessMenuItem = forwardRef(
4905
+ function HeadlessMenuItem2({ children, className, ...props }, ref) {
4906
+ return /* @__PURE__ */ jsx(MenuItem$1, { ...props, ref, className: className ?? "", children });
4907
+ }
4908
+ );
4909
+ function HeadlessMenuSection({
4910
+ children,
4911
+ "aria-label": ariaLabel,
4912
+ className
4913
+ }) {
4914
+ return /* @__PURE__ */ jsx(
4915
+ MenuSection$1,
4916
+ {
4917
+ ...ariaLabel !== void 0 ? { "aria-label": ariaLabel } : {},
4918
+ className: className ?? "",
4919
+ children
4920
+ }
4921
+ );
4922
+ }
4923
+ function HeadlessMenuDivider({
4924
+ className,
4925
+ ...props
4926
+ }) {
4927
+ return /* @__PURE__ */ jsx(Separator, { ...props, className: className ?? "" });
4928
+ }
4929
+ var menuContainerVariants = cva(
4930
+ [
4931
+ // Elevation
4932
+ "shadow-elevation-2",
4933
+ // Width constraints per MD3 spec (112dp min / 280dp max)
4934
+ "min-w-28 max-w-70",
4935
+ // Layout
4936
+ "py-2",
4937
+ // Scroll: show scrollbar when content overflows; max height avoids clipping
4938
+ "overflow-y-auto",
4939
+ "max-h-[calc(var(--visual-viewport-height,100vh)-2rem)]",
4940
+ // Stacking
4941
+ "z-50",
4942
+ // Focus outline handled by React Aria
4943
+ "outline-none",
4944
+ // GPU compositing — promotes menu to its own compositor layer so
4945
+ // scale + opacity animations run without triggering layout reflow.
4946
+ "will-change-[transform,opacity]",
4947
+ // Pointer events blocked during animation to prevent accidental clicks
4948
+ // on menu items while the panel is still animating in or out.
4949
+ "data-[entering]:pointer-events-none data-[exiting]:pointer-events-none",
4950
+ // ── Enter animation ────────────────────────────────────────────────────
4951
+ // @keyframes menu-enter (defined in styles.css): scale(0.8)+opacity:0 →
4952
+ // scale(1)+opacity:1 in 120ms with cubic-bezier(0,0,0.2,1) (standard
4953
+ // decelerate — matches Angular Material's _mat-menu-enter keyframe).
4954
+ "data-[entering]:animate-[menu-enter_120ms_cubic-bezier(0,0,0.2,1)_both]",
4955
+ // ── Exit animation ─────────────────────────────────────────────────────
4956
+ // @keyframes menu-exit (defined in styles.css): opacity:1 → opacity:0
4957
+ // in 100ms after 25ms delay, linear — matches Angular Material's
4958
+ // _mat-menu-exit keyframe (fade-only, no reverse scale).
4959
+ "data-[exiting]:animate-[menu-exit_100ms_25ms_linear_both]",
4960
+ // ── Transform origin (placement-aware) ────────────────────────────────
4961
+ // RAC sets data-placement="bottom|top|left|right" on the Popover element.
4962
+ // Default (bottom): origin at top edge (menu expands downward).
4963
+ "origin-top",
4964
+ // top: origin at bottom edge (menu expands upward)
4965
+ "data-[placement=top]:origin-bottom",
4966
+ // left: origin at right edge
4967
+ "data-[placement=left]:origin-right",
4968
+ // right: origin at left edge
4969
+ "data-[placement=right]:origin-left",
4970
+ // ── Reduced motion ────────────────────────────────────────────────────
4971
+ // Skip both animations entirely for users who prefer reduced motion.
4972
+ "motion-reduce:data-[entering]:animate-none motion-reduce:data-[exiting]:animate-none"
4973
+ ],
4974
+ {
4975
+ variants: {
4976
+ /**
4977
+ * Color scheme — drives the container background.
4978
+ * baseline+standard uses a separate compound variant.
4979
+ */
4980
+ colorScheme: {
4981
+ standard: [],
4982
+ vibrant: []
4983
+ },
4984
+ /**
4985
+ * Visual style — drives corner radius and baseline vs vertical background.
4986
+ */
4987
+ menuStyle: {
4988
+ baseline: ["rounded-xs", "bg-surface-container"],
4989
+ vertical: ["rounded-lg", "bg-surface-container-low"]
4990
+ }
4991
+ },
4992
+ compoundVariants: [
4993
+ // Vertical + vibrant: tertiary container background
4994
+ {
4995
+ menuStyle: "vertical",
4996
+ colorScheme: "vibrant",
4997
+ class: ["bg-tertiary-container"]
4998
+ }
4999
+ ],
5000
+ defaultVariants: {
5001
+ colorScheme: "standard",
5002
+ menuStyle: "baseline"
5003
+ }
5004
+ }
5005
+ );
5006
+ var menuItemVariants = cva(
5007
+ [
5008
+ // Layout — height set by density context in MenuItem component
5009
+ "relative flex w-full items-center",
5010
+ "px-3 gap-3",
5011
+ // Typography: Body Large per MD3 baseline spec
5012
+ "text-body-large",
5013
+ // Interaction
5014
+ "cursor-pointer select-none outline-none",
5015
+ // State layer pseudo-element
5016
+ "before:absolute before:inset-0 before:rounded-[inherit]",
5017
+ "before:transition-opacity before:duration-short2 before:ease-standard",
5018
+ "before:opacity-0",
5019
+ // Hover state layer
5020
+ "hover:before:opacity-8",
5021
+ // Focus visible state layer
5022
+ "focus-visible:before:opacity-12",
5023
+ // Active pressed state layer
5024
+ "active:before:opacity-12",
5025
+ // Color transition for selection
5026
+ "transition-colors duration-short2 ease-standard"
5027
+ ],
5028
+ {
5029
+ variants: {
5030
+ /**
5031
+ * Disabled state: reduces opacity and blocks interaction.
5032
+ */
5033
+ isDisabled: {
5034
+ true: ["opacity-38 cursor-not-allowed pointer-events-none"],
5035
+ false: []
5036
+ },
5037
+ /**
5038
+ * Selected state: background and text color driven by compound variants.
5039
+ */
5040
+ isSelected: {
5041
+ true: [],
5042
+ false: []
5043
+ },
5044
+ /**
5045
+ * Color scheme: drives default text and state layer colors.
5046
+ * - standard: on-surface text, on-surface state layer
5047
+ * - vibrant (vertical only): on-tertiary-container text + state layer
5048
+ */
5049
+ colorScheme: {
5050
+ standard: ["text-on-surface", "before:bg-on-surface"],
5051
+ vibrant: ["text-on-tertiary-container", "before:bg-on-tertiary-container"]
5052
+ },
5053
+ /**
5054
+ * Visual style: drives corner radius on items (vertical uses rounded-lg
5055
+ * inherited from container, items stay flat inside).
5056
+ */
5057
+ menuStyle: {
5058
+ baseline: [],
5059
+ vertical: []
5060
+ }
5061
+ },
5062
+ compoundVariants: [
5063
+ // ── Baseline selection (both colorSchemes) ──────────────────────────
5064
+ {
5065
+ isSelected: true,
5066
+ menuStyle: "baseline",
5067
+ class: [
5068
+ "bg-surface-container-highest"
5069
+ // text-on-surface already applied by standard colorScheme variant
5070
+ ]
5071
+ },
5072
+ // ── Vertical + Standard selection ───────────────────────────────────
5073
+ {
5074
+ isSelected: true,
5075
+ menuStyle: "vertical",
5076
+ colorScheme: "standard",
5077
+ class: [
5078
+ "bg-tertiary-container",
5079
+ "text-on-tertiary-container",
5080
+ "before:bg-on-tertiary-container"
5081
+ ]
5082
+ },
5083
+ // ── Vertical + Vibrant selection ─────────────────────────────────────
5084
+ {
5085
+ isSelected: true,
5086
+ menuStyle: "vertical",
5087
+ colorScheme: "vibrant",
5088
+ class: ["bg-tertiary", "text-on-tertiary", "before:bg-on-tertiary"]
5089
+ }
5090
+ ],
5091
+ defaultVariants: {
5092
+ isDisabled: false,
5093
+ isSelected: false,
5094
+ colorScheme: "standard",
5095
+ menuStyle: "baseline"
5096
+ }
5097
+ }
5098
+ );
5099
+ var menuSectionVariants = cva(["flex flex-col w-full"]);
5100
+ var menuSectionHeaderVariants = cva([
5101
+ "px-3 pt-2 pb-1",
5102
+ "text-title-small text-on-surface-variant",
5103
+ "select-none"
5104
+ ]);
5105
+ var menuDividerVariants = cva(["border-t border-outline-variant", "my-2 mx-0"]);
5106
+ cva(["h-2 w-full"]);
5107
+ var menuItemTrailingTextVariants = cva([
5108
+ "ml-auto shrink-0 text-label-large text-on-surface-variant",
5109
+ "select-none"
5110
+ ]);
5111
+ var menuItemDescriptionVariants = cva([
5112
+ "text-body-medium text-on-surface-variant",
5113
+ "select-none"
5114
+ ]);
5115
+ var Menu = forwardRef(function Menu2({
5116
+ children,
5117
+ className,
5118
+ colorScheme = "standard",
5119
+ menuStyle = "baseline",
5120
+ density = 0,
5121
+ disableRipple = false,
5122
+ selectionMode,
5123
+ selectedKeys,
5124
+ onSelectionChange,
5125
+ ...props
5126
+ }, _ref) {
5127
+ const close = () => {
5128
+ };
5129
+ const contextValue = {
5130
+ close,
5131
+ disableRipple,
5132
+ colorScheme,
5133
+ menuStyle,
5134
+ density,
5135
+ ...selectionMode !== void 0 ? { selectionMode } : {},
5136
+ ...selectedKeys !== void 0 ? { selectedKeys } : {}
5137
+ };
5138
+ return /* @__PURE__ */ jsx(MenuContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
5139
+ HeadlessMenu,
5140
+ {
5141
+ ...props,
5142
+ ...selectionMode !== void 0 ? { selectionMode } : {},
5143
+ ...selectedKeys !== void 0 ? { selectedKeys } : {},
5144
+ ...onSelectionChange !== void 0 ? { onSelectionChange } : {},
5145
+ className: cn(menuContainerVariants({ colorScheme, menuStyle }), className),
5146
+ children
5147
+ }
5148
+ ) });
5149
+ });
5150
+ function MenuTrigger({
5151
+ children,
5152
+ placement = "bottom start",
5153
+ shouldFlip = true,
5154
+ ...rest
5155
+ }) {
5156
+ return /* @__PURE__ */ jsx(HeadlessMenuTrigger, { placement, shouldFlip, ...rest, children });
5157
+ }
5158
+ MenuTrigger.Menu = Menu;
5159
+ function CheckIcon() {
5160
+ return /* @__PURE__ */ jsx(
5161
+ "svg",
5162
+ {
5163
+ "data-testid": "check-icon",
5164
+ xmlns: "http://www.w3.org/2000/svg",
5165
+ viewBox: "0 0 24 24",
5166
+ fill: "currentColor",
5167
+ "aria-hidden": "true",
5168
+ focusable: "false",
5169
+ className: "h-full w-full",
5170
+ children: /* @__PURE__ */ jsx("path", { d: "M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" })
5171
+ }
5172
+ );
5173
+ }
5174
+ var DENSITY_HEIGHT = {
5175
+ 0: "h-12",
5176
+ [-1]: "h-11",
5177
+ [-2]: "h-10",
5178
+ [-3]: "h-9"
5179
+ };
5180
+ var MenuItem = forwardRef(function MenuItem2({
5181
+ children,
5182
+ leadingIcon,
5183
+ trailingIcon,
5184
+ trailingText,
5185
+ description,
5186
+ badge,
5187
+ className,
5188
+ disableRipple: itemDisableRipple,
5189
+ ...props
5190
+ }, ref) {
5191
+ const ctx = useMenuContext();
5192
+ const disableRipple = itemDisableRipple ?? ctx?.disableRipple ?? false;
5193
+ const colorScheme = ctx?.colorScheme ?? "standard";
5194
+ const menuStyle = ctx?.menuStyle ?? "baseline";
5195
+ const density = ctx?.density ?? 0;
5196
+ const selectionMode = ctx?.selectionMode;
5197
+ const heightClass = DENSITY_HEIGHT[density];
5198
+ const isSelectionMenu = selectionMode != null;
5199
+ const { ripples, onMouseDown } = useRipple({ disabled: disableRipple });
5200
+ const computeClassName = ({ isDisabled, isSelected }) => cn(
5201
+ menuItemVariants({
5202
+ isDisabled,
5203
+ isSelected: isSelected ?? false,
5204
+ colorScheme,
5205
+ menuStyle
5206
+ }),
5207
+ // Height: auto when description is present (multi-line), otherwise density
5208
+ description ? "min-h-12 py-2 h-auto items-start" : heightClass,
5209
+ className
5210
+ );
5211
+ return /* @__PURE__ */ jsx(
5212
+ HeadlessMenuItem,
5213
+ {
5214
+ ...props,
5215
+ ref,
5216
+ className: computeClassName,
5217
+ onMouseDown,
5218
+ children: ({ isSelected }) => /* @__PURE__ */ jsxs(Fragment, { children: [
5219
+ !disableRipple && /* @__PURE__ */ jsx("span", { className: "pointer-events-none absolute inset-0 z-0 overflow-hidden rounded-[inherit]", children: ripples }),
5220
+ (leadingIcon != null || isSelectionMenu) && /* @__PURE__ */ jsx(
5221
+ "span",
5222
+ {
5223
+ className: "text-on-surface-variant relative z-10 flex h-6 w-6 shrink-0 items-center justify-center",
5224
+ "aria-hidden": "true",
5225
+ children: isSelectionMenu && leadingIcon == null ? isSelected ? /* @__PURE__ */ jsx(CheckIcon, {}) : null : leadingIcon
5226
+ }
5227
+ ),
5228
+ description != null ? /* @__PURE__ */ jsxs("span", { className: "relative z-10 flex min-w-0 flex-1 flex-col", children: [
5229
+ /* @__PURE__ */ jsx("span", { className: "text-body-large", children }),
5230
+ /* @__PURE__ */ jsx("span", { className: menuItemDescriptionVariants(), children: description })
5231
+ ] }) : /* @__PURE__ */ jsx("span", { className: "text-body-large relative z-10 min-w-0 flex-1", children }),
5232
+ badge != null && /* @__PURE__ */ jsx("span", { className: "relative z-10 shrink-0", children: badge }),
5233
+ trailingIcon != null && trailingText == null && /* @__PURE__ */ jsx(
5234
+ "span",
5235
+ {
5236
+ className: "text-on-surface-variant relative z-10 ml-auto flex h-6 w-6 shrink-0 items-center justify-center",
5237
+ "aria-hidden": "true",
5238
+ children: trailingIcon
5239
+ }
5240
+ ),
5241
+ trailingText != null && trailingIcon == null && /* @__PURE__ */ jsx(
5242
+ "span",
5243
+ {
5244
+ className: cn(menuItemTrailingTextVariants(), "relative z-10"),
5245
+ "aria-keyshortcuts": trailingText,
5246
+ children: trailingText
5247
+ }
5248
+ )
5249
+ ] })
5250
+ }
5251
+ );
5252
+ });
5253
+ function MenuSection({
5254
+ children,
5255
+ header,
5256
+ showDivider = false,
5257
+ className,
5258
+ "aria-label": ariaLabel
5259
+ }) {
5260
+ const sectionAriaLabel = ariaLabel ?? header;
5261
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
5262
+ showDivider && /* @__PURE__ */ jsx(HeadlessMenuDivider, { className: menuDividerVariants() }),
5263
+ /* @__PURE__ */ jsxs(
5264
+ HeadlessMenuSection,
5265
+ {
5266
+ "aria-label": sectionAriaLabel,
5267
+ className: cn(menuSectionVariants(), className),
5268
+ children: [
5269
+ header && /* @__PURE__ */ jsx(Header, { className: menuSectionHeaderVariants(), "aria-hidden": "true", children: header }),
5270
+ children
5271
+ ]
5272
+ }
5273
+ )
5274
+ ] });
5275
+ }
5276
+ function MenuDivider({ className }) {
5277
+ return /* @__PURE__ */ jsx(HeadlessMenuDivider, { className: cn(menuDividerVariants(), className) });
5278
+ }
5279
+ var SnackbarHeadless = forwardRef(
5280
+ function SnackbarHeadless2({
5281
+ message,
5282
+ supportingText,
5283
+ action,
5284
+ showClose,
5285
+ duration = 4e3,
5286
+ severity = "default",
5287
+ position = "bottom-center",
5288
+ onClose,
5289
+ children,
5290
+ className,
5291
+ getAnimationClassName
5292
+ }, ref) {
5293
+ const [animationState, setAnimationState] = useState("entering");
5294
+ const remainingRef = useRef(duration);
5295
+ const startedAtRef = useRef(Date.now());
5296
+ const dismissTimerRef = useRef(null);
5297
+ const exitFallbackRef = useRef(null);
5298
+ const pausedRef = useRef(false);
5299
+ const closedRef = useRef(false);
5300
+ const clearDismissTimer = useCallback(() => {
5301
+ if (dismissTimerRef.current !== null) {
5302
+ clearTimeout(dismissTimerRef.current);
5303
+ dismissTimerRef.current = null;
5304
+ }
5305
+ }, []);
5306
+ const triggerExit = useCallback(() => {
5307
+ if (closedRef.current) return;
5308
+ clearDismissTimer();
5309
+ setAnimationState("exiting");
5310
+ exitFallbackRef.current = setTimeout(() => {
5311
+ if (!closedRef.current) {
5312
+ closedRef.current = true;
5313
+ setAnimationState("exited");
5314
+ onClose?.();
5315
+ }
5316
+ }, 250);
5317
+ }, [clearDismissTimer, onClose]);
5318
+ const startDismissTimer = useCallback(
5319
+ (ms) => {
5320
+ if (ms <= 0) return;
5321
+ clearDismissTimer();
5322
+ startedAtRef.current = Date.now();
5323
+ dismissTimerRef.current = setTimeout(triggerExit, ms);
5324
+ },
5325
+ [clearDismissTimer, triggerExit]
5326
+ );
5327
+ useEffect(() => {
5328
+ let frameId;
5329
+ frameId = requestAnimationFrame(() => {
5330
+ frameId = requestAnimationFrame(() => {
5331
+ setAnimationState("visible");
5332
+ });
5333
+ });
5334
+ return () => cancelAnimationFrame(frameId);
5335
+ }, []);
5336
+ useEffect(() => {
5337
+ if (animationState !== "visible") return;
5338
+ if (duration <= 0) return;
5339
+ remainingRef.current = duration;
5340
+ startDismissTimer(duration);
5341
+ return clearDismissTimer;
5342
+ }, [animationState, duration, startDismissTimer, clearDismissTimer]);
5343
+ useEffect(
5344
+ () => () => {
5345
+ clearDismissTimer();
5346
+ if (exitFallbackRef.current !== null) {
5347
+ clearTimeout(exitFallbackRef.current);
5348
+ exitFallbackRef.current = null;
5349
+ }
5350
+ },
5351
+ [clearDismissTimer]
5352
+ );
5353
+ const handleTransitionEnd = useCallback(() => {
5354
+ if (animationState === "exiting" && !closedRef.current) {
5355
+ if (exitFallbackRef.current !== null) {
5356
+ clearTimeout(exitFallbackRef.current);
5357
+ exitFallbackRef.current = null;
5358
+ }
5359
+ closedRef.current = true;
5360
+ setAnimationState("exited");
5361
+ onClose?.();
5362
+ }
5363
+ }, [animationState, onClose]);
5364
+ const handleMouseEnter = useCallback(() => {
5365
+ if (pausedRef.current || animationState !== "visible") return;
5366
+ pausedRef.current = true;
5367
+ const elapsed = Date.now() - startedAtRef.current;
5368
+ remainingRef.current = Math.max(remainingRef.current - elapsed, 0);
5369
+ clearDismissTimer();
5370
+ }, [animationState, clearDismissTimer]);
5371
+ const handleMouseLeave = useCallback(() => {
5372
+ if (!pausedRef.current || animationState !== "visible") return;
5373
+ pausedRef.current = false;
5374
+ if (duration > 0) startDismissTimer(remainingRef.current);
5375
+ }, [animationState, duration, startDismissTimer]);
5376
+ const handleFocusIn = useCallback(() => {
5377
+ if (pausedRef.current || animationState !== "visible") return;
5378
+ pausedRef.current = true;
5379
+ const elapsed = Date.now() - startedAtRef.current;
5380
+ remainingRef.current = Math.max(remainingRef.current - elapsed, 0);
5381
+ clearDismissTimer();
5382
+ }, [animationState, clearDismissTimer]);
5383
+ const handleFocusOut = useCallback(() => {
5384
+ if (!pausedRef.current || animationState !== "visible") return;
5385
+ pausedRef.current = false;
5386
+ if (duration > 0) startDismissTimer(remainingRef.current);
5387
+ }, [animationState, duration, startDismissTimer]);
5388
+ const handleClose = useCallback(() => {
5389
+ triggerExit();
5390
+ }, [triggerExit]);
5391
+ const ariaProps = severity === "error" ? { role: "alert", "aria-live": "assertive", "aria-atomic": "true" } : { role: "status", "aria-live": "polite", "aria-atomic": "true" };
5392
+ const computedClassName = cn(className, getAnimationClassName?.(animationState, position));
5393
+ const childContent = typeof children === "function" ? children({ animationState, onClose: handleClose }) : children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
5394
+ /* @__PURE__ */ jsx("span", { children: message }),
5395
+ supportingText && /* @__PURE__ */ jsx("span", { children: supportingText }),
5396
+ action && /* @__PURE__ */ jsx("button", { type: "button", onClick: action.onAction, children: action.label }),
5397
+ showClose && /* @__PURE__ */ jsx("button", { type: "button", "aria-label": "Close", onClick: handleClose, children: "\u2715" })
5398
+ ] });
5399
+ return (
5400
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions
5401
+ /* @__PURE__ */ jsx(
5402
+ "div",
5403
+ {
5404
+ ref,
5405
+ className: computedClassName,
5406
+ ...ariaProps,
5407
+ onMouseEnter: handleMouseEnter,
5408
+ onMouseLeave: handleMouseLeave,
5409
+ onFocus: handleFocusIn,
5410
+ onBlur: handleFocusOut,
5411
+ onTransitionEnd: handleTransitionEnd,
5412
+ "data-animation-state": animationState,
5413
+ children: childContent
5414
+ }
5415
+ )
5416
+ );
5417
+ }
5418
+ );
5419
+ SnackbarHeadless.displayName = "SnackbarHeadless";
5420
+ var snackbarStackContainerVariants = cva(
5421
+ ["fixed", "z-50", "flex", "gap-2", "pointer-events-none"],
5422
+ {
5423
+ variants: {
5424
+ position: {
5425
+ "bottom-center": [
5426
+ "bottom-4",
5427
+ "left-1/2",
5428
+ "-translate-x-1/2",
5429
+ "flex-col-reverse",
5430
+ "items-center"
5431
+ ],
5432
+ "bottom-left": ["bottom-4", "left-4", "flex-col-reverse", "items-start"],
5433
+ "bottom-right": ["bottom-4", "right-4", "flex-col-reverse", "items-end"],
5434
+ "top-center": ["top-4", "left-1/2", "-translate-x-1/2", "flex-col", "items-center"],
5435
+ "top-left": ["top-4", "left-4", "flex-col", "items-start"],
5436
+ "top-right": ["top-4", "right-4", "flex-col", "items-end"]
5437
+ }
5438
+ },
5439
+ defaultVariants: { position: "bottom-center" }
5440
+ }
5441
+ );
5442
+ var snackbarBaseVariants = cva(
5443
+ [
5444
+ // Sizing (MD3 spec: 288dp min, 568dp max)
5445
+ "min-w-72",
5446
+ "max-w-snackbar-max",
5447
+ "w-max",
5448
+ "min-h-12",
5449
+ // Restore pointer events so hover/focus timer pause works
5450
+ "pointer-events-auto",
5451
+ // Surface
5452
+ "bg-inverse-surface",
5453
+ // Shape: MD3 extra-small corner = 4dp
5454
+ "rounded-xs",
5455
+ // Elevation level 3
5456
+ "shadow-elevation-3",
5457
+ // Layout
5458
+ "flex",
5459
+ "items-center",
5460
+ "gap-x-1",
5461
+ "pl-4 pr-2",
5462
+ // Typography
5463
+ "text-body-medium",
5464
+ "text-inverse-on-surface",
5465
+ // Transition (properties used by both entry and exit)
5466
+ "transition-[opacity,transform]",
5467
+ "will-change-[opacity,transform]"
5468
+ ],
5469
+ {
5470
+ variants: {
5471
+ /**
5472
+ * Whether the Snackbar has supporting text (two-line layout).
5473
+ * Adjusts vertical padding to MD3 spec for two-line configuration.
5474
+ */
5475
+ twoLine: {
5476
+ true: "py-1",
5477
+ false: "py-1"
5478
+ }
5479
+ },
5480
+ defaultVariants: {
5481
+ twoLine: false
5482
+ }
5483
+ }
5484
+ );
5485
+ cva("", {
5486
+ variants: {
5487
+ position: {
5488
+ "bottom-center": ["bottom-4", "left-1/2", "-translate-x-1/2"],
5489
+ "bottom-left": ["bottom-4", "left-4"],
5490
+ "bottom-right": ["bottom-4", "right-4"],
5491
+ "top-center": ["top-4", "left-1/2", "-translate-x-1/2"],
5492
+ "top-left": ["top-4", "left-4"],
5493
+ "top-right": ["top-4", "right-4"]
5494
+ }
5495
+ },
5496
+ defaultVariants: {
5497
+ position: "bottom-center"
5498
+ }
5499
+ });
5500
+ var snackbarAnimationVariants = cva("", {
5501
+ variants: {
5502
+ animationState: {
5503
+ entering: ["opacity-0", "scale-75"],
5504
+ visible: ["scale-100", "opacity-100", "duration-medium1", "ease-emphasized-decelerate"],
5505
+ exiting: ["scale-75", "opacity-0", "duration-short4", "ease-standard-accelerate"],
5506
+ exited: ["scale-75", "opacity-0", "duration-short4", "ease-standard-accelerate"]
5507
+ },
5508
+ enterDirection: {
5509
+ up: ["origin-bottom"],
5510
+ down: ["origin-top"]
5511
+ }
5512
+ },
5513
+ defaultVariants: {
5514
+ animationState: "entering",
5515
+ enterDirection: "up"
5516
+ }
5517
+ });
5518
+ cva([...snackbarBaseVariants()], {
5519
+ variants: {
5520
+ animationState: {
5521
+ entering: ["opacity-0", "scale-75"],
5522
+ visible: ["scale-100", "opacity-100", "duration-medium1", "ease-emphasized-decelerate"],
5523
+ exiting: ["scale-75", "opacity-0", "duration-short4", "ease-standard-accelerate"],
5524
+ exited: ["scale-75", "opacity-0", "duration-short4", "ease-standard-accelerate"]
5525
+ },
5526
+ enterDirection: {
5527
+ up: ["origin-bottom"],
5528
+ down: ["origin-top"]
5529
+ },
5530
+ position: {
5531
+ "bottom-center": ["bottom-4", "left-1/2", "-translate-x-1/2"],
5532
+ "bottom-left": ["bottom-4", "left-4"],
5533
+ "bottom-right": ["bottom-4", "right-4"],
5534
+ "top-center": ["top-4", "left-1/2", "-translate-x-1/2"],
5535
+ "top-left": ["top-4", "left-4"],
5536
+ "top-right": ["top-4", "right-4"]
5537
+ },
5538
+ twoLine: {
5539
+ true: "py-1",
5540
+ false: "py-1"
5541
+ }
5542
+ },
5543
+ defaultVariants: {
5544
+ animationState: "entering",
5545
+ enterDirection: "up",
5546
+ position: "bottom-center",
5547
+ twoLine: false
5548
+ }
5549
+ });
5550
+ var snackbarMessageVariants = cva([
5551
+ "flex-1",
5552
+ "text-body-medium",
5553
+ "text-inverse-on-surface"
5554
+ ]);
5555
+ var snackbarSupportingTextVariants = cva([
5556
+ "text-body-medium",
5557
+ "text-inverse-on-surface",
5558
+ "opacity-80"
5559
+ ]);
5560
+ var snackbarActionVariants = cva(["shrink-0", "text-inverse-primary"]);
5561
+ var snackbarCloseVariants = cva(["shrink-0", "text-inverse-on-surface"]);
5562
+ var snackbarContentVariants = cva(["flex", "flex-col", "flex-1", "min-w-0 py-2 pr-2"]);
5563
+ cva(["scale-75", "opacity-0"]);
5564
+ function getEnterDirection(position) {
5565
+ return position.startsWith("top") ? "down" : "up";
5566
+ }
5567
+ function CloseIcon() {
5568
+ return /* @__PURE__ */ jsx(
5569
+ "svg",
5570
+ {
5571
+ xmlns: "http://www.w3.org/2000/svg",
5572
+ width: "24",
5573
+ height: "24",
5574
+ viewBox: "0 0 24 24",
5575
+ fill: "currentColor",
5576
+ "aria-hidden": "true",
5577
+ children: /* @__PURE__ */ jsx("path", { d: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" })
5578
+ }
5579
+ );
5580
+ }
5581
+ var Snackbar = forwardRef(function Snackbar2({
5582
+ message,
5583
+ supportingText,
5584
+ action,
5585
+ showClose = false,
5586
+ duration = 4e3,
5587
+ severity = "default",
5588
+ position = "bottom-center",
5589
+ onClose,
5590
+ className
5591
+ }, ref) {
5592
+ const isTwoLine = Boolean(supportingText);
5593
+ const baseClassName = cn(snackbarBaseVariants({ twoLine: isTwoLine }), className);
5594
+ return /* @__PURE__ */ jsx(
5595
+ SnackbarHeadless,
5596
+ {
5597
+ ref,
5598
+ message,
5599
+ ...supportingText !== void 0 && { supportingText },
5600
+ ...action !== void 0 && { action },
5601
+ showClose,
5602
+ duration,
5603
+ severity,
5604
+ position,
5605
+ ...onClose !== void 0 && { onClose },
5606
+ className: baseClassName,
5607
+ getAnimationClassName: (state, pos) => snackbarAnimationVariants({ animationState: state, enterDirection: getEnterDirection(pos) }),
5608
+ children: ({ onClose: triggerClose }) => /* @__PURE__ */ jsxs(Fragment, { children: [
5609
+ /* @__PURE__ */ jsxs("div", { className: snackbarContentVariants(), children: [
5610
+ /* @__PURE__ */ jsx("span", { className: snackbarMessageVariants(), children: message }),
5611
+ supportingText && /* @__PURE__ */ jsx("span", { className: snackbarSupportingTextVariants(), children: supportingText })
5612
+ ] }),
5613
+ action && /* @__PURE__ */ jsx("span", { className: snackbarActionVariants(), children: /* @__PURE__ */ jsx(
5614
+ Button,
5615
+ {
5616
+ variant: "text",
5617
+ onPress: action.onAction,
5618
+ className: "text-inverse-primary hover:text-inverse-primary",
5619
+ children: action.label
5620
+ }
5621
+ ) }),
5622
+ showClose && /* @__PURE__ */ jsx("span", { className: snackbarCloseVariants(), children: /* @__PURE__ */ jsx(
5623
+ IconButton,
5624
+ {
5625
+ variant: "standard",
5626
+ "aria-label": "Close",
5627
+ onPress: triggerClose,
5628
+ className: "text-inverse-on-surface hover:text-inverse-on-surface",
5629
+ children: /* @__PURE__ */ jsx(CloseIcon, {})
5630
+ }
5631
+ ) })
5632
+ ] })
5633
+ }
5634
+ );
5635
+ });
5636
+ Snackbar.displayName = "Snackbar";
5637
+ var SnackbarContext = createContext(null);
5638
+ function useSnackbar() {
5639
+ const ctx = useContext(SnackbarContext);
5640
+ if (!ctx) {
5641
+ throw new Error(
5642
+ "[Snackbar] useSnackbar must be used inside <SnackbarProvider>. Wrap your application (or Storybook decorator) with <SnackbarProvider>."
5643
+ );
5644
+ }
5645
+ return ctx;
5646
+ }
5647
+ function SnackbarProvider({ children, maxVisible = 5 }) {
5648
+ const [queue, setQueue] = useState([]);
5649
+ const counterRef = useRef(0);
5650
+ const baseId = useId();
5651
+ const showSnackbar = useCallback(
5652
+ (options) => {
5653
+ const id = `${baseId}-snackbar-${++counterRef.current}`;
5654
+ setQueue((prev) => [...prev, { ...options, id }]);
5655
+ return id;
5656
+ },
5657
+ [baseId]
5658
+ );
5659
+ const closeSnackbar = useCallback(() => {
5660
+ setQueue((prev) => {
5661
+ if (prev.length === 0) return prev;
5662
+ return prev.slice(1);
5663
+ });
5664
+ }, []);
5665
+ const removeById = useCallback((id) => {
5666
+ setQueue((prev) => prev.filter((item) => item.id !== id));
5667
+ }, []);
5668
+ const contextValue = { showSnackbar, closeSnackbar };
5669
+ const positionGroups = useMemo(() => {
5670
+ const groups = /* @__PURE__ */ new Map();
5671
+ const countByPosition = /* @__PURE__ */ new Map();
5672
+ for (const item of queue) {
5673
+ const pos = item.position ?? "bottom-center";
5674
+ const count = countByPosition.get(pos) ?? 0;
5675
+ if (count < maxVisible) {
5676
+ const existing = groups.get(pos) ?? [];
5677
+ groups.set(pos, [...existing, item]);
5678
+ countByPosition.set(pos, count + 1);
5679
+ }
5680
+ }
5681
+ return groups;
5682
+ }, [queue, maxVisible]);
5683
+ return /* @__PURE__ */ jsxs(SnackbarContext.Provider, { value: contextValue, children: [
5684
+ children,
5685
+ typeof document !== "undefined" && createPortal(
5686
+ /* @__PURE__ */ jsx(Fragment, { children: Array.from(positionGroups.entries()).map(([position, items]) => /* @__PURE__ */ jsx("div", { className: snackbarStackContainerVariants({ position }), children: items.map((item) => /* @__PURE__ */ jsx(
5687
+ Snackbar,
5688
+ {
5689
+ message: item.message,
5690
+ ...item.supportingText !== void 0 && {
5691
+ supportingText: item.supportingText
5692
+ },
5693
+ ...item.action !== void 0 && { action: item.action },
5694
+ ...item.showClose !== void 0 && { showClose: item.showClose },
5695
+ ...item.duration !== void 0 && { duration: item.duration },
5696
+ ...item.severity !== void 0 && { severity: item.severity },
5697
+ ...item.position !== void 0 && { position: item.position },
5698
+ ...item.className !== void 0 && { className: item.className },
5699
+ onClose: () => {
5700
+ item.onClose?.();
5701
+ removeById(item.id);
5702
+ }
5703
+ },
5704
+ item.id
5705
+ )) }, position)) }),
5706
+ document.body
5707
+ )
5708
+ ] });
5709
+ }
5710
+ var DialogContext = createContext(null);
5711
+ function useDialogContext() {
5712
+ const ctx = useContext(DialogContext);
5713
+ if (ctx === null) {
5714
+ throw new Error(
5715
+ "[Dialog] DialogHeadline, DialogContent, and DialogActions must be rendered inside a <Dialog> or <DialogHeadless> component."
5716
+ );
5717
+ }
5718
+ return ctx;
5719
+ }
5720
+ var DialogPanel = ({
5721
+ ariaLabel,
5722
+ headlineId,
5723
+ contentId,
5724
+ onClose,
5725
+ onTransitionEnd,
5726
+ variant,
5727
+ isDismissable,
5728
+ wrapperClassName,
5729
+ className,
5730
+ animationState,
5731
+ getAnimationClassName,
5732
+ children
5733
+ }) => {
5734
+ const panelRef = useRef(null);
5735
+ usePreventScroll();
5736
+ const { dialogProps } = useDialog(
5737
+ {
5738
+ ...ariaLabel ? { "aria-label": ariaLabel } : {},
5739
+ "aria-labelledby": headlineId,
5740
+ "aria-describedby": contentId
5741
+ },
5742
+ panelRef
5743
+ );
5744
+ const { overlayProps } = useOverlay(
5745
+ {
5746
+ isOpen: true,
5747
+ onClose,
5748
+ isDismissable,
5749
+ shouldCloseOnBlur: false
5750
+ },
5751
+ panelRef
5752
+ );
5753
+ const panelClassName = cn(className, getAnimationClassName?.(animationState));
5754
+ return (
5755
+ // Centering/positioning wrapper — structural only, no ARIA role
5756
+ /* @__PURE__ */ jsx("div", { className: wrapperClassName, children: /* @__PURE__ */ jsx(
5757
+ "div",
5758
+ {
5759
+ ...mergeProps(overlayProps, dialogProps),
5760
+ ref: panelRef,
5761
+ "aria-modal": "true",
5762
+ className: panelClassName,
5763
+ "data-animation-state": animationState,
5764
+ "data-variant": variant,
5765
+ onTransitionEnd,
5766
+ children
5767
+ }
5768
+ ) })
5769
+ );
5770
+ };
5771
+ DialogPanel.displayName = "DialogPanel";
5772
+ var DialogHeadless = forwardRef(
5773
+ function DialogHeadless2({
5774
+ variant = "basic",
5775
+ open,
5776
+ defaultOpen = false,
5777
+ onOpenChange,
5778
+ "aria-label": ariaLabel,
5779
+ children,
5780
+ className,
5781
+ scrimClassName,
5782
+ getAnimationClassName
5783
+ }, _ref) {
5784
+ const state = useOverlayTriggerState({
5785
+ ...open !== void 0 ? { isOpen: open } : {},
5786
+ ...defaultOpen !== void 0 ? { defaultOpen } : {},
5787
+ ...onOpenChange !== void 0 ? { onOpenChange } : {}
5788
+ });
5789
+ const isOpen = state.isOpen;
5790
+ const close = useCallback(() => {
5791
+ state.close();
5792
+ }, [state]);
5793
+ const [animationState, setAnimationState] = useState("exited");
5794
+ const closedRef = useRef(false);
5795
+ const exitFallbackRef = useRef(null);
5796
+ useEffect(() => {
5797
+ if (!isOpen) return;
5798
+ closedRef.current = false;
5799
+ setAnimationState("entering");
5800
+ const id = setTimeout(() => {
5801
+ setAnimationState("visible");
5802
+ }, 0);
5803
+ return () => clearTimeout(id);
5804
+ }, [isOpen]);
5805
+ useEffect(() => {
5806
+ if (isOpen) return;
5807
+ if (animationState === "exited" || animationState === "entering") return;
5808
+ if (animationState === "visible") {
5809
+ setAnimationState("exiting");
5810
+ exitFallbackRef.current = setTimeout(() => {
5811
+ if (!closedRef.current) {
5812
+ closedRef.current = true;
5813
+ setAnimationState("exited");
5814
+ }
5815
+ }, 150);
5816
+ }
5817
+ }, [isOpen, animationState]);
5818
+ useEffect(
5819
+ () => () => {
5820
+ if (exitFallbackRef.current !== null) {
5821
+ clearTimeout(exitFallbackRef.current);
5822
+ }
5823
+ },
5824
+ []
5825
+ );
5826
+ const handleTransitionEnd = useCallback(() => {
5827
+ if (animationState === "exiting" && !closedRef.current) {
5828
+ if (exitFallbackRef.current !== null) {
5829
+ clearTimeout(exitFallbackRef.current);
5830
+ exitFallbackRef.current = null;
5831
+ }
5832
+ closedRef.current = true;
5833
+ setAnimationState("exited");
5834
+ }
5835
+ }, [animationState]);
5836
+ const baseId = useId();
5837
+ const headlineId = `${baseId}-dialog-headline`;
5838
+ const contentId = `${baseId}-dialog-content`;
5839
+ const contextValue = {
5840
+ headlineId,
5841
+ contentId,
5842
+ close,
5843
+ variant
5844
+ };
5845
+ const handleScrimClick = useCallback(() => {
5846
+ if (variant === "basic") {
5847
+ close();
5848
+ }
5849
+ }, [variant, close]);
5850
+ if (!isOpen && animationState === "exited") {
5851
+ return null;
5852
+ }
5853
+ const content = /* @__PURE__ */ jsxs(DialogContext.Provider, { value: contextValue, children: [
5854
+ /* @__PURE__ */ jsx(
5855
+ "div",
5856
+ {
5857
+ "data-testid": "dialog-scrim",
5858
+ className: scrimClassName,
5859
+ onClick: handleScrimClick,
5860
+ "aria-hidden": "true"
5861
+ }
5862
+ ),
5863
+ /* @__PURE__ */ jsx(FocusScope, { contain: true, restoreFocus: true, autoFocus: true, children: /* @__PURE__ */ jsx(
5864
+ DialogPanel,
5865
+ {
5866
+ ariaLabel,
5867
+ headlineId,
5868
+ contentId,
5869
+ onClose: close,
5870
+ onTransitionEnd: handleTransitionEnd,
5871
+ variant,
5872
+ isDismissable: variant === "basic",
5873
+ wrapperClassName: variant === "basic" ? "fixed inset-0 z-50 flex items-center justify-center px-4" : "fixed inset-0 z-50",
5874
+ className,
5875
+ animationState,
5876
+ getAnimationClassName,
5877
+ children
5878
+ }
5879
+ ) })
5880
+ ] });
5881
+ if (typeof document === "undefined") return null;
5882
+ return createPortal(content, document.body);
5883
+ }
5884
+ );
5885
+ DialogHeadless.displayName = "DialogHeadless";
5886
+ var dialogScrimVariants = cva([
5887
+ "fixed",
5888
+ "inset-0",
5889
+ "z-40",
5890
+ "bg-scrim",
5891
+ "opacity-32",
5892
+ "transition-opacity",
5893
+ "duration-medium2",
5894
+ "ease-standard"
5895
+ ]);
5896
+ var dialogPanelVariants = cva(
5897
+ [
5898
+ // Stacking above scrim
5899
+ "z-50",
5900
+ // Surface
5901
+ "bg-surface-container-high",
5902
+ // Flex column layout for slots
5903
+ "flex",
5904
+ "flex-col",
5905
+ // Transition for animation state changes
5906
+ "transition-[opacity,transform]",
5907
+ "will-change-[opacity,transform]"
5908
+ ],
5909
+ {
5910
+ variants: {
5911
+ variant: {
5912
+ basic: [
5913
+ // Shape: MD3 extra-large = 28dp
5914
+ "rounded-xl",
5915
+ // Elevation level 3
5916
+ "shadow-elevation-3",
5917
+ // Width constraints per MD3 spec (280dp min, 560dp max)
5918
+ "min-w-70",
5919
+ "max-w-dialog-max",
5920
+ "w-full",
5921
+ // Internal spacing
5922
+ "pt-6",
5923
+ "pb-3",
5924
+ "px-6",
5925
+ // Positioned in viewport center
5926
+ "relative"
5927
+ ],
5928
+ fullscreen: [
5929
+ // Full viewport
5930
+ "w-full",
5931
+ "h-full",
5932
+ // No rounded corners on fullscreen
5933
+ "rounded-none",
5934
+ // No elevation shadow on fullscreen
5935
+ "shadow-none",
5936
+ // Positioned to fill portal
5937
+ "relative"
5938
+ ]
5939
+ }
5940
+ },
5941
+ defaultVariants: {
5942
+ variant: "basic"
5943
+ }
5944
+ }
5945
+ );
5946
+ cva([], {
5947
+ variants: {
5948
+ variant: {
5949
+ basic: ["fixed", "inset-0", "z-50", "flex", "items-center", "justify-center", "px-4"],
5950
+ fullscreen: ["fixed", "inset-0", "z-50"]
5951
+ }
5952
+ },
5953
+ defaultVariants: {
5954
+ variant: "basic"
5955
+ }
5956
+ });
5957
+ var dialogAnimationVariants = cva("", {
5958
+ variants: {
5959
+ animationState: {
5960
+ entering: [],
5961
+ visible: [],
5962
+ exiting: [],
5963
+ exited: []
5964
+ },
5965
+ variant: {
5966
+ basic: [],
5967
+ fullscreen: []
5968
+ }
5969
+ },
5970
+ compoundVariants: [
5971
+ // Basic: entering — start scaled down + transparent
5972
+ {
5973
+ animationState: "entering",
5974
+ variant: "basic",
5975
+ className: ["scale-90", "opacity-0"]
5976
+ },
5977
+ // Basic: visible — scale to full + fade in
5978
+ {
5979
+ animationState: "visible",
5980
+ variant: "basic",
5981
+ className: ["scale-100", "opacity-100", "duration-medium4", "ease-emphasized-decelerate"]
5982
+ },
5983
+ // Basic: exiting — fade out (scale stays at 1)
5984
+ {
5985
+ animationState: "exiting",
5986
+ variant: "basic",
5987
+ className: ["scale-100", "opacity-0", "duration-short2", "ease-emphasized-accelerate"]
5988
+ },
5989
+ // Basic: exited — fully transparent
5990
+ {
5991
+ animationState: "exited",
5992
+ variant: "basic",
5993
+ className: ["scale-100", "opacity-0"]
5994
+ },
5995
+ // Fullscreen: entering — start below viewport + transparent
5996
+ {
5997
+ animationState: "entering",
5998
+ variant: "fullscreen",
5999
+ className: ["translate-y-full", "opacity-0"]
6000
+ },
6001
+ // Fullscreen: visible — slide up + fade in
6002
+ {
6003
+ animationState: "visible",
6004
+ variant: "fullscreen",
6005
+ className: ["translate-y-0", "opacity-100", "duration-medium4", "ease-emphasized-decelerate"]
6006
+ },
6007
+ // Fullscreen: exiting — slide down + fade out
6008
+ {
6009
+ animationState: "exiting",
6010
+ variant: "fullscreen",
6011
+ className: ["translate-y-full", "opacity-0", "duration-short2", "ease-emphasized-accelerate"]
6012
+ },
6013
+ // Fullscreen: exited — fully off-screen
6014
+ {
6015
+ animationState: "exited",
6016
+ variant: "fullscreen",
6017
+ className: ["translate-y-full", "opacity-0"]
6018
+ }
6019
+ ],
6020
+ defaultVariants: {
6021
+ animationState: "entering",
6022
+ variant: "basic"
6023
+ }
6024
+ });
6025
+ var dialogHeadlineVariants = cva(["text-headline-small", "text-on-surface"], {
6026
+ variants: {
6027
+ variant: {
6028
+ basic: ["mb-4"],
6029
+ fullscreen: [
6030
+ // Top app bar row in fullscreen: flex, items-center, gap
6031
+ "flex",
6032
+ "items-center",
6033
+ "gap-4",
6034
+ "px-4",
6035
+ "h-14",
6036
+ "shrink-0",
6037
+ "border-b",
6038
+ "border-outline-variant"
6039
+ ]
6040
+ }
6041
+ },
6042
+ defaultVariants: {
6043
+ variant: "basic"
6044
+ }
6045
+ });
6046
+ var dialogHeadlineTitleVariants = cva([
6047
+ "flex-1",
6048
+ "text-headline-small",
6049
+ "text-on-surface",
6050
+ "truncate"
6051
+ ]);
6052
+ var dialogContentVariants = cva(
6053
+ ["text-body-medium", "text-on-surface-variant", "overflow-y-auto", "flex-1"],
6054
+ {
6055
+ variants: {
6056
+ variant: {
6057
+ basic: ["mb-6"],
6058
+ fullscreen: ["px-6", "py-4"]
6059
+ }
6060
+ },
6061
+ defaultVariants: {
6062
+ variant: "basic"
6063
+ }
6064
+ }
6065
+ );
6066
+ var dialogActionsVariants = cva([
6067
+ "flex",
6068
+ "items-center",
6069
+ "justify-end",
6070
+ "gap-2",
6071
+ "pt-3",
6072
+ "shrink-0"
6073
+ ]);
6074
+ var Dialog = forwardRef(function Dialog2({
6075
+ variant = "basic",
6076
+ open,
6077
+ defaultOpen = false,
6078
+ onOpenChange,
6079
+ "aria-label": ariaLabel,
6080
+ children,
6081
+ className
6082
+ }, _ref) {
6083
+ const panelClassName = cn(dialogPanelVariants({ variant }), className);
6084
+ const scrimClass = dialogScrimVariants();
6085
+ return /* @__PURE__ */ jsx(
6086
+ DialogHeadless,
6087
+ {
6088
+ variant,
6089
+ ...open !== void 0 ? { open } : {},
6090
+ ...defaultOpen !== void 0 ? { defaultOpen } : {},
6091
+ ...onOpenChange !== void 0 ? { onOpenChange } : {},
6092
+ ...ariaLabel ? { "aria-label": ariaLabel } : {},
6093
+ className: panelClassName,
6094
+ scrimClassName: scrimClass,
6095
+ getAnimationClassName: (state) => dialogAnimationVariants({ animationState: state, variant }),
6096
+ children
6097
+ }
6098
+ );
6099
+ });
6100
+ Dialog.displayName = "Dialog";
6101
+ var DialogHeadline = forwardRef(
6102
+ function DialogHeadline2({ children, className, closeButton, confirmButton }, ref) {
6103
+ const { headlineId, variant } = useDialogContext();
6104
+ if (variant === "fullscreen") {
6105
+ return (
6106
+ // Top app bar row for fullscreen variant
6107
+ /* @__PURE__ */ jsxs("div", { className: cn(dialogHeadlineVariants({ variant: "fullscreen" }), className), children: [
6108
+ closeButton,
6109
+ /* @__PURE__ */ jsx("h2", { id: headlineId, className: dialogHeadlineTitleVariants(), children }),
6110
+ confirmButton
6111
+ ] })
6112
+ );
6113
+ }
6114
+ return /* @__PURE__ */ jsx(
6115
+ "h2",
6116
+ {
6117
+ ref,
6118
+ id: headlineId,
6119
+ className: cn(dialogHeadlineVariants({ variant: "basic" }), className),
6120
+ children
6121
+ }
6122
+ );
6123
+ }
6124
+ );
6125
+ DialogHeadline.displayName = "DialogHeadline";
6126
+ var DialogContent = forwardRef(function DialogContent2({ children, className }, ref) {
6127
+ const { contentId, variant } = useDialogContext();
6128
+ return /* @__PURE__ */ jsx("div", { ref, id: contentId, className: cn(dialogContentVariants({ variant }), className), children });
6129
+ });
6130
+ DialogContent.displayName = "DialogContent";
6131
+ var DialogActions = forwardRef(function DialogActions2({ children, className }, ref) {
6132
+ useDialogContext();
6133
+ return /* @__PURE__ */ jsx("div", { ref, className: cn(dialogActionsVariants(), className), children });
6134
+ });
6135
+ DialogActions.displayName = "DialogActions";
2987
6136
 
2988
- export { Button, Checkbox, FAB, FABHeadless, IconButton, IconButtonHeadless, Radio, RadioGroup, RadioGroupHeadless, RadioHeadless, STATE_LAYER_OPACITY, Switch, TYPOGRAPHY_ELEMENT_MAP, TYPOGRAPHY_USAGE, TextField, applyStateLayer, cn, generateMD3Theme, getColorValue, getFontFamily, getMD3Color, getResponsiveTypography, getTypographyClassName, getTypographyForElement, getTypographyStyle, getTypographyToken, hexToRgb, pxToRem, remToPx, rgbToHex, truncateText, withOpacity };
6137
+ export { AppBar, AppBarHeadless, Button, Checkbox, Dialog, DialogActions, DialogContent, DialogContext, DialogHeadless, DialogHeadline, Drawer, DrawerItem, DrawerSection, FAB, FABHeadless, HeadlessDrawer, HeadlessDrawerItem, HeadlessMenu, HeadlessMenuDivider, HeadlessMenuItem, HeadlessMenuSection, HeadlessMenuTrigger, HeadlessNavigationBar, HeadlessNavigationBarItem, HeadlessTab, HeadlessTabList, HeadlessTabPanel, IconButton, IconButtonHeadless, Menu, MenuContext, MenuDivider, MenuItem, MenuSection, MenuTrigger, NavigationBar, NavigationBarItem, Progress, ProgressHeadless, Radio, RadioGroup, RadioGroupHeadless, RadioHeadless, STATE_LAYER_OPACITY, Snackbar, SnackbarContext, SnackbarHeadless, SnackbarProvider, Switch, TYPOGRAPHY_ELEMENT_MAP, TYPOGRAPHY_USAGE, Tab, TabList, TabPanel, Tabs, TextField, applyStateLayer, cn, generateMD3Theme, getColorValue, getFontFamily, getMD3Color, getResponsiveTypography, getTypographyClassName, getTypographyForElement, getTypographyStyle, getTypographyToken, hexToRgb, pxToRem, remToPx, rgbToHex, truncateText, useDialogContext, useMenuContext, useSnackbar, withOpacity };
2989
6138
  //# sourceMappingURL=index.js.map
2990
6139
  //# sourceMappingURL=index.js.map