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