@tinybigui/react 0.1.1 → 0.2.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
@@ -2,12 +2,12 @@ import { clsx } from 'clsx';
2
2
  import { twMerge } 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, Children, isValidElement } 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 } from 'react-aria';
9
+ import { mergeProps, filterDOMProps } from '@react-aria/utils';
10
+ import { useToggleState, useRadioGroupState, useTabListState, Item, useOverlayTriggerState } from 'react-stately';
11
11
 
12
12
  // src/utils/cn.ts
13
13
  function cn(...inputs) {
@@ -130,6 +130,162 @@ function truncateText(lines = 1) {
130
130
  textOverflow: "ellipsis"
131
131
  };
132
132
  }
133
+ function useScrollElevation(options = {}) {
134
+ const { scrolled: controlledScrolled, onScrollStateChange, threshold = 0 } = options;
135
+ const isControlled = controlledScrolled !== void 0;
136
+ const [internalScrolled, setInternalScrolled] = useState(false);
137
+ const handleScroll = useCallback(() => {
138
+ const currentlyScrolled = window.scrollY > threshold;
139
+ setInternalScrolled((prev) => {
140
+ if (prev !== currentlyScrolled) {
141
+ onScrollStateChange?.(currentlyScrolled);
142
+ return currentlyScrolled;
143
+ }
144
+ return prev;
145
+ });
146
+ }, [threshold, onScrollStateChange]);
147
+ useEffect(() => {
148
+ if (isControlled) return;
149
+ window.addEventListener("scroll", handleScroll, { passive: true });
150
+ handleScroll();
151
+ return () => {
152
+ window.removeEventListener("scroll", handleScroll);
153
+ };
154
+ }, [isControlled, handleScroll]);
155
+ return {
156
+ isScrolled: isControlled ? controlledScrolled : internalScrolled
157
+ };
158
+ }
159
+ var AppBarHeadless = forwardRef(
160
+ ({ className, children, scrolled: scrolledProp, onScrollStateChange }, ref) => {
161
+ const { isScrolled } = useScrollElevation({
162
+ scrolled: scrolledProp,
163
+ onScrollStateChange
164
+ });
165
+ return /* @__PURE__ */ jsx("header", { ref, role: "banner", className, "data-scrolled": isScrolled, children });
166
+ }
167
+ );
168
+ AppBarHeadless.displayName = "AppBarHeadless";
169
+ var appBarVariants = cva(
170
+ [
171
+ // Base classes (always applied)
172
+ "w-full",
173
+ "bg-surface text-on-surface",
174
+ "flex flex-col",
175
+ // Elevation transition using MD3 motion tokens
176
+ "transition-shadow duration-medium2 ease-standard"
177
+ ],
178
+ {
179
+ variants: {
180
+ /**
181
+ * Size variant (MD3 specification)
182
+ * Controls bar height, title placement, and type scale
183
+ */
184
+ variant: {
185
+ /** 64dp, title left-aligned, title-large */
186
+ small: "h-appbar-small",
187
+ /** 64dp, title centered, title-large */
188
+ "center-aligned": "h-appbar-small variant-center-aligned",
189
+ /** 112dp, title bottom-left, headline-small */
190
+ medium: "h-appbar-medium",
191
+ /** 152dp, title bottom-left, display-small */
192
+ large: "h-appbar-large"
193
+ },
194
+ /**
195
+ * Scroll state — controls surface elevation
196
+ * MD3: flat at rest, elevated on scroll
197
+ */
198
+ scrolled: {
199
+ false: "shadow-elevation-0",
200
+ true: "shadow-elevation-2"
201
+ }
202
+ },
203
+ defaultVariants: {
204
+ variant: "small",
205
+ scrolled: false
206
+ }
207
+ }
208
+ );
209
+ var appBarTitleVariants = cva("text-on-surface font-normal truncate", {
210
+ variants: {
211
+ variant: {
212
+ small: "text-title-large",
213
+ "center-aligned": "text-title-large",
214
+ medium: "text-headline-small",
215
+ large: "text-display-small"
216
+ }
217
+ },
218
+ defaultVariants: {
219
+ variant: "small"
220
+ }
221
+ });
222
+ var AppBar = forwardRef(
223
+ ({
224
+ variant = "small",
225
+ title,
226
+ navigationIcon,
227
+ actions,
228
+ scrolled: scrolledProp,
229
+ onScrollStateChange,
230
+ className
231
+ }, ref) => {
232
+ const { isScrolled } = useScrollElevation({
233
+ scrolled: scrolledProp,
234
+ onScrollStateChange
235
+ });
236
+ const isExpandedVariant = variant === "medium" || variant === "large";
237
+ return /* @__PURE__ */ jsxs(
238
+ AppBarHeadless,
239
+ {
240
+ ref,
241
+ scrolled: isScrolled,
242
+ className: cn(appBarVariants({ variant, scrolled: isScrolled }), className),
243
+ children: [
244
+ /* @__PURE__ */ jsxs(
245
+ "div",
246
+ {
247
+ "data-slot": "top-row",
248
+ className: cn(
249
+ "flex items-center",
250
+ "px-1",
251
+ // Small and center-aligned: fill the full bar height
252
+ !isExpandedVariant && "flex-1",
253
+ // Expanded variants: fixed height for the top row (64dp)
254
+ isExpandedVariant && "h-16 shrink-0"
255
+ ),
256
+ children: [
257
+ navigationIcon != null && /* @__PURE__ */ jsx("div", { "data-slot": "navigation", className: "flex shrink-0 items-center", children: navigationIcon }),
258
+ !isExpandedVariant && /* @__PURE__ */ jsx(
259
+ "span",
260
+ {
261
+ "data-testid": "appbar-title",
262
+ className: cn(
263
+ "min-w-0 flex-1 px-1",
264
+ appBarTitleVariants({ variant }),
265
+ // Center-aligned: center the title text
266
+ variant === "center-aligned" && "text-center"
267
+ ),
268
+ children: title
269
+ }
270
+ ),
271
+ actions != null && /* @__PURE__ */ jsx("div", { "data-slot": "actions", className: "flex shrink-0 items-center gap-0.5", children: actions })
272
+ ]
273
+ }
274
+ ),
275
+ isExpandedVariant && /* @__PURE__ */ jsx("div", { "data-slot": "expanded-title", className: cn("flex flex-1 items-end", "px-4 pb-4"), children: /* @__PURE__ */ jsx(
276
+ "span",
277
+ {
278
+ "data-testid": "appbar-title",
279
+ className: cn("min-w-0 truncate", appBarTitleVariants({ variant })),
280
+ children: title
281
+ }
282
+ ) })
283
+ ]
284
+ }
285
+ );
286
+ }
287
+ );
288
+ AppBar.displayName = "AppBar";
133
289
  var ButtonHeadless = forwardRef(
134
290
  ({ className, children, tabIndex = 0, onMouseDown, type, ...restProps }, forwardedRef) => {
135
291
  const internalRef = useRef(null);
@@ -303,7 +459,7 @@ var buttonVariants = cva(
303
459
  {
304
460
  variant: "tonal",
305
461
  color: "primary",
306
- className: "bg-secondary-container text-on-secondary-container"
462
+ className: "bg-primary-container text-on-primary-container"
307
463
  },
308
464
  {
309
465
  variant: "tonal",
@@ -574,7 +730,7 @@ IconButtonHeadless.displayName = "IconButtonHeadless";
574
730
  var iconButtonVariants = cva(
575
731
  [
576
732
  // Base classes (always applied)
577
- "relative inline-flex items-center justify-center",
733
+ "relative inline-flex items-center justify-center cursor-pointer",
578
734
  "overflow-hidden rounded-full",
579
735
  // Circular shape
580
736
  "transition-all duration-200",
@@ -888,7 +1044,7 @@ FABHeadless.displayName = "FABHeadless";
888
1044
  var fabVariants = cva(
889
1045
  [
890
1046
  // Base classes (always applied)
891
- "relative inline-flex items-center justify-center",
1047
+ "relative inline-flex items-center justify-center cursor-pointer",
892
1048
  "overflow-hidden",
893
1049
  "transition-all duration-200",
894
1050
  "focus-visible:outline-primary focus-visible:outline-2 focus-visible:outline-offset-2",
@@ -1329,8 +1485,8 @@ var textFieldLabelVariants = cva(
1329
1485
  {
1330
1486
  variants: {
1331
1487
  variant: {
1332
- filled: "top-4",
1333
- outlined: "top-3 bg-surface px-1"
1488
+ filled: "top-2.5",
1489
+ outlined: "top-2.5 bg-surface px-1"
1334
1490
  },
1335
1491
  size: {
1336
1492
  small: "text-sm",
@@ -1338,7 +1494,7 @@ var textFieldLabelVariants = cva(
1338
1494
  large: "text-lg"
1339
1495
  },
1340
1496
  floating: {
1341
- true: "-translate-y-6 scale-75",
1497
+ true: "-translate-y-5 scale-75",
1342
1498
  false: "scale-100"
1343
1499
  },
1344
1500
  focused: {
@@ -1363,7 +1519,7 @@ var textFieldLabelVariants = cva(
1363
1519
  {
1364
1520
  variant: "outlined",
1365
1521
  floating: true,
1366
- className: "-top-2"
1522
+ className: "top-2.5"
1367
1523
  }
1368
1524
  ],
1369
1525
  defaultVariants: {
@@ -1676,7 +1832,8 @@ var TextField = forwardRef(
1676
1832
  hasLeadingIcon: !!leadingIcon,
1677
1833
  hasTrailingIcon: !!trailingIcon,
1678
1834
  multiline: true
1679
- })
1835
+ }),
1836
+ label && "placeholder:opacity-0"
1680
1837
  ),
1681
1838
  rows,
1682
1839
  spellCheck: spellCheckProp
@@ -1694,7 +1851,9 @@ var TextField = forwardRef(
1694
1851
  hasLeadingIcon: !!leadingIcon,
1695
1852
  hasTrailingIcon: !!trailingIcon,
1696
1853
  multiline: false
1697
- })
1854
+ }),
1855
+ label && "placeholder:opacity-0"
1856
+ // Hide placeholder when there's a value to prevent overlap with floating label
1698
1857
  ),
1699
1858
  spellCheck: spellCheckProp
1700
1859
  }
@@ -1928,8 +2087,7 @@ var checkboxLabelVariants = cva(
1928
2087
  // MD3: Body Medium (14px)
1929
2088
  "text-on-surface",
1930
2089
  "select-none",
1931
- "ml-4"
1932
- // 16px spacing between checkbox and label (MD3 standard)
2090
+ "ml-1.5"
1933
2091
  ],
1934
2092
  {
1935
2093
  variants: {
@@ -2191,7 +2349,7 @@ var switchHandleContainerVariants = cva(
2191
2349
  */
2192
2350
  selected: {
2193
2351
  true: [
2194
- "left-[28px]",
2352
+ "left-[24px]",
2195
2353
  // Position when ON (52px - 24px = 28px)
2196
2354
  "text-primary"
2197
2355
  // State layer color
@@ -2731,8 +2889,7 @@ var radioLabelVariants = cva(
2731
2889
  // MD3: Body Medium (14px)
2732
2890
  "text-on-surface",
2733
2891
  "select-none",
2734
- "ml-4"
2735
- // 16px spacing between radio and label (MD3 standard)
2892
+ "ml-1.5"
2736
2893
  ],
2737
2894
  {
2738
2895
  variants: {
@@ -2984,7 +3141,1364 @@ var RadioHeadless = forwardRef(
2984
3141
  }
2985
3142
  );
2986
3143
  RadioHeadless.displayName = "RadioHeadless";
3144
+ var HeadlessTabsContext = createContext(null);
3145
+ function useHeadlessTabsContext(componentName) {
3146
+ const context = useContext(HeadlessTabsContext);
3147
+ if (!context) {
3148
+ throw new Error(`${componentName} must be used within a Tabs component`);
3149
+ }
3150
+ return context;
3151
+ }
3152
+ var HeadlessTabList = forwardRef(
3153
+ ({ children, className }, forwardedRef) => {
3154
+ const { state } = useHeadlessTabsContext("HeadlessTabList");
3155
+ const internalRef = useRef(null);
3156
+ const ref = forwardedRef ?? internalRef;
3157
+ const { tabListProps } = useTabList({}, state, ref);
3158
+ return /* @__PURE__ */ jsx("div", { ...tabListProps, ref, className, children });
3159
+ }
3160
+ );
3161
+ HeadlessTabList.displayName = "HeadlessTabList";
3162
+ var HeadlessTab = forwardRef(
3163
+ ({ item, className, onMouseDown, children, ...props }, forwardedRef) => {
3164
+ const { state } = useHeadlessTabsContext("HeadlessTab");
3165
+ const internalRef = useRef(null);
3166
+ const ref = forwardedRef ?? internalRef;
3167
+ const { tabProps, isSelected, isDisabled, isPressed } = useTab(
3168
+ {
3169
+ key: item.key,
3170
+ ...item.isDisabled !== void 0 && { isDisabled: item.isDisabled }
3171
+ },
3172
+ state,
3173
+ ref
3174
+ );
3175
+ const { isFocusVisible, focusProps } = useFocusRing();
3176
+ const mergedProps = mergeProps(tabProps, focusProps, {
3177
+ className,
3178
+ onMouseDown
3179
+ });
3180
+ return /* @__PURE__ */ jsx("button", { ...mergedProps, ref, type: "button", "data-key": String(item.key), ...props, children: typeof children === "function" ? children({ isSelected, isDisabled, isFocusVisible, isPressed }) : children });
3181
+ }
3182
+ );
3183
+ HeadlessTab.displayName = "HeadlessTab";
3184
+ var HeadlessTabPanel = forwardRef(
3185
+ ({ children, className, ...props }, forwardedRef) => {
3186
+ const { state } = useHeadlessTabsContext("HeadlessTabPanel");
3187
+ const internalRef = useRef(null);
3188
+ const ref = forwardedRef ?? internalRef;
3189
+ const { tabPanelProps } = useTabPanel(props, state, ref);
3190
+ return /* @__PURE__ */ jsx("div", { ...tabPanelProps, ref, className, children });
3191
+ }
3192
+ );
3193
+ HeadlessTabPanel.displayName = "HeadlessTabPanel";
3194
+ var TabsContext = createContext(null);
3195
+ function useTabsContext() {
3196
+ const context = useContext(TabsContext);
3197
+ if (!context) {
3198
+ throw new Error("Component must be used within a Tabs component");
3199
+ }
3200
+ return context;
3201
+ }
3202
+ function getComponentName(type) {
3203
+ if (typeof type === "string") return type;
3204
+ const exotic = type;
3205
+ return exotic.displayName ?? exotic.name ?? "";
3206
+ }
3207
+ function extractTabItemsFromChildren(children) {
3208
+ const items = [];
3209
+ Children.forEach(children, (child) => {
3210
+ if (!isValidElement(child)) return;
3211
+ if (getComponentName(child.type) === "TabList") {
3212
+ const tabListProps = child.props;
3213
+ Children.forEach(tabListProps.children, (tabChild) => {
3214
+ if (!isValidElement(tabChild)) return;
3215
+ if (getComponentName(tabChild.type) === "Tab") {
3216
+ const tabProps = tabChild.props;
3217
+ items.push({
3218
+ key: tabProps.id,
3219
+ ...tabProps.label !== void 0 && { label: tabProps.label },
3220
+ ...tabProps.icon !== void 0 && { icon: tabProps.icon },
3221
+ ...tabProps.isDisabled !== void 0 && { isDisabled: tabProps.isDisabled },
3222
+ ...tabProps["aria-label"] !== void 0 && { "aria-label": tabProps["aria-label"] }
3223
+ });
3224
+ }
3225
+ });
3226
+ }
3227
+ });
3228
+ return items;
3229
+ }
3230
+ var Tabs = forwardRef(
3231
+ ({
3232
+ selectedKey,
3233
+ defaultSelectedKey,
3234
+ onSelectionChange,
3235
+ variant = "primary",
3236
+ layout = "fixed",
3237
+ children,
3238
+ className,
3239
+ "aria-label": ariaLabel,
3240
+ "aria-labelledby": ariaLabelledBy
3241
+ }, ref) => {
3242
+ const tabItems = useMemo(() => extractTabItemsFromChildren(children), [children]);
3243
+ const state = useTabListState({
3244
+ ...selectedKey !== void 0 && { selectedKey },
3245
+ ...defaultSelectedKey !== void 0 && { defaultSelectedKey },
3246
+ ...onSelectionChange !== void 0 && { onSelectionChange },
3247
+ disabledKeys: tabItems.filter((t) => t.isDisabled).map((t) => t.key),
3248
+ children: tabItems.map((item) => /* @__PURE__ */ jsx(Item, { textValue: item.label ?? item["aria-label"] ?? String(item.key), children: item.label ?? item["aria-label"] ?? "" }, item.key))
3249
+ });
3250
+ const tabsContextValue = useMemo(
3251
+ () => ({
3252
+ selectedKey: state.selectedKey,
3253
+ variant,
3254
+ layout,
3255
+ disabledKeys: tabItems.filter((t) => t.isDisabled).map((t) => t.key),
3256
+ ...ariaLabel !== void 0 && { "aria-label": ariaLabel },
3257
+ ...ariaLabelledBy !== void 0 && { "aria-labelledby": ariaLabelledBy }
3258
+ }),
3259
+ [state.selectedKey, variant, layout, tabItems, ariaLabel, ariaLabelledBy]
3260
+ );
3261
+ const headlessContextValue = useMemo(() => ({ state }), [state]);
3262
+ 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 }) }) });
3263
+ }
3264
+ );
3265
+ Tabs.displayName = "Tabs";
3266
+ var tabListVariants = cva(
3267
+ [
3268
+ // Base classes
3269
+ "relative flex",
3270
+ "bg-surface",
3271
+ // Bottom divider line (MD3 spec)
3272
+ "border-b border-outline-variant"
3273
+ ],
3274
+ {
3275
+ variants: {
3276
+ layout: {
3277
+ fixed: "w-full",
3278
+ scrollable: "overflow-x-auto scrollbar-none"
3279
+ }
3280
+ },
3281
+ defaultVariants: {
3282
+ layout: "fixed"
3283
+ }
3284
+ }
3285
+ );
3286
+ var tabVariants = cva(
3287
+ [
3288
+ // Base layout
3289
+ "relative flex flex-col items-center justify-center",
3290
+ "min-h-12 px-4",
3291
+ "cursor-pointer select-none",
3292
+ "overflow-hidden",
3293
+ // Typography: MD3 Title Small
3294
+ "text-sm font-medium tracking-[0.1px]",
3295
+ // Transition
3296
+ "transition-colors duration-200",
3297
+ // Focus visible
3298
+ "focus-visible:outline-none",
3299
+ // State layer via before pseudo-element
3300
+ "before:absolute before:inset-0 before:transition-opacity before:duration-200",
3301
+ "before:bg-current before:opacity-0",
3302
+ "hover:before:opacity-8",
3303
+ "active:before:opacity-12",
3304
+ "focus-visible:before:opacity-12"
3305
+ ],
3306
+ {
3307
+ variants: {
3308
+ /**
3309
+ * Tab variant (Primary or Secondary)
3310
+ */
3311
+ variant: {
3312
+ primary: "",
3313
+ secondary: ""
3314
+ },
3315
+ /**
3316
+ * Selected state
3317
+ */
3318
+ selected: {
3319
+ true: "",
3320
+ false: ""
3321
+ },
3322
+ /**
3323
+ * Disabled state
3324
+ */
3325
+ disabled: {
3326
+ true: "opacity-38 cursor-not-allowed pointer-events-none",
3327
+ false: ""
3328
+ },
3329
+ /**
3330
+ * Layout determines min-width behavior
3331
+ */
3332
+ layout: {
3333
+ fixed: "flex-1",
3334
+ scrollable: "min-w-[90px] shrink-0"
3335
+ }
3336
+ },
3337
+ compoundVariants: [
3338
+ // Primary + selected
3339
+ {
3340
+ variant: "primary",
3341
+ selected: true,
3342
+ disabled: false,
3343
+ className: "text-primary"
3344
+ },
3345
+ // Primary + unselected
3346
+ {
3347
+ variant: "primary",
3348
+ selected: false,
3349
+ disabled: false,
3350
+ className: "text-on-surface-variant"
3351
+ },
3352
+ // Secondary + selected
3353
+ {
3354
+ variant: "secondary",
3355
+ selected: true,
3356
+ disabled: false,
3357
+ className: "text-on-surface"
3358
+ },
3359
+ // Secondary + unselected
3360
+ {
3361
+ variant: "secondary",
3362
+ selected: false,
3363
+ disabled: false,
3364
+ className: "text-on-surface-variant"
3365
+ }
3366
+ ],
3367
+ defaultVariants: {
3368
+ variant: "primary",
3369
+ selected: false,
3370
+ disabled: false,
3371
+ layout: "fixed"
3372
+ }
3373
+ }
3374
+ );
3375
+ var tabIndicatorVariants = cva(
3376
+ [
3377
+ // Base: absolutely positioned at bottom
3378
+ "absolute bottom-0 left-0",
3379
+ "pointer-events-none",
3380
+ // Transition using MD3 motion tokens (medium2 duration, emphasized easing)
3381
+ "transition-[left,width]",
3382
+ "duration-medium2",
3383
+ "ease-emphasized"
3384
+ ],
3385
+ {
3386
+ variants: {
3387
+ variant: {
3388
+ primary: ["h-[3px]", "bg-primary", "rounded-t-sm"],
3389
+ secondary: ["h-[2px]", "bg-on-surface-variant"]
3390
+ }
3391
+ },
3392
+ defaultVariants: {
3393
+ variant: "primary"
3394
+ }
3395
+ }
3396
+ );
3397
+ var tabPanelVariants = cva(
3398
+ [
3399
+ // Base panel styles
3400
+ "outline-none",
3401
+ "focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
3402
+ ],
3403
+ {
3404
+ variants: {},
3405
+ defaultVariants: {}
3406
+ }
3407
+ );
3408
+ var tabBadgeVariants = cva(
3409
+ [
3410
+ // Base badge
3411
+ "absolute",
3412
+ "inline-flex items-center justify-center",
3413
+ "bg-error text-on-error",
3414
+ "font-medium leading-none",
3415
+ "pointer-events-none"
3416
+ ],
3417
+ {
3418
+ variants: {
3419
+ type: {
3420
+ dot: ["top-1 right-1", "w-1.5 h-1.5", "rounded-full"],
3421
+ count: ["-top-1 -right-1", "min-w-[16px] h-4", "px-1", "rounded-full", "text-[11px]"]
3422
+ }
3423
+ },
3424
+ defaultVariants: {
3425
+ type: "count"
3426
+ }
3427
+ }
3428
+ );
3429
+ var tabIconVariants = cva(
3430
+ ["relative", "inline-flex items-center justify-center", "w-6 h-6"],
3431
+ {
3432
+ variants: {
3433
+ hasLabel: {
3434
+ true: "mb-1",
3435
+ false: ""
3436
+ }
3437
+ },
3438
+ defaultVariants: {
3439
+ hasLabel: false
3440
+ }
3441
+ }
3442
+ );
3443
+ var TabList = forwardRef(
3444
+ ({ children, className }, forwardedRef) => {
3445
+ const { state } = useHeadlessTabsContext("TabList");
3446
+ const {
3447
+ variant,
3448
+ layout,
3449
+ "aria-label": ariaLabel,
3450
+ "aria-labelledby": ariaLabelledBy
3451
+ } = useTabsContext();
3452
+ const internalRef = useRef(null);
3453
+ const ref = forwardedRef ?? internalRef;
3454
+ const { tabListProps } = useTabList(
3455
+ {
3456
+ ...ariaLabel !== void 0 && { "aria-label": ariaLabel },
3457
+ ...ariaLabelledBy !== void 0 && { "aria-labelledby": ariaLabelledBy }
3458
+ },
3459
+ state,
3460
+ ref
3461
+ );
3462
+ const handleNavKeyDown = useCallback(
3463
+ (e) => {
3464
+ if (!["ArrowRight", "ArrowLeft", "Home", "End"].includes(e.key)) return;
3465
+ const container = ref.current;
3466
+ if (!container) return;
3467
+ const focusedEl = document.activeElement;
3468
+ const currentKey = focusedEl?.dataset?.key;
3469
+ if (!currentKey) return;
3470
+ const allTabEls = [...container.querySelectorAll("[data-key]")];
3471
+ const allKeys = allTabEls.map((el) => el.dataset.key).filter(Boolean);
3472
+ const enabledKeys = allKeys.filter(
3473
+ (k) => allTabEls.find((el) => el.dataset.key === k)?.getAttribute("aria-disabled") !== "true"
3474
+ );
3475
+ const currentIndex = enabledKeys.indexOf(currentKey);
3476
+ if (currentIndex === -1) return;
3477
+ let nextKey = null;
3478
+ switch (e.key) {
3479
+ case "ArrowRight":
3480
+ nextKey = enabledKeys[(currentIndex + 1) % enabledKeys.length] ?? null;
3481
+ break;
3482
+ case "ArrowLeft":
3483
+ nextKey = enabledKeys[(currentIndex - 1 + enabledKeys.length) % enabledKeys.length] ?? null;
3484
+ break;
3485
+ case "Home":
3486
+ nextKey = enabledKeys[0] ?? null;
3487
+ break;
3488
+ case "End":
3489
+ nextKey = enabledKeys[enabledKeys.length - 1] ?? null;
3490
+ break;
3491
+ }
3492
+ if (nextKey != null) {
3493
+ e.preventDefault();
3494
+ state.setSelectedKey(nextKey);
3495
+ const nextEl = container.querySelector(
3496
+ `[data-key="${CSS.escape(nextKey)}"]`
3497
+ );
3498
+ nextEl?.focus();
3499
+ }
3500
+ },
3501
+ [state, ref]
3502
+ );
3503
+ const { onKeyDown: tabListKeyDown } = tabListProps;
3504
+ const handleKeyDown = useCallback(
3505
+ (e) => {
3506
+ handleNavKeyDown(e);
3507
+ if (!e.defaultPrevented) {
3508
+ tabListKeyDown?.(e);
3509
+ }
3510
+ },
3511
+ [handleNavKeyDown, tabListKeyDown]
3512
+ );
3513
+ const [indicatorStyle, setIndicatorStyle] = useState({ left: 0, width: 0 });
3514
+ const [indicatorReady, setIndicatorReady] = useState(false);
3515
+ const updateIndicator = useCallback(() => {
3516
+ const container = ref.current;
3517
+ if (!container) return;
3518
+ const selectedTab = container.querySelector('[aria-selected="true"]');
3519
+ if (!selectedTab) return;
3520
+ const containerRect = container.getBoundingClientRect();
3521
+ const tabRect = selectedTab.getBoundingClientRect();
3522
+ const newLeft = tabRect.left - containerRect.left + container.scrollLeft;
3523
+ const newWidth = tabRect.width;
3524
+ setIndicatorStyle((prev) => {
3525
+ if (prev.left === newLeft && prev.width === newWidth) return prev;
3526
+ return { left: newLeft, width: newWidth };
3527
+ });
3528
+ setIndicatorReady(true);
3529
+ }, [ref]);
3530
+ useLayoutEffect(() => {
3531
+ updateIndicator();
3532
+ }, [state.selectedKey, updateIndicator]);
3533
+ const mergedTabListProps = { ...tabListProps, onKeyDown: handleKeyDown };
3534
+ return /* @__PURE__ */ jsxs("div", { ...mergedTabListProps, ref, className: cn(tabListVariants({ layout }), className), children: [
3535
+ children,
3536
+ /* @__PURE__ */ jsx(
3537
+ "span",
3538
+ {
3539
+ "data-tab-indicator": true,
3540
+ "aria-hidden": "true",
3541
+ className: cn(
3542
+ tabIndicatorVariants({ variant }),
3543
+ // Hide until first position is calculated to avoid flash
3544
+ !indicatorReady && "opacity-0"
3545
+ ),
3546
+ style: {
3547
+ // Dynamic left/width values from DOM measurements
3548
+ left: `${indicatorStyle.left}px`,
3549
+ width: `${indicatorStyle.width}px`
3550
+ }
3551
+ }
3552
+ )
3553
+ ] });
3554
+ }
3555
+ );
3556
+ TabList.displayName = "TabList";
3557
+ function resolveBadgeDisplay(badge) {
3558
+ if (badge === void 0 || badge === false) return null;
3559
+ if (badge === true) return "dot";
3560
+ if (typeof badge === "number") {
3561
+ if (badge === 0) return null;
3562
+ return badge > 999 ? "999+" : String(badge);
3563
+ }
3564
+ return null;
3565
+ }
3566
+ var Tab = forwardRef(
3567
+ ({ id, icon, label, badge, isDisabled = false, disableRipple = false, className, ...htmlProps }, forwardedRef) => {
3568
+ const { state } = useHeadlessTabsContext("Tab");
3569
+ const { variant, layout } = useTabsContext();
3570
+ const internalRef = useRef(null);
3571
+ const ref = forwardedRef ?? internalRef;
3572
+ const {
3573
+ tabProps,
3574
+ isSelected,
3575
+ isDisabled: ariaIsDisabled
3576
+ } = useTab({ key: id, isDisabled }, state, ref);
3577
+ const { isFocusVisible, focusProps } = useFocusRing();
3578
+ const finalIsDisabled = isDisabled || ariaIsDisabled;
3579
+ const { onMouseDown: handleRipple, ripples } = useRipple({
3580
+ disabled: finalIsDisabled || disableRipple
3581
+ });
3582
+ const handleClick = useCallback(() => {
3583
+ if (!finalIsDisabled) {
3584
+ state.setSelectedKey(id);
3585
+ }
3586
+ }, [state, id, finalIsDisabled]);
3587
+ const handleFocus = useCallback(() => {
3588
+ if (!finalIsDisabled) {
3589
+ state.selectionManager.setFocusedKey(id);
3590
+ }
3591
+ }, [state.selectionManager, id, finalIsDisabled]);
3592
+ const mergedProps = mergeProps(tabProps, focusProps, {
3593
+ onMouseDown: disableRipple ? void 0 : handleRipple,
3594
+ onClick: handleClick,
3595
+ onFocus: handleFocus
3596
+ });
3597
+ const badgeDisplay = resolveBadgeDisplay(badge);
3598
+ const isDotBadge = badgeDisplay === "dot";
3599
+ const hasIcon = Boolean(icon);
3600
+ const hasLabel = Boolean(label);
3601
+ return /* @__PURE__ */ jsxs(
3602
+ "button",
3603
+ {
3604
+ ...mergedProps,
3605
+ ref,
3606
+ type: "button",
3607
+ "data-key": String(id),
3608
+ tabIndex: finalIsDisabled ? -1 : isSelected ? 0 : -1,
3609
+ className: cn(
3610
+ tabVariants({
3611
+ variant,
3612
+ selected: isSelected,
3613
+ disabled: finalIsDisabled,
3614
+ layout
3615
+ }),
3616
+ isFocusVisible && "outline-primary outline-2 outline-offset-2",
3617
+ hasLabel && hasIcon && "min-h-16",
3618
+ className
3619
+ ),
3620
+ ...htmlProps,
3621
+ children: [
3622
+ !disableRipple && ripples,
3623
+ hasIcon && /* @__PURE__ */ jsxs("span", { className: cn(tabIconVariants({ hasLabel })), children: [
3624
+ icon,
3625
+ badgeDisplay && /* @__PURE__ */ jsx(
3626
+ "span",
3627
+ {
3628
+ "data-badge-type": isDotBadge ? "dot" : "count",
3629
+ "aria-hidden": "true",
3630
+ className: cn(tabBadgeVariants({ type: isDotBadge ? "dot" : "count" })),
3631
+ children: !isDotBadge && badgeDisplay
3632
+ }
3633
+ )
3634
+ ] }),
3635
+ hasLabel && /* @__PURE__ */ jsxs("span", { className: "relative z-10 truncate", children: [
3636
+ label,
3637
+ !hasIcon && badgeDisplay && /* @__PURE__ */ jsx(
3638
+ "span",
3639
+ {
3640
+ "data-badge-type": isDotBadge ? "dot" : "count",
3641
+ "aria-hidden": "true",
3642
+ className: cn(
3643
+ "relative ml-1",
3644
+ tabBadgeVariants({ type: isDotBadge ? "dot" : "count" })
3645
+ ),
3646
+ children: !isDotBadge && badgeDisplay
3647
+ }
3648
+ )
3649
+ ] })
3650
+ ]
3651
+ }
3652
+ );
3653
+ }
3654
+ );
3655
+ Tab.displayName = "Tab";
3656
+ var TabPanel = forwardRef(
3657
+ ({ id, children, className }, forwardedRef) => {
3658
+ const { state } = useHeadlessTabsContext("TabPanel");
3659
+ useTabsContext();
3660
+ const internalRef = useRef(null);
3661
+ const ref = forwardedRef ?? internalRef;
3662
+ const { tabPanelProps } = useTabPanel({}, state, ref);
3663
+ if (state.selectedKey !== id) {
3664
+ return null;
3665
+ }
3666
+ return /* @__PURE__ */ jsx("div", { ...tabPanelProps, ref, className: cn(tabPanelVariants(), className), children });
3667
+ }
3668
+ );
3669
+ TabPanel.displayName = "TabPanel";
3670
+ var NavigationBarContext = createContext(null);
3671
+ function useNavigationBarContext() {
3672
+ const ctx = useContext(NavigationBarContext);
3673
+ if (ctx === null) {
3674
+ throw new Error("HeadlessNavigationBarItem must be rendered inside HeadlessNavigationBar.");
3675
+ }
3676
+ return ctx;
3677
+ }
3678
+ var HeadlessNavigationBar = forwardRef(
3679
+ ({
3680
+ items,
3681
+ selectedKey,
3682
+ defaultSelectedKey,
3683
+ onSelectionChange,
3684
+ "aria-label": ariaLabel,
3685
+ className,
3686
+ renderItem
3687
+ }, ref) => {
3688
+ const collectionChildren = items.map((item) => /* @__PURE__ */ jsx(Item, { textValue: item.label ?? item["aria-label"] ?? item.key, children: item.key }, item.key));
3689
+ const disabledKeys = items.filter((item) => item.isDisabled).map((item) => item.key);
3690
+ const state = useTabListState({
3691
+ children: collectionChildren,
3692
+ ...selectedKey !== void 0 ? { selectedKey } : {},
3693
+ ...defaultSelectedKey !== void 0 ? { defaultSelectedKey } : {},
3694
+ ...onSelectionChange ? { onSelectionChange } : {},
3695
+ disabledKeys
3696
+ });
3697
+ const tabListRef = useRef(null);
3698
+ const { tabListProps } = useTabList({ "aria-label": ariaLabel }, state, tabListRef);
3699
+ return /* @__PURE__ */ jsx(
3700
+ "nav",
3701
+ {
3702
+ ref,
3703
+ role: "navigation",
3704
+ "aria-label": ariaLabel,
3705
+ className,
3706
+ 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) => {
3707
+ const itemConfig = items.find((i) => String(i.key) === String(collectionItem.key));
3708
+ if (!itemConfig) return null;
3709
+ return renderItem(itemConfig);
3710
+ }) }) })
3711
+ }
3712
+ );
3713
+ }
3714
+ );
3715
+ HeadlessNavigationBar.displayName = "HeadlessNavigationBar";
3716
+ var HeadlessNavigationBarItem = forwardRef(({ itemKey, children, className, "aria-label": ariaLabel }, ref) => {
3717
+ const { state } = useNavigationBarContext();
3718
+ const internalRef = useRef(null);
3719
+ const resolvedRef = ref ?? internalRef;
3720
+ const { tabProps, isSelected } = useTab({ key: itemKey }, state, resolvedRef);
3721
+ const { "aria-controls": _controls, ...tabPropsWithoutControls } = tabProps;
3722
+ const { focusProps, isFocusVisible } = useFocusRing();
3723
+ const content = typeof children === "function" ? children({ isSelected, isFocusVisible }) : children;
3724
+ return /* @__PURE__ */ jsx(
3725
+ "button",
3726
+ {
3727
+ type: "button",
3728
+ ...mergeProps(tabPropsWithoutControls, focusProps),
3729
+ ref: resolvedRef,
3730
+ className,
3731
+ "aria-label": ariaLabel,
3732
+ "data-focus-visible": isFocusVisible || void 0,
3733
+ "data-selected": isSelected,
3734
+ children: content
3735
+ }
3736
+ );
3737
+ });
3738
+ HeadlessNavigationBarItem.displayName = "HeadlessNavigationBarItem";
3739
+ var navigationBarVariants = cva([
3740
+ // Layout
3741
+ "fixed bottom-0 left-0 right-0 z-10",
3742
+ "w-full",
3743
+ "flex flex-row items-stretch",
3744
+ // MD3 surface
3745
+ "bg-surface-container",
3746
+ // MD3 height: 80dp
3747
+ "h-20",
3748
+ // Safe-area bottom (for mobile devices)
3749
+ "pb-safe"
3750
+ ]);
3751
+ var navigationBarItemVariants = cva(
3752
+ [
3753
+ // Layout
3754
+ "relative flex flex-1 flex-col items-center justify-center",
3755
+ "cursor-pointer select-none outline-none",
3756
+ // State layer pseudo-element (covers the full item area)
3757
+ "before:absolute before:inset-0 before:rounded-none before:transition-opacity before:duration-short2 before:ease-standard",
3758
+ // Focus-visible ring
3759
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary",
3760
+ // Transition for color changes
3761
+ "transition-colors duration-short2 ease-standard"
3762
+ ],
3763
+ {
3764
+ variants: {
3765
+ /**
3766
+ * Whether this item is the currently selected destination.
3767
+ * Controls icon and label colors per MD3 spec.
3768
+ */
3769
+ isActive: {
3770
+ true: [
3771
+ // Active icon color applied via text-color on the icon wrapper
3772
+ "[&>[data-icon]]:text-on-secondary-container",
3773
+ // Active label color
3774
+ "[&>[data-label]]:text-on-surface",
3775
+ // State layer color for active item
3776
+ "before:bg-on-surface-variant"
3777
+ ],
3778
+ false: [
3779
+ // Inactive icon color
3780
+ "[&>[data-icon]]:text-on-surface-variant",
3781
+ // Inactive label color
3782
+ "[&>[data-label]]:text-on-surface-variant",
3783
+ // State layer color for inactive item
3784
+ "before:bg-on-surface-variant"
3785
+ ]
3786
+ },
3787
+ /**
3788
+ * Whether the item is disabled.
3789
+ * Applies `opacity-38` per MD3 disabled state.
3790
+ */
3791
+ isDisabled: {
3792
+ true: ["cursor-not-allowed opacity-38 pointer-events-none"],
3793
+ false: []
3794
+ },
3795
+ /**
3796
+ * Hover and press state layer opacities.
3797
+ * Applied via compound variants on hover/active pseudo-classes.
3798
+ */
3799
+ isHovered: {
3800
+ true: ["before:opacity-8"],
3801
+ false: ["before:opacity-0"]
3802
+ },
3803
+ isPressed: {
3804
+ true: ["before:opacity-12"],
3805
+ false: []
3806
+ }
3807
+ },
3808
+ compoundVariants: [
3809
+ // When not hovered or pressed, state layer is invisible
3810
+ {
3811
+ isHovered: false,
3812
+ isPressed: false,
3813
+ className: "before:opacity-0"
3814
+ }
3815
+ ],
3816
+ defaultVariants: {
3817
+ isActive: false,
3818
+ isDisabled: false,
3819
+ isHovered: false,
3820
+ isPressed: false
3821
+ }
3822
+ }
3823
+ );
3824
+ var indicatorPillVariants = cva(
3825
+ [
3826
+ "absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2",
3827
+ "w-16 h-8",
3828
+ "rounded-full bg-secondary-container",
3829
+ "transition-[transform,opacity] duration-medium2 ease-emphasized",
3830
+ "origin-center"
3831
+ ],
3832
+ {
3833
+ variants: {
3834
+ isActive: {
3835
+ true: ["scale-x-100 opacity-100"],
3836
+ false: ["scale-x-0 opacity-0"]
3837
+ }
3838
+ },
3839
+ defaultVariants: {
3840
+ isActive: false
3841
+ }
3842
+ }
3843
+ );
3844
+ var badgeVariants = cva(
3845
+ [
3846
+ "absolute",
3847
+ "flex items-center justify-center",
3848
+ "bg-error text-on-error",
3849
+ "font-medium leading-none",
3850
+ // MD3 label-small
3851
+ "text-[0.6875rem]"
3852
+ ],
3853
+ {
3854
+ variants: {
3855
+ isDot: {
3856
+ true: [
3857
+ // Dot: 6dp diameter, top-right of icon
3858
+ "top-0 right-0.5 z-10",
3859
+ "w-1.5 h-1.5 min-w-0 rounded-full"
3860
+ ],
3861
+ false: [
3862
+ // Numeric: pill shape, top-right of icon
3863
+ "-top-1 left-3 z-10",
3864
+ "min-w-[1rem] h-4 px-1 rounded-full"
3865
+ ]
3866
+ }
3867
+ },
3868
+ defaultVariants: {
3869
+ isDot: false
3870
+ }
3871
+ }
3872
+ );
3873
+ var iconWrapperVariants = cva([
3874
+ "relative z-10 flex items-center justify-center",
3875
+ "w-6 h-6"
3876
+ ]);
3877
+ var labelVariants = cva([
3878
+ "mt-1 select-none",
3879
+ "text-label-medium",
3880
+ "transition-colors duration-short2 ease-standard",
3881
+ "truncate max-w-full"
3882
+ ]);
3883
+ var MIN_ITEMS = 3;
3884
+ var MAX_ITEMS = 5;
3885
+ function validateItemCount(count) {
3886
+ if (process.env.NODE_ENV !== "production" && (count < MIN_ITEMS || count > MAX_ITEMS)) {
3887
+ console.warn(
3888
+ `[NavigationBar] MD3 Navigation Bar requires between ${MIN_ITEMS} and ${MAX_ITEMS} destination items. Received ${count}. See: https://m3.material.io/components/navigation-bar/overview`
3889
+ );
3890
+ }
3891
+ }
3892
+ function getBadgeText(badge) {
3893
+ if (badge === void 0 || badge === 0) return null;
3894
+ if (badge === true) return null;
3895
+ if (badge > 999) return "999+";
3896
+ return String(badge);
3897
+ }
3898
+ function isBadgeVisible(badge) {
3899
+ if (badge === void 0) return false;
3900
+ if (badge === 0) return false;
3901
+ return true;
3902
+ }
3903
+ function ItemVisual({ config, isActive, hideLabels, disableRipple }) {
3904
+ const isItemDisabled = config.isDisabled === true;
3905
+ const { onMouseDown, ripples } = useRipple({
3906
+ ...disableRipple || isItemDisabled ? { disabled: true } : {}
3907
+ });
3908
+ const showBadge = isBadgeVisible(config.badge);
3909
+ const isDot = config.badge === true;
3910
+ const badgeText = getBadgeText(config.badge);
3911
+ return (
3912
+ // Overflow-hidden wrapper required for ripple containment.
3913
+ // pointer-events-none is intentional: the parent <button> handles all interaction.
3914
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions
3915
+ /* @__PURE__ */ jsxs(
3916
+ "span",
3917
+ {
3918
+ onMouseDown,
3919
+ className: "pointer-events-none relative flex h-full w-full flex-col items-center justify-center overflow-hidden",
3920
+ children: [
3921
+ ripples,
3922
+ /* @__PURE__ */ jsx(
3923
+ "span",
3924
+ {
3925
+ "data-indicator-pill": true,
3926
+ "data-active": isActive,
3927
+ "aria-hidden": "true",
3928
+ className: cn(indicatorPillVariants({ isActive }), !hideLabels && "-mt-3.5")
3929
+ }
3930
+ ),
3931
+ /* @__PURE__ */ jsxs(
3932
+ "span",
3933
+ {
3934
+ "data-icon": true,
3935
+ className: cn(
3936
+ iconWrapperVariants(),
3937
+ isActive ? "text-on-secondary-container" : "text-on-surface-variant"
3938
+ ),
3939
+ children: [
3940
+ /* @__PURE__ */ jsx("span", { className: "relative z-10 flex h-6 w-6 items-center justify-center", children: config.icon }),
3941
+ showBadge && /* @__PURE__ */ jsx(
3942
+ "span",
3943
+ {
3944
+ "data-badge": true,
3945
+ "data-badge-dot": isDot || void 0,
3946
+ "aria-label": isDot ? "notification" : badgeText ? `${badgeText} notifications` : void 0,
3947
+ "aria-live": "polite",
3948
+ className: cn(badgeVariants({ isDot })),
3949
+ children: isDot ? null : badgeText
3950
+ }
3951
+ )
3952
+ ]
3953
+ }
3954
+ ),
3955
+ !hideLabels && config.label && /* @__PURE__ */ jsx(
3956
+ "span",
3957
+ {
3958
+ "data-label": true,
3959
+ className: cn(labelVariants(), isActive ? "text-on-surface" : "text-on-surface-variant"),
3960
+ children: config.label
3961
+ }
3962
+ )
3963
+ ]
3964
+ }
3965
+ )
3966
+ );
3967
+ }
3968
+ var NavigationBar = forwardRef(
3969
+ ({
3970
+ items,
3971
+ activeKey,
3972
+ defaultActiveKey,
3973
+ onActiveChange,
3974
+ hideLabels = false,
3975
+ "aria-label": ariaLabel,
3976
+ disableRipple = false,
3977
+ className
3978
+ }, ref) => {
3979
+ validateItemCount(items.length);
3980
+ return /* @__PURE__ */ jsx(
3981
+ HeadlessNavigationBar,
3982
+ {
3983
+ ref,
3984
+ items,
3985
+ ...activeKey !== void 0 ? { selectedKey: activeKey } : {},
3986
+ ...defaultActiveKey !== void 0 ? { defaultSelectedKey: defaultActiveKey } : {},
3987
+ ...onActiveChange ? { onSelectionChange: onActiveChange } : {},
3988
+ "aria-label": ariaLabel,
3989
+ className: cn(navigationBarVariants(), className),
3990
+ renderItem: (config) => (
3991
+ // HeadlessNavigationBarItem renders the <button role="tab"> with all
3992
+ // ARIA semantics. ItemVisual renders the icon/pill/badge/label inside
3993
+ // — no nested <button> elements.
3994
+ /* @__PURE__ */ jsx(
3995
+ HeadlessNavigationBarItem,
3996
+ {
3997
+ itemKey: config.key,
3998
+ ...config["aria-label"] !== void 0 ? { "aria-label": config["aria-label"] } : {},
3999
+ className: cn(
4000
+ "relative flex flex-1 flex-col items-center justify-center",
4001
+ "cursor-pointer outline-none select-none",
4002
+ "duration-short2 ease-standard transition-colors",
4003
+ // State layer pseudo-element
4004
+ "before:absolute before:inset-0 before:rounded-none",
4005
+ "before:bg-on-surface-variant before:opacity-0",
4006
+ "before:duration-short2 before:ease-standard before:transition-opacity",
4007
+ "hover:before:opacity-8",
4008
+ "active:before:opacity-12",
4009
+ // Focus ring
4010
+ "focus-visible:outline-primary focus-visible:outline-2 focus-visible:outline-offset-2",
4011
+ // Disabled styling
4012
+ config.isDisabled && "pointer-events-none cursor-not-allowed opacity-38"
4013
+ ),
4014
+ children: ({ isSelected }) => /* @__PURE__ */ jsx(
4015
+ ItemVisual,
4016
+ {
4017
+ config,
4018
+ isActive: isSelected,
4019
+ hideLabels,
4020
+ disableRipple
4021
+ }
4022
+ )
4023
+ },
4024
+ config.key
4025
+ )
4026
+ )
4027
+ }
4028
+ );
4029
+ }
4030
+ );
4031
+ NavigationBar.displayName = "NavigationBar";
4032
+ function getBadgeContent(badge) {
4033
+ if (badge === true) return null;
4034
+ if (badge === 0) return "";
4035
+ if (badge > 999) return "999+";
4036
+ return String(badge);
4037
+ }
4038
+ function isBadgeVisible2(badge) {
4039
+ if (badge === void 0) return false;
4040
+ if (badge === 0) return false;
4041
+ return true;
4042
+ }
4043
+ var NavigationBarItem = forwardRef(
4044
+ ({
4045
+ itemKey: _itemKey,
4046
+ icon,
4047
+ label,
4048
+ badge,
4049
+ isActive = false,
4050
+ hideLabels = false,
4051
+ isDisabled = false,
4052
+ disableRipple = false,
4053
+ className,
4054
+ "aria-label": ariaLabel,
4055
+ // Spread remaining props (e.g. tabProps from useTab)
4056
+ ...rest
4057
+ }, ref) => {
4058
+ const { onMouseDown, ripples } = useRipple({
4059
+ ...disableRipple || isDisabled ? { disabled: true } : {}
4060
+ });
4061
+ const showBadge = isBadgeVisible2(badge);
4062
+ const isDot = badge === true;
4063
+ const badgeContent = badge !== void 0 ? getBadgeContent(badge) : null;
4064
+ return /* @__PURE__ */ jsx(
4065
+ "button",
4066
+ {
4067
+ type: "button",
4068
+ ref,
4069
+ onMouseDown,
4070
+ disabled: isDisabled,
4071
+ "aria-label": ariaLabel,
4072
+ className: cn(
4073
+ navigationBarItemVariants({
4074
+ isActive,
4075
+ isDisabled
4076
+ }),
4077
+ className
4078
+ ),
4079
+ ...rest,
4080
+ children: /* @__PURE__ */ jsxs("span", { className: "relative flex h-full w-full flex-col items-center justify-center overflow-hidden rounded-none", children: [
4081
+ ripples,
4082
+ /* @__PURE__ */ jsx(
4083
+ "span",
4084
+ {
4085
+ "data-indicator-pill": true,
4086
+ "data-active": isActive,
4087
+ className: cn(indicatorPillVariants({ isActive }), !hideLabels && "-mt-3.5"),
4088
+ "aria-hidden": "true"
4089
+ }
4090
+ ),
4091
+ /* @__PURE__ */ jsxs("span", { "data-icon": true, className: cn(iconWrapperVariants()), children: [
4092
+ /* @__PURE__ */ jsx("span", { className: "relative z-10 flex h-6 w-6 items-center justify-center", children: icon }),
4093
+ showBadge && /* @__PURE__ */ jsx(
4094
+ "span",
4095
+ {
4096
+ "data-badge": true,
4097
+ "data-badge-dot": isDot || void 0,
4098
+ "aria-label": isDot ? "notification" : badgeContent ? `${badgeContent} notifications` : void 0,
4099
+ "aria-live": "polite",
4100
+ className: cn(badgeVariants({ isDot })),
4101
+ children: isDot ? null : badgeContent
4102
+ }
4103
+ )
4104
+ ] }),
4105
+ !hideLabels && label && /* @__PURE__ */ jsx("span", { "data-label": true, className: cn(labelVariants()), children: label })
4106
+ ] })
4107
+ }
4108
+ );
4109
+ }
4110
+ );
4111
+ NavigationBarItem.displayName = "NavigationBarItem";
4112
+ var DrawerContext = createContext(null);
4113
+ var HeadlessDrawer = forwardRef(
4114
+ ({
4115
+ variant = "standard",
4116
+ open,
4117
+ defaultOpen = false,
4118
+ onOpenChange,
4119
+ "aria-label": ariaLabel,
4120
+ children,
4121
+ className,
4122
+ scrimClassName,
4123
+ disableRipple = false
4124
+ }, ref) => {
4125
+ const state = useOverlayTriggerState({
4126
+ ...open !== void 0 ? { isOpen: open } : {},
4127
+ ...defaultOpen !== void 0 ? { defaultOpen } : {},
4128
+ ...onOpenChange !== void 0 ? { onOpenChange } : {}
4129
+ });
4130
+ const isOpen = state.isOpen;
4131
+ const close = useCallback(() => {
4132
+ state.close();
4133
+ }, [state]);
4134
+ const contextValue = {
4135
+ isOpen,
4136
+ close,
4137
+ disableRipple
4138
+ };
4139
+ if (variant === "modal") {
4140
+ return /* @__PURE__ */ jsx(DrawerContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx("nav", { ref, role: "navigation", "aria-label": ariaLabel, children: isOpen && /* @__PURE__ */ jsxs(Fragment, { children: [
4141
+ /* @__PURE__ */ jsx(
4142
+ "div",
4143
+ {
4144
+ "data-testid": "drawer-scrim",
4145
+ className: scrimClassName,
4146
+ onClick: () => state.close(),
4147
+ "aria-hidden": "true"
4148
+ }
4149
+ ),
4150
+ /* @__PURE__ */ jsx(FocusScope, { contain: true, restoreFocus: true, autoFocus: true, children: /* @__PURE__ */ jsx(
4151
+ ModalDrawerPanel,
4152
+ {
4153
+ ariaLabel,
4154
+ onClose: () => state.close(),
4155
+ className,
4156
+ children
4157
+ }
4158
+ ) })
4159
+ ] }) }) });
4160
+ }
4161
+ return /* @__PURE__ */ jsx(DrawerContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
4162
+ "nav",
4163
+ {
4164
+ ref,
4165
+ role: "navigation",
4166
+ "aria-label": ariaLabel,
4167
+ className,
4168
+ children
4169
+ }
4170
+ ) });
4171
+ }
4172
+ );
4173
+ HeadlessDrawer.displayName = "HeadlessDrawer";
4174
+ var ModalDrawerPanel = ({
4175
+ ariaLabel,
4176
+ onClose,
4177
+ className,
4178
+ children
4179
+ }) => {
4180
+ const panelRef = useRef(null);
4181
+ usePreventScroll();
4182
+ const { dialogProps } = useDialog({ "aria-label": ariaLabel }, panelRef);
4183
+ const { overlayProps } = useOverlay(
4184
+ {
4185
+ isOpen: true,
4186
+ onClose,
4187
+ isDismissable: true,
4188
+ shouldCloseOnBlur: false
4189
+ },
4190
+ panelRef
4191
+ );
4192
+ return /* @__PURE__ */ jsx(
4193
+ "div",
4194
+ {
4195
+ ...mergeProps(overlayProps, dialogProps),
4196
+ ref: panelRef,
4197
+ className,
4198
+ "aria-modal": "true",
4199
+ children
4200
+ }
4201
+ );
4202
+ };
4203
+ ModalDrawerPanel.displayName = "ModalDrawerPanel";
4204
+ var HeadlessDrawerItem = forwardRef(
4205
+ ({
4206
+ href,
4207
+ isActive = false,
4208
+ children,
4209
+ className,
4210
+ isDisabled,
4211
+ onMouseDown,
4212
+ onPress,
4213
+ onPressStart,
4214
+ onPressEnd,
4215
+ onPressChange,
4216
+ onPressUp,
4217
+ ...restProps
4218
+ }, forwardedRef) => {
4219
+ const internalRef = useRef(null);
4220
+ const { isFocusVisible, focusProps } = useFocusRing();
4221
+ if (href) {
4222
+ const linkRef = forwardedRef ?? internalRef;
4223
+ const { linkProps } = useLink(
4224
+ {
4225
+ href,
4226
+ ...isDisabled !== void 0 ? { isDisabled } : {},
4227
+ ...onPress !== void 0 ? { onPress } : {}
4228
+ },
4229
+ linkRef
4230
+ );
4231
+ return /* @__PURE__ */ jsx(
4232
+ "a",
4233
+ {
4234
+ ...mergeProps(linkProps, focusProps, { onMouseDown }),
4235
+ ref: linkRef,
4236
+ href,
4237
+ className,
4238
+ "aria-current": isActive ? "page" : void 0,
4239
+ "data-focus-visible": isFocusVisible || void 0,
4240
+ "data-active": isActive || void 0,
4241
+ children
4242
+ }
4243
+ );
4244
+ }
4245
+ const buttonRef = forwardedRef ?? internalRef;
4246
+ const { buttonProps } = useButton(
4247
+ {
4248
+ ...restProps,
4249
+ ...isDisabled !== void 0 ? { isDisabled } : {},
4250
+ ...onPress !== void 0 ? { onPress } : {},
4251
+ ...onPressStart !== void 0 ? { onPressStart } : {},
4252
+ ...onPressEnd !== void 0 ? { onPressEnd } : {},
4253
+ ...onPressChange !== void 0 ? { onPressChange } : {},
4254
+ ...onPressUp !== void 0 ? { onPressUp } : {},
4255
+ elementType: "button"
4256
+ },
4257
+ buttonRef
4258
+ );
4259
+ return /* @__PURE__ */ jsx(
4260
+ "button",
4261
+ {
4262
+ type: "button",
4263
+ ...mergeProps(buttonProps, focusProps, { onMouseDown }),
4264
+ ref: buttonRef,
4265
+ className,
4266
+ "aria-current": isActive ? "page" : void 0,
4267
+ "data-focus-visible": isFocusVisible || void 0,
4268
+ "data-active": isActive || void 0,
4269
+ children
4270
+ }
4271
+ );
4272
+ }
4273
+ );
4274
+ HeadlessDrawerItem.displayName = "HeadlessDrawerItem";
4275
+ var drawerVariants = cva(
4276
+ [
4277
+ // Layout
4278
+ "fixed top-0 left-0 h-full w-drawer",
4279
+ "flex flex-col overflow-y-auto",
4280
+ // Stacking and shape
4281
+ "z-50",
4282
+ "rounded-r-xl",
4283
+ // Slide animation (transition applies to all open/closed state changes)
4284
+ "transition-transform duration-medium4 ease-emphasized-decelerate",
4285
+ // Focus outline removal (focus management handled by FocusScope / React Aria)
4286
+ "outline-none",
4287
+ // Padding for content spacing
4288
+ "px-3"
4289
+ ],
4290
+ {
4291
+ variants: {
4292
+ /**
4293
+ * Structural variant — drives surface color and elevation.
4294
+ * - `standard`: inline nav panel, lower-elevation surface
4295
+ * - `modal`: overlay dialog with elevation shadow
4296
+ */
4297
+ variant: {
4298
+ standard: ["bg-surface-container-low"],
4299
+ modal: ["bg-surface-container", "shadow-elevation-1"]
4300
+ },
4301
+ /**
4302
+ * Open/closed state — drives translation.
4303
+ * - `true`: drawer visible (`translate-x-0`)
4304
+ * - `false`: drawer off-screen (`-translate-x-full`)
4305
+ */
4306
+ open: {
4307
+ true: ["translate-x-0"],
4308
+ false: ["-translate-x-full"]
4309
+ }
4310
+ },
4311
+ defaultVariants: {
4312
+ variant: "standard",
4313
+ open: false
4314
+ }
4315
+ }
4316
+ );
4317
+ var drawerItemVariants = cva(
4318
+ [
4319
+ // Layout
4320
+ "relative flex w-full items-center gap-3",
4321
+ "h-14 px-4",
4322
+ "rounded-full",
4323
+ // Typography
4324
+ "text-label-large",
4325
+ // Interaction
4326
+ "cursor-pointer select-none outline-none",
4327
+ // State layer pseudo-element
4328
+ "before:absolute before:inset-0 before:rounded-full",
4329
+ "before:transition-opacity before:duration-short2 before:ease-standard",
4330
+ "before:opacity-0",
4331
+ // Hover and focus visible state layers
4332
+ "hover:before:opacity-8",
4333
+ "focus-visible:before:opacity-12",
4334
+ // Active pressed state
4335
+ "active:before:opacity-12",
4336
+ // Transition for color changes
4337
+ "transition-colors duration-short2 ease-standard"
4338
+ ],
4339
+ {
4340
+ variants: {
4341
+ /**
4342
+ * Whether this item is the currently active destination.
4343
+ * Controls background, text color, and icon color per MD3 spec.
4344
+ */
4345
+ isActive: {
4346
+ true: [
4347
+ "bg-secondary-container",
4348
+ "text-on-secondary-container",
4349
+ "before:bg-on-secondary-container"
4350
+ ],
4351
+ false: ["bg-transparent", "text-on-surface-variant", "before:bg-on-surface-variant"]
4352
+ },
4353
+ /**
4354
+ * Whether the item is disabled.
4355
+ * Applies `opacity-38` per MD3 disabled state spec.
4356
+ */
4357
+ isDisabled: {
4358
+ true: ["opacity-38 cursor-not-allowed pointer-events-none"],
4359
+ false: []
4360
+ }
4361
+ },
4362
+ defaultVariants: {
4363
+ isActive: false,
4364
+ isDisabled: false
4365
+ }
4366
+ }
4367
+ );
4368
+ var scrimVariants = cva([
4369
+ "fixed inset-0 z-40",
4370
+ "bg-scrim opacity-32",
4371
+ "transition-opacity duration-medium2 ease-standard"
4372
+ ]);
4373
+ var drawerSectionVariants = cva(["flex flex-col w-full"]);
4374
+ var drawerSectionHeaderVariants = cva([
4375
+ "px-4 pt-4 pb-2",
4376
+ "text-title-small text-on-surface-variant",
4377
+ "select-none"
4378
+ ]);
4379
+ var drawerDividerVariants = cva(["border-t border-outline-variant", "mx-4 my-2"]);
4380
+ var Drawer = forwardRef(
4381
+ ({
4382
+ variant = "standard",
4383
+ open,
4384
+ defaultOpen = false,
4385
+ onOpenChange,
4386
+ "aria-label": ariaLabel,
4387
+ children,
4388
+ className,
4389
+ disableRipple = false,
4390
+ ...restProps
4391
+ }, ref) => {
4392
+ const isOpen = open ?? defaultOpen;
4393
+ const drawerPanelClass = cn(
4394
+ drawerVariants({
4395
+ variant,
4396
+ open: isOpen
4397
+ }),
4398
+ className
4399
+ );
4400
+ const scrimClass = scrimVariants();
4401
+ return /* @__PURE__ */ jsx(
4402
+ HeadlessDrawer,
4403
+ {
4404
+ ref,
4405
+ variant,
4406
+ ...open !== void 0 ? { open } : {},
4407
+ ...defaultOpen !== void 0 ? { defaultOpen } : {},
4408
+ ...onOpenChange !== void 0 ? { onOpenChange } : {},
4409
+ "aria-label": ariaLabel,
4410
+ className: drawerPanelClass,
4411
+ scrimClassName: scrimClass,
4412
+ disableRipple,
4413
+ ...restProps,
4414
+ children
4415
+ }
4416
+ );
4417
+ }
4418
+ );
4419
+ Drawer.displayName = "Drawer";
4420
+ var DrawerItem = forwardRef(
4421
+ ({
4422
+ href,
4423
+ icon,
4424
+ label,
4425
+ badge,
4426
+ secondaryText,
4427
+ isActive = false,
4428
+ isDisabled = false,
4429
+ disableRipple = false,
4430
+ className,
4431
+ onPress,
4432
+ onPressStart,
4433
+ onPressEnd,
4434
+ onPressChange,
4435
+ onPressUp,
4436
+ ...restProps
4437
+ }, ref) => {
4438
+ const isItemDisabled = isDisabled;
4439
+ const { onMouseDown: handleRipple, ripples } = useRipple({
4440
+ disabled: isItemDisabled || disableRipple
4441
+ });
4442
+ return /* @__PURE__ */ jsxs(
4443
+ HeadlessDrawerItem,
4444
+ {
4445
+ ...restProps,
4446
+ ref,
4447
+ ...href !== void 0 ? { href } : {},
4448
+ isActive,
4449
+ ...isItemDisabled !== void 0 ? { isDisabled: isItemDisabled } : {},
4450
+ ...onPress !== void 0 ? { onPress } : {},
4451
+ ...onPressStart !== void 0 ? { onPressStart } : {},
4452
+ ...onPressEnd !== void 0 ? { onPressEnd } : {},
4453
+ ...onPressChange !== void 0 ? { onPressChange } : {},
4454
+ ...onPressUp !== void 0 ? { onPressUp } : {},
4455
+ onMouseDown: handleRipple,
4456
+ className: cn(
4457
+ drawerItemVariants({
4458
+ isActive,
4459
+ isDisabled: isItemDisabled
4460
+ }),
4461
+ className
4462
+ ),
4463
+ children: [
4464
+ ripples,
4465
+ icon && /* @__PURE__ */ jsx(
4466
+ "span",
4467
+ {
4468
+ className: "relative z-10 flex shrink-0 items-center justify-center",
4469
+ "aria-hidden": "true",
4470
+ children: icon
4471
+ }
4472
+ ),
4473
+ /* @__PURE__ */ jsxs("span", { className: "relative z-10 flex min-w-0 flex-1 flex-col text-left", children: [
4474
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: label }),
4475
+ secondaryText && /* @__PURE__ */ jsx("span", { className: "text-body-small truncate opacity-70", children: secondaryText })
4476
+ ] }),
4477
+ badge && /* @__PURE__ */ jsx(
4478
+ "span",
4479
+ {
4480
+ className: "relative z-10 ml-auto flex shrink-0 items-center pr-2",
4481
+ "aria-hidden": "true",
4482
+ children: badge
4483
+ }
4484
+ )
4485
+ ]
4486
+ }
4487
+ );
4488
+ }
4489
+ );
4490
+ DrawerItem.displayName = "DrawerItem";
4491
+ var DrawerSection = forwardRef(
4492
+ ({ header, children, showDivider = false, className }, ref) => {
4493
+ return /* @__PURE__ */ jsxs("div", { ref, className: cn(drawerSectionVariants(), className), children: [
4494
+ showDivider && /* @__PURE__ */ jsx("hr", { role: "separator", "aria-hidden": "true", className: drawerDividerVariants() }),
4495
+ header && /* @__PURE__ */ jsx("span", { className: drawerSectionHeaderVariants(), children: header }),
4496
+ children
4497
+ ] });
4498
+ }
4499
+ );
4500
+ DrawerSection.displayName = "DrawerSection";
2987
4501
 
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 };
4502
+ export { AppBar, AppBarHeadless, Button, Checkbox, Drawer, DrawerItem, DrawerSection, FAB, FABHeadless, HeadlessDrawer, HeadlessDrawerItem, HeadlessNavigationBar, HeadlessNavigationBarItem, HeadlessTab, HeadlessTabList, HeadlessTabPanel, IconButton, IconButtonHeadless, NavigationBar, NavigationBarItem, Radio, RadioGroup, RadioGroupHeadless, RadioHeadless, STATE_LAYER_OPACITY, 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, withOpacity };
2989
4503
  //# sourceMappingURL=index.js.map
2990
4504
  //# sourceMappingURL=index.js.map