@tinybigui/react 0.4.1 → 0.4.2

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,10 +2,10 @@ import { clsx } from 'clsx';
2
2
  import { extendTailwindMerge } from 'tailwind-merge';
3
3
  import { argbFromHex, themeFromSourceColor } from '@material/material-color-utilities';
4
4
  export { argbFromHex, hexFromArgb } from '@material/material-color-utilities';
5
- import React, { forwardRef, useRef, createContext, useId, useState, useCallback, useEffect, useContext, useMemo, useLayoutEffect, Children, isValidElement, cloneElement } from 'react';
5
+ import React, { forwardRef, useRef, createContext, useState, useCallback, useId, useMemo, useEffect, useContext, useLayoutEffect, Children, isValidElement, cloneElement } from 'react';
6
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
7
  import { cva } from 'class-variance-authority';
8
- import { useButton, useTextField, useFocusRing, useCheckbox, VisuallyHidden, mergeProps as mergeProps$1, useSwitch, useHover, useRadioGroup, useRadio, useTabList, useTab, useTabPanel, FocusScope, usePreventScroll, useDialog, useOverlay, useLink, useSeparator, useProgressBar, useToggleButton, useListBox, useOption, useSearchField, useSlider, useDatePicker, useLocale, useDateField, useSliderThumb, useRangeCalendar, useCalendar, usePopover, DismissButton, useDateSegment, useCalendarGrid, useCalendarCell, useTooltipTrigger, useTooltip, useOverlayPosition } from 'react-aria';
8
+ import { useButton, useHover, useFocusRing, mergeProps as mergeProps$1, useTextField, useCheckbox, VisuallyHidden, useSwitch, useRadioGroup, useRadio, useTabList, useTab, useTabPanel, FocusScope, usePreventScroll, useDialog, useOverlay, useLink, useSeparator, useProgressBar, useToggleButton, useListBox, useOption, useSearchField, useSlider, useDatePicker, useLocale, useDateField, useSliderThumb, useRangeCalendar, useCalendar, usePopover, DismissButton, useDateSegment, useCalendarGrid, useCalendarCell, useTooltipTrigger, useTooltip, useOverlayPosition } from 'react-aria';
9
9
  import { mergeProps, filterDOMProps } from '@react-aria/utils';
10
10
  import { useToggleState, useRadioGroupState, useTabListState, Item, useOverlayTriggerState, useListState, useSearchFieldState, useMenuTriggerState, useSliderState, useDatePickerState, useDateFieldState, useRangeCalendarState, useCalendarState, useTooltipTriggerState } from 'react-stately';
11
11
  import { MenuItem as MenuItem$1, Menu as Menu$1, MenuTrigger as MenuTrigger$1, Popover, MenuSection as MenuSection$1, Separator, Header, useSlottedContext, ButtonContext } from 'react-aria-components';
@@ -421,218 +421,206 @@ var ButtonHeadless = forwardRef(
421
421
  ButtonHeadless.displayName = "ButtonHeadless";
422
422
  var buttonVariants = cva(
423
423
  [
424
- // Base classes (always applied)
425
- "relative inline-flex items-center justify-center cursor-pointer",
426
- "overflow-hidden rounded-full font-medium",
427
- // Split MD3 transition: spatial (border-radius) uses expressive spring with overshoot;
428
- // effects (color/bg/shadow) use standard effects spring — no overshoot allowed on color.
424
+ // Layout + shape — NO overflow-hidden here (see note above)
425
+ "relative inline-flex items-center justify-center",
426
+ "rounded-full cursor-pointer select-none",
427
+ // Split MD3 transition: spatial (border-radius) expressive spring;
428
+ // effects (color/bg/shadow) standard effects spring.
429
429
  "btn-transition",
430
- "tracking-[0.1px]",
431
- // MD3 spec: +0.1px letter-spacing for label-large
432
- "focus-visible:outline-primary focus-visible:outline-2 focus-visible:outline-offset-2"
430
+ // Disabled — self-targeting data-[x]: selectors
431
+ "data-[disabled]:cursor-not-allowed data-[disabled]:pointer-events-none"
433
432
  ],
434
433
  {
435
434
  variants: {
436
435
  /**
437
- * Button variant (MD3 specification)
436
+ * Button variant (MD3 specification — strict, no color override)
437
+ *
438
+ * Elevation per state follows _md-comp-*-button.scss tokens:
439
+ * Filled/Tonal hover→level-1, focus/pressed→level-0
440
+ * Elevated base→level-1, hover→level-2, focus/pressed→level-1
441
+ * Outlined/Text no elevation
438
442
  */
439
443
  variant: {
440
- filled: "shadow-none hover:shadow-elevation-1",
441
- // MD3: gains elevation on hover
442
- outlined: "bg-transparent border border-outline-variant",
443
- tonal: "",
444
- elevated: "shadow-elevation-1 hover:shadow-elevation-2",
445
- // MD3: level 1 → level 2 on hover
446
- text: "bg-transparent"
447
- },
448
- /**
449
- * Color scheme (MD3 color roles)
450
- */
451
- color: {
452
- primary: "",
453
- secondary: "",
454
- tertiary: "",
455
- error: ""
444
+ /**
445
+ * Filled highest emphasis.
446
+ * MD3: container=primary, label=on-primary, state-layer=on-primary
447
+ * Elevation: 0 base → 1 hover → 0 focus → 0 pressed
448
+ */
449
+ filled: [
450
+ "bg-primary text-on-primary shadow-none",
451
+ // Hover: gains level-1 elevation
452
+ "group-data-[hovered]/button:shadow-elevation-1",
453
+ // Focus/pressed: shadow must explicitly return to level-0
454
+ // (doubled attribute selector → higher specificity than hover)
455
+ "group-data-[focus-visible]/button:shadow-none",
456
+ "group-data-[pressed]/button:group-data-[pressed]/button:shadow-none",
457
+ // Disabled overrides
458
+ "group-data-[disabled]/button:bg-on-surface/12",
459
+ "group-data-[disabled]/button:text-on-surface/38",
460
+ "group-data-[disabled]/button:shadow-none"
461
+ ],
462
+ /**
463
+ * Outlined — medium emphasis. Transparent with border.
464
+ * MD3: container=transparent, outline=outline, label=primary, state-layer=primary
465
+ * Elevation: always 0
466
+ */
467
+ outlined: [
468
+ "bg-transparent border border-outline text-primary",
469
+ // Disabled overrides
470
+ "group-data-[disabled]/button:border-on-surface/12",
471
+ "group-data-[disabled]/button:text-on-surface/38"
472
+ ],
473
+ /**
474
+ * Tonal — secondary emphasis.
475
+ * MD3 name: "Filled tonal". container=secondary-container, label=on-secondary-container
476
+ * Elevation: 0 base → 1 hover → 0 focus → 0 pressed
477
+ */
478
+ tonal: [
479
+ "bg-secondary-container text-on-secondary-container shadow-none",
480
+ // Hover: gains level-1 elevation (same as filled)
481
+ "group-data-[hovered]/button:shadow-elevation-1",
482
+ // Focus/pressed: return to level-0
483
+ "group-data-[focus-visible]/button:shadow-none",
484
+ "group-data-[pressed]/button:group-data-[pressed]/button:shadow-none",
485
+ // Disabled overrides
486
+ "group-data-[disabled]/button:bg-on-surface/12",
487
+ "group-data-[disabled]/button:text-on-surface/38",
488
+ "group-data-[disabled]/button:shadow-none"
489
+ ],
490
+ /**
491
+ * Elevated — separation via shadow.
492
+ * MD3: container=surface-container-low, label=primary
493
+ * Elevation: 1 base → 2 hover → 1 focus → 1 pressed
494
+ */
495
+ elevated: [
496
+ "bg-surface-container-low text-primary shadow-elevation-1",
497
+ // Hover: gains extra elevation
498
+ "group-data-[hovered]/button:shadow-elevation-2",
499
+ // Focus/pressed: return to base level-1
500
+ // (doubled selector wins over single hover selector at same cascade position)
501
+ "group-data-[focus-visible]/button:shadow-elevation-1",
502
+ "group-data-[pressed]/button:group-data-[pressed]/button:shadow-elevation-1",
503
+ // Disabled overrides
504
+ "group-data-[disabled]/button:bg-on-surface/12",
505
+ "group-data-[disabled]/button:text-on-surface/38",
506
+ "group-data-[disabled]/button:shadow-none"
507
+ ],
508
+ /**
509
+ * Text — lowest emphasis.
510
+ * MD3: container=transparent, label=primary, state-layer=primary
511
+ * Elevation: always 0
512
+ */
513
+ text: [
514
+ "bg-transparent text-primary",
515
+ // Disabled overrides
516
+ "group-data-[disabled]/button:text-on-surface/38"
517
+ ]
456
518
  },
457
519
  /**
458
520
  * Button size
521
+ * MD3 spec: small=32dp, medium=40dp, large=56dp
522
+ * Padding: small=16dp, medium=24dp, large=32dp
523
+ * Text variant uses reduced padding: small=12dp, medium=12dp
459
524
  */
460
525
  size: {
461
- small: "h-8 px-4 text-sm gap-2",
462
- medium: "h-10 px-6 text-sm gap-2",
463
- large: "h-12 px-8 text-base gap-3"
526
+ small: "h-8 px-4 gap-1 text-label-medium tracking-[0.1px]",
527
+ medium: "h-10 px-6 gap-2 text-label-large tracking-[0.1px]",
528
+ large: "h-14 px-8 gap-2 text-title-medium"
464
529
  },
465
530
  /**
466
- * Full width variant
531
+ * Full width button (spans container)
467
532
  */
468
533
  fullWidth: {
469
534
  true: "w-full",
470
535
  false: ""
471
- },
472
- /**
473
- * Disabled state (MD3 spec: container 12% opacity, content 38% opacity)
474
- */
475
- disabled: {
476
- true: [
477
- "pointer-events-none cursor-not-allowed",
478
- "bg-on-surface/12",
479
- // MD3: disabled container uses on-surface at 12%
480
- "text-on-surface/38",
481
- // MD3: disabled text/icons use on-surface at 38%
482
- "border-on-surface/12",
483
- // For outlined variant
484
- "shadow-none"
485
- // Remove elevation when disabled
486
- ],
487
- false: ""
488
- },
489
- /**
490
- * Loading state
491
- */
492
- loading: {
493
- true: "cursor-wait",
494
- false: ""
495
536
  }
496
537
  },
497
538
  /**
498
- * Compound variants - combinations of variant + color
539
+ * Compound variants for text variant reduced padding per size
540
+ * MD3: text buttons use 12dp padding (px-3) instead of standard padding
499
541
  */
500
542
  compoundVariants: [
501
- // ====================
502
- // FILLED VARIANTS
503
- // ====================
504
- {
505
- variant: "filled",
506
- color: "primary",
507
- className: "bg-primary text-on-primary"
508
- },
509
- {
510
- variant: "filled",
511
- color: "secondary",
512
- className: "bg-secondary text-on-secondary"
513
- },
514
- {
515
- variant: "filled",
516
- color: "tertiary",
517
- className: "bg-tertiary text-on-tertiary"
518
- },
519
- {
520
- variant: "filled",
521
- color: "error",
522
- className: "bg-error text-on-error"
523
- },
524
- // ====================
525
- // OUTLINED VARIANTS
526
- // ====================
527
- {
528
- variant: "outlined",
529
- color: "primary",
530
- className: "text-primary"
531
- },
532
- {
533
- variant: "outlined",
534
- color: "secondary",
535
- className: "text-secondary"
536
- },
537
- {
538
- variant: "outlined",
539
- color: "tertiary",
540
- className: "text-tertiary"
541
- },
542
- {
543
- variant: "outlined",
544
- color: "error",
545
- className: "text-error"
546
- },
547
- // ====================
548
- // TONAL VARIANTS
549
- // ====================
550
- {
551
- variant: "tonal",
552
- color: "primary",
553
- className: "bg-primary-container text-on-primary-container"
554
- },
555
- {
556
- variant: "tonal",
557
- color: "secondary",
558
- className: "bg-secondary-container text-on-secondary-container"
559
- },
560
- {
561
- variant: "tonal",
562
- color: "tertiary",
563
- className: "bg-tertiary-container text-on-tertiary-container"
564
- },
565
- {
566
- variant: "tonal",
567
- color: "error",
568
- className: "bg-error-container text-on-error-container"
569
- },
570
- // ====================
571
- // ELEVATED VARIANTS
572
- // ====================
573
- {
574
- variant: "elevated",
575
- color: "primary",
576
- className: "bg-surface-container-low text-primary"
577
- },
578
- {
579
- variant: "elevated",
580
- color: "secondary",
581
- className: "bg-surface-container-low text-secondary"
582
- },
583
- {
584
- variant: "elevated",
585
- color: "tertiary",
586
- className: "bg-surface-container-low text-tertiary"
587
- },
588
- {
589
- variant: "elevated",
590
- color: "error",
591
- className: "bg-surface-container-low text-error"
592
- },
593
- // ====================
594
- // TEXT VARIANTS
595
- // ====================
596
- {
597
- variant: "text",
598
- color: "primary",
599
- className: "text-primary hover:bg-primary/[0.08]"
600
- // MD3: text buttons gain primary color at 8% opacity on hover
601
- },
602
- {
603
- variant: "text",
604
- color: "secondary",
605
- className: "text-secondary hover:bg-secondary/[0.08]"
606
- // MD3: text buttons gain secondary color at 8% opacity on hover
607
- },
608
- {
609
- variant: "text",
610
- color: "tertiary",
611
- className: "text-tertiary hover:bg-tertiary/[0.08]"
612
- // MD3: text buttons gain tertiary color at 8% opacity on hover
613
- },
614
- {
615
- variant: "text",
616
- color: "error",
617
- className: "text-error hover:bg-error/[0.08]"
618
- // MD3: text buttons gain error color at 8% opacity on hover
619
- }
543
+ { variant: "text", size: "small", className: "px-3" },
544
+ { variant: "text", size: "medium", className: "px-3" },
545
+ { variant: "text", size: "large", className: "px-4" }
620
546
  ],
621
- /**
622
- * Default variants
623
- */
624
547
  defaultVariants: {
625
548
  variant: "filled",
626
- color: "primary",
627
549
  size: "medium",
628
- fullWidth: false,
629
- disabled: false,
630
- loading: false
550
+ fullWidth: false
631
551
  }
632
552
  }
633
553
  );
554
+ var buttonStateLayerVariants = cva(
555
+ [
556
+ "absolute inset-0 rounded-[inherit] overflow-hidden pointer-events-none opacity-0",
557
+ // Effects transition for opacity — standard spring, no overshoot
558
+ "transition-opacity duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
559
+ // Hover: 8%
560
+ "group-data-[hovered]/button:opacity-8",
561
+ // Focus: 10%
562
+ "group-data-[focus-visible]/button:opacity-10",
563
+ // Pressed: 10%, doubled selector wins over hover
564
+ "group-data-[pressed]/button:group-data-[pressed]/button:opacity-10",
565
+ // No state layer when disabled
566
+ "group-data-[disabled]/button:hidden"
567
+ ],
568
+ {
569
+ variants: {
570
+ variant: {
571
+ filled: "bg-on-primary",
572
+ outlined: "bg-primary",
573
+ tonal: "bg-on-secondary-container",
574
+ elevated: "bg-primary",
575
+ text: "bg-primary"
576
+ }
577
+ },
578
+ defaultVariants: { variant: "filled" }
579
+ }
580
+ );
581
+ var buttonFocusRingVariants = cva([
582
+ "pointer-events-none absolute inset-[-3px] rounded-full",
583
+ "outline outline-2 outline-offset-0 outline-secondary",
584
+ // Effects transition — opacity change must not overshoot
585
+ "transition-opacity duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
586
+ "opacity-0",
587
+ "group-data-[focus-visible]/button:opacity-100"
588
+ ]);
589
+ var buttonIconVariants = cva(
590
+ [
591
+ "relative z-10 inline-flex shrink-0 items-center justify-center",
592
+ "size-[18px]",
593
+ // Color transition uses effects token (no spatial overshoot on color)
594
+ "transition-colors duration-spring-standard-fast-effects ease-spring-standard-fast-effects"
595
+ ],
596
+ {
597
+ variants: {
598
+ hidden: {
599
+ true: "invisible",
600
+ false: ""
601
+ }
602
+ },
603
+ defaultVariants: { hidden: false }
604
+ }
605
+ );
606
+ var buttonLabelVariants = cva(["relative z-10 inline-flex items-center"]);
607
+ var QUERY = "(prefers-reduced-motion: reduce)";
608
+ function useReducedMotion() {
609
+ const [reduced, setReduced] = useState(() => {
610
+ if (typeof window === "undefined") return false;
611
+ return window.matchMedia(QUERY).matches;
612
+ });
613
+ useEffect(() => {
614
+ const mql = window.matchMedia(QUERY);
615
+ const handler = (e) => setReduced(e.matches);
616
+ mql.addEventListener("change", handler);
617
+ return () => mql.removeEventListener("change", handler);
618
+ }, []);
619
+ return reduced;
620
+ }
634
621
  function useRipple(options = {}) {
635
622
  const { disabled = false, color = "currentColor", duration = 450 } = options;
623
+ const prefersReducedMotion = useReducedMotion();
636
624
  const [ripples, setRipples] = useState([]);
637
625
  const rippleKeyCounter = useRef(0);
638
626
  const timersRef = useRef([]);
@@ -643,7 +631,7 @@ function useRipple(options = {}) {
643
631
  }, []);
644
632
  const onMouseDown = useCallback(
645
633
  (event) => {
646
- if (disabled) return;
634
+ if (disabled || prefersReducedMotion) return;
647
635
  const element = event.currentTarget;
648
636
  const rect = element.getBoundingClientRect();
649
637
  const x = event.clientX - rect.left;
@@ -659,9 +647,9 @@ function useRipple(options = {}) {
659
647
  }, duration);
660
648
  timersRef.current.push(timer);
661
649
  },
662
- [disabled, duration]
650
+ [disabled, duration, prefersReducedMotion]
663
651
  );
664
- const rippleElements = disabled ? null : /* @__PURE__ */ jsx(
652
+ const rippleElements = disabled || prefersReducedMotion ? null : /* @__PURE__ */ jsx(
665
653
  "span",
666
654
  {
667
655
  "data-ripple-container": true,
@@ -669,7 +657,7 @@ function useRipple(options = {}) {
669
657
  children: ripples.map((ripple) => /* @__PURE__ */ jsx(
670
658
  "span",
671
659
  {
672
- className: "animate-ripple absolute rounded-full opacity-12",
660
+ className: "animate-md-ripple absolute rounded-full opacity-12",
673
661
  style: {
674
662
  left: ripple.x,
675
663
  top: ripple.y,
@@ -761,7 +749,7 @@ var Spinner = () => /* @__PURE__ */ jsxs(
761
749
  {
762
750
  role: "progressbar",
763
751
  "aria-label": "Loading",
764
- className: "h-4 w-4 animate-spin",
752
+ className: "relative z-10 h-[18px] w-[18px] animate-spin",
765
753
  xmlns: "http://www.w3.org/2000/svg",
766
754
  fill: "none",
767
755
  viewBox: "0 0 24 24",
@@ -782,7 +770,6 @@ var Button = forwardRef(
782
770
  ({
783
771
  // Variant props (CVA)
784
772
  variant = "filled",
785
- color = "primary",
786
773
  size = "medium",
787
774
  fullWidth = false,
788
775
  // Content props
@@ -795,64 +782,84 @@ var Button = forwardRef(
795
782
  isDisabled = false,
796
783
  // Styling
797
784
  className,
798
- // Other props
785
+ // Other button props
799
786
  tabIndex = 0,
800
787
  type = "button",
801
- onPress,
788
+ // Passed through to ButtonHeadless → useButton
802
789
  ...props
803
790
  }, ref) => {
791
+ const buttonRef = useRef(null);
792
+ const resolvedRef = ref ?? buttonRef;
804
793
  const groupCtx = useOptionalButtonGroup();
805
794
  const isConnected = groupCtx?.variant === "connected";
806
- if (process.env.NODE_ENV === "development") {
807
- if (!children) {
808
- console.warn(
809
- "[Button] Button should have text content. Use IconButton for icon-only buttons."
810
- );
811
- }
812
- if (icon && trailingIcon) {
813
- console.warn("[Button] Button should have either icon or trailingIcon, not both.");
814
- }
815
- }
816
795
  const isButtonDisabled = isDisabled || loading;
796
+ const [isPressed, setIsPressed] = useState(false);
797
+ const handlePressStart = useCallback(() => setIsPressed(true), []);
798
+ const handlePressEnd = useCallback(() => setIsPressed(false), []);
799
+ const { isHovered, hoverProps } = useHover({ isDisabled: isButtonDisabled });
800
+ const { isFocusVisible, focusProps } = useFocusRing();
817
801
  const { onMouseDown: handleRipple, ripples } = useRipple({
818
802
  disabled: isButtonDisabled || disableRipple
819
803
  });
804
+ const buttonValue = props.value;
805
+ const isGroupSelected = isConnected && groupCtx && buttonValue ? groupCtx.selectedValues.has(buttonValue) : false;
820
806
  const connectedClasses = isConnected && groupCtx ? [
821
- ...getConnectedRadiusClasses(groupCtx, props?.value),
807
+ ...getConnectedRadiusClasses(groupCtx, buttonValue),
822
808
  groupCtx.enforceMinWidth ? "min-w-12" : ""
823
809
  ] : [];
810
+ const hasIcon = !!icon || !!trailingIcon;
811
+ if (process.env.NODE_ENV === "development") {
812
+ if (!children) {
813
+ console.warn(
814
+ "[Button] Button should have text content. Use IconButton for icon-only buttons."
815
+ );
816
+ }
817
+ }
824
818
  return /* @__PURE__ */ jsxs(
825
819
  ButtonHeadless,
826
820
  {
827
- ...props,
828
- ref,
821
+ ...mergeProps$1(
822
+ hoverProps,
823
+ focusProps,
824
+ // Track pressed state via useButton's press lifecycle callbacks,
825
+ // rather than a separate usePress hook, to avoid event handler conflicts.
826
+ { onPressStart: handlePressStart, onPressEnd: handlePressEnd },
827
+ props
828
+ ),
829
+ ref: resolvedRef,
829
830
  type,
830
831
  isDisabled: isButtonDisabled,
831
- ...onPress && { onPress },
832
832
  tabIndex,
833
833
  onMouseDown: handleRipple,
834
+ ...getInteractionDataAttributes({
835
+ isHovered,
836
+ isFocusVisible,
837
+ isPressed,
838
+ isDisabled: isButtonDisabled
839
+ }),
834
840
  "data-variant": variant,
835
- "data-color": color,
841
+ "data-with-icon": hasIcon ? "" : void 0,
842
+ "data-loading": loading ? "" : void 0,
843
+ "data-group-selected": isGroupSelected ? "" : void 0,
836
844
  className: cn(
837
- // Apply CVA variants (includes rounded-full base)
838
- buttonVariants({
839
- variant,
840
- color,
841
- size,
842
- fullWidth,
843
- disabled: isButtonDisabled,
844
- loading
845
- }),
845
+ buttonVariants({ variant, size, fullWidth }),
846
+ // group/button: enables group-data-[x]/button child selectors in all slots
847
+ // (added here, not in CVA, following the Switch pattern)
848
+ "group/button",
849
+ // Asymmetric border-radius easing: expressive when selected, decelerate when not
850
+ isGroupSelected ? "btn-transition-selected" : "",
846
851
  ...connectedClasses,
847
852
  // User custom classes
848
853
  className
849
854
  ),
850
855
  children: [
851
856
  ripples,
852
- icon && /* @__PURE__ */ jsx("span", { className: cn("relative z-10 inline-flex shrink-0", loading && "invisible"), children: icon }),
853
- loading && /* @__PURE__ */ jsx("span", { className: "relative z-10", children: /* @__PURE__ */ jsx(Spinner, {}) }),
854
- /* @__PURE__ */ jsx("span", { className: "relative z-10 inline-flex items-center", children }),
855
- trailingIcon && /* @__PURE__ */ jsx("span", { className: cn("relative z-10 inline-flex shrink-0", loading && "invisible"), children: trailingIcon })
857
+ /* @__PURE__ */ jsx("span", { className: cn(buttonStateLayerVariants({ variant })), "aria-hidden": "true" }),
858
+ /* @__PURE__ */ jsx("span", { className: cn(buttonFocusRingVariants()), "aria-hidden": "true" }),
859
+ icon && /* @__PURE__ */ jsx("span", { className: cn(buttonIconVariants({ hidden: loading })), children: icon }),
860
+ loading && /* @__PURE__ */ jsx(Spinner, {}),
861
+ /* @__PURE__ */ jsx("span", { className: cn(buttonLabelVariants()), children }),
862
+ trailingIcon && /* @__PURE__ */ jsx("span", { className: cn(buttonIconVariants({ hidden: loading })), children: trailingIcon })
856
863
  ]
857
864
  }
858
865
  );
@@ -866,6 +873,7 @@ var ButtonGroupHeadless = forwardRef(
866
873
  size = "medium",
867
874
  shape = "round",
868
875
  selectionMode,
876
+ isDisabled = false,
869
877
  // Selection — controlled
870
878
  selectedValues: controlledValues,
871
879
  onSelectionChange: onControlledChange,
@@ -887,7 +895,7 @@ var ButtonGroupHeadless = forwardRef(
887
895
  const isControlled = controlledValues !== void 0;
888
896
  const selectedValues = isControlled ? controlledValues : uncontrolledValues;
889
897
  const handleSelectionChange = (value) => {
890
- if (!selectionMode) return;
898
+ if (!selectionMode || isDisabled) return;
891
899
  let nextValues;
892
900
  if (selectionMode === "multi") {
893
901
  nextValues = new Set(selectedValues);
@@ -928,6 +936,7 @@ var ButtonGroupHeadless = forwardRef(
928
936
  selectionMode,
929
937
  selectedValues,
930
938
  onSelectionChange: handleSelectionChange,
939
+ isDisabled,
931
940
  connectedInnerRadius: getInnerRadius(size),
932
941
  connectedOuterRadius: getOuterRadius(shape, size),
933
942
  enforceMinWidth: variant === "connected" && (size === "extra-small" || size === "small")
@@ -940,9 +949,15 @@ var ButtonGroupHeadless = forwardRef(
940
949
  }
941
950
  );
942
951
  ButtonGroupHeadless.displayName = "ButtonGroupHeadless";
943
- var buttonGroupVariants = cva(
944
- // Base classes applied to every ButtonGroup container
945
- ["items-center"],
952
+ var buttonGroupRootVariants = cva(
953
+ [
954
+ // Layout
955
+ "items-center",
956
+ // Spatial motion for gap changes — standard spring (calm, utility UI)
957
+ "transition-[gap] duration-spring-standard-fast-spatial ease-spring-standard-fast-spatial",
958
+ // Disabled state — self-targeting data-[x]: selector on root
959
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-38"
960
+ ],
946
961
  {
947
962
  variants: {
948
963
  /**
@@ -990,6 +1005,14 @@ var buttonGroupVariants = cva(
990
1005
  }
991
1006
  }
992
1007
  );
1008
+ var buttonGroupFocusRingVariants = cva([
1009
+ "pointer-events-none absolute inset-[-3px] rounded-[inherit]",
1010
+ "outline outline-2 outline-offset-0 outline-secondary",
1011
+ "transition-opacity duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
1012
+ "opacity-0",
1013
+ "group-data-[focus-visible]/button-group:opacity-100"
1014
+ ]);
1015
+ var buttonGroupVariants = buttonGroupRootVariants;
993
1016
  var ButtonGroup = forwardRef(
994
1017
  ({
995
1018
  variant = "standard",
@@ -999,6 +1022,7 @@ var ButtonGroup = forwardRef(
999
1022
  selectedValues,
1000
1023
  onSelectionChange,
1001
1024
  defaultValue,
1025
+ isDisabled = false,
1002
1026
  children,
1003
1027
  className,
1004
1028
  ...htmlProps
@@ -1015,6 +1039,13 @@ var ButtonGroup = forwardRef(
1015
1039
  },
1016
1040
  [ref]
1017
1041
  );
1042
+ const hasSelection = useMemo(() => {
1043
+ if (selectedValues) return selectedValues.size > 0;
1044
+ if (defaultValue) {
1045
+ return Array.isArray(defaultValue) ? defaultValue.length > 0 : true;
1046
+ }
1047
+ return false;
1048
+ }, [selectedValues, defaultValue]);
1018
1049
  if (process.env.NODE_ENV === "development") {
1019
1050
  const childArray = Array.isArray(children) ? children : [children];
1020
1051
  for (const child of childArray) {
@@ -1063,7 +1094,12 @@ var ButtonGroup = forwardRef(
1063
1094
  selectedValues,
1064
1095
  onSelectionChange,
1065
1096
  defaultValue,
1066
- className: cn(buttonGroupVariants({ variant, size }), className),
1097
+ isDisabled,
1098
+ className: cn(buttonGroupRootVariants({ variant, size }), "group/button-group", className),
1099
+ ...getInteractionDataAttributes({ isDisabled }),
1100
+ "data-connected": variant === "connected" ? "" : void 0,
1101
+ "data-has-selection": hasSelection ? "" : void 0,
1102
+ "data-selection-mode": selectionMode ?? void 0,
1067
1103
  children
1068
1104
  }
1069
1105
  );
@@ -1123,10 +1159,12 @@ var iconButtonVariants = cva(
1123
1159
  "relative inline-flex items-center justify-center cursor-pointer",
1124
1160
  "overflow-hidden rounded-full",
1125
1161
  // Circular shape
1126
- // Spatial (border-radius, transform): expressive fast spring 350ms, visible overshoot
1127
- "transition-all duration-expressive-fast-spatial ease-expressive-fast-spatial",
1162
+ // Split MD3 transition: btn-transition handles spatial (border-radius) with asymmetric
1163
+ // easing (decelerate by default, switched to expressive via btn-transition-selected when
1164
+ // the button is group-selected) and effects (color/bg/shadow) with standard spring.
1165
+ "btn-transition",
1128
1166
  "focus-visible:outline-primary focus-visible:outline-2 focus-visible:outline-offset-2",
1129
- // State layers — effects token: opacity only, no overshoot
1167
+ // State layers — effects token: opacity only, no overshoot (separate ::before pseudo-element)
1130
1168
  "before:absolute before:inset-0 before:rounded-[inherit]",
1131
1169
  "before:transition-opacity before:duration-spring-standard-fast-effects before:ease-spring-standard-fast-effects",
1132
1170
  "before:bg-current before:opacity-0",
@@ -1363,6 +1401,7 @@ var IconButton = forwardRef(
1363
1401
  onMouseDown: mergedOnMouseDown,
1364
1402
  isDisabled
1365
1403
  });
1404
+ const isGroupSelected = isConnected && groupCtx && value ? groupCtx.selectedValues.has(value) : false;
1366
1405
  const connectedClasses = isConnected && groupCtx ? [
1367
1406
  ...getConnectedRadiusClasses(groupCtx, value),
1368
1407
  groupCtx.enforceMinWidth ? "min-w-12" : ""
@@ -1372,22 +1411,13 @@ var IconButton = forwardRef(
1372
1411
  {
1373
1412
  ref,
1374
1413
  className: cn(
1375
- // Base classes
1376
- "relative inline-flex items-center justify-center",
1377
- "overflow-hidden rounded-full",
1378
- // Circular shape (overridden by connected group classes)
1379
- // Spatial (border-radius, transform): expressive fast spring — 350ms, visible overshoot
1380
- "duration-expressive-fast-spatial ease-expressive-fast-spatial transition-all",
1381
- "focus-visible:outline-primary focus-visible:outline-2 focus-visible:outline-offset-2",
1382
- // State layers (hover, focus, active) — effects token: opacity, no overshoot
1383
- "before:absolute before:inset-0 before:rounded-[inherit]",
1384
- "before:duration-spring-standard-fast-effects before:ease-spring-standard-fast-effects before:transition-opacity",
1385
- "before:bg-current before:opacity-0",
1386
- "hover:before:opacity-8",
1387
- "focus-visible:before:opacity-12",
1388
- "active:before:opacity-12",
1389
- // CVA variants
1414
+ // CVA variants — includes btn-transition for asymmetric border-radius easing
1390
1415
  iconButtonVariants({ variant, color, size, selected: selected ?? false, isDisabled }),
1416
+ // Asymmetric border-radius easing: expressive when selected, decelerate when not.
1417
+ // btn-transition-selected overrides --_btn-radius-easing to the bouncy spring while
1418
+ // the button is gaining the pill shape; removal restores decelerate for the return
1419
+ // path, preventing the overshoot-to-0px sharp-corner flash.
1420
+ isGroupSelected ? "btn-transition-selected" : "",
1391
1421
  ...connectedClasses,
1392
1422
  // User custom classes
1393
1423
  className
@@ -1395,6 +1425,7 @@ var IconButton = forwardRef(
1395
1425
  "aria-label": ariaLabel,
1396
1426
  "data-variant": variant,
1397
1427
  "data-color": color,
1428
+ "data-group-selected": isGroupSelected ? "" : void 0,
1398
1429
  ...selected !== void 0 && { selected },
1399
1430
  ...title && { title },
1400
1431
  ...mergedPropsValue,
@@ -4786,20 +4817,6 @@ var BadgeContent = forwardRef(
4786
4817
  }
4787
4818
  );
4788
4819
  BadgeContent.displayName = "BadgeContent";
4789
- var QUERY = "(prefers-reduced-motion: reduce)";
4790
- function useReducedMotion() {
4791
- const [reduced, setReduced] = useState(() => {
4792
- if (typeof window === "undefined") return false;
4793
- return window.matchMedia(QUERY).matches;
4794
- });
4795
- useEffect(() => {
4796
- const mql = window.matchMedia(QUERY);
4797
- const handler = (e) => setReduced(e.matches);
4798
- mql.addEventListener("change", handler);
4799
- return () => mql.removeEventListener("change", handler);
4800
- }, []);
4801
- return reduced;
4802
- }
4803
4820
  var Badge = forwardRef(
4804
4821
  ({
4805
4822
  count,
@@ -14792,6 +14809,6 @@ var DateField = forwardRef((props, forwardedRef) => {
14792
14809
  });
14793
14810
  DateField.displayName = "DateField";
14794
14811
 
14795
- export { AppBar, AppBarHeadless, Badge, BadgeContent, BadgeHeadless, BottomSheet, BottomSheetContext, BottomSheetHandle, BottomSheetHeadless, Button, ButtonGroup, ButtonGroupContext, ButtonGroupHeadless, CalendarCore, Card, CardActions, CardContent, CardHeader, CardHeadless, CardMedia, Checkbox, Chip, ChipHeadless, ChipSet, DateField, DatePicker, DatePickerDocked, DatePickerModal, DatePickerModalInput, Dialog, DialogActions, DialogContent, DialogContext, DialogHeadless, DialogHeadline, Divider, DividerHeadless, Drawer, DrawerIconOnlyContext, DrawerItem, DrawerSection, FAB, FABHeadless, FABMenu, FABMenuContext, FABMenuHeadless, FABMenuItem, HeadlessDrawer, HeadlessDrawerItem, HeadlessMenu, HeadlessMenuDivider, HeadlessMenuItem, HeadlessMenuSection, HeadlessMenuTrigger, HeadlessNavigationBar, HeadlessNavigationBarItem, HeadlessTab, HeadlessTabList, HeadlessTabPanel, IconButton, IconButtonHeadless, List, ListHeadless, ListItem, ListItemHeadless, ListItemLeading, ListItemText, ListItemTrailing, Menu, MenuContext, MenuDivider, MenuItem, MenuSection, MenuTrigger, NavigationBar, NavigationBarItem, Progress, ProgressHeadless, Radio, RadioGroup, RadioGroupHeadless, RadioHeadless, RichTooltip, STATE_LAYER_OPACITY, Search, SearchBar, SearchBarHeadless, SearchView, SearchViewHeadless, Slider, SliderHeadless, Snackbar, SnackbarContext, SnackbarHeadless, SnackbarProvider, SplitButton, SplitButtonHeadless, Switch, TYPOGRAPHY_ELEMENT_MAP, TYPOGRAPHY_USAGE, Tab, TabList, TabPanel, Tabs, TextField, TimePicker, TimePickerDial, TimePickerInput, Tooltip, TooltipOverlayHeadless, TooltipTrigger, TooltipTriggerHeadless, applyStateLayer, badgeVariants2 as badgeVariants, bottomSheetAnimationVariants, bottomSheetHandlePillVariants, bottomSheetHandleWrapperVariants, bottomSheetScrimVariants, bottomSheetVariants, buttonGroupVariants, calendarCellVariants, cardVariants, chipVariants, clockDialContainerVariants, clockDialNumberVariants, clockHandCenterVariants, clockHandHandleVariants, clockHandTrackVariants, cn, datePickerActionButtonVariants, datePickerActionVariants, datePickerContainerVariants, datePickerDividerVariants, datePickerHeaderVariants, datePickerHeadlineVariants, datePickerNavVariants, datePickerRangeIndicatorVariants, datePickerScrimVariants, datePickerSupportingTextVariants, datePickerWeekdayVariants, dividerVariants, fabMenuItemVariants, fabMenuVariants, generateMD3Theme, getColorValue, getConnectedRadiusClasses, getFontFamily, getMD3Color, getResponsiveTypography, getTypographyClassName, getTypographyForElement, getTypographyStyle, getTypographyToken, hexToRgb, listItemVariants, listVariants, periodSelectorContainerVariants, periodSelectorItemVariants, pxToRem, remToPx, rgbToHex, richTooltipVariants, searchBarVariants, searchViewHeaderVariants, searchViewVariants, sliderActiveTrackVariants, sliderContainerVariants, sliderHandleStateLayerVariants, sliderHandleVariants, sliderInactiveTrackVariants, sliderTrackLayoutVariants, splitButtonContainerVariants, splitButtonDropdownVariants, splitButtonPrimaryVariants, splitButtonVariants, timeInputFieldVariants, timePickerActionButtonVariants, timePickerActionRowVariants, timePickerContainerVariants, timePickerHeadlineVariants, timePickerModeToggleVariants, timeSelectorContainerVariants, timeSeparatorVariants, tooltipVariants, truncateText, useBottomSheetContext, useBottomSheetDrag, useButtonGroup, useDialogContext, useFABMenuContext, useMenuContext, useOptionalButtonGroup, useSnackbar, withOpacity, yearItemVariants };
14812
+ export { AppBar, AppBarHeadless, Badge, BadgeContent, BadgeHeadless, BottomSheet, BottomSheetContext, BottomSheetHandle, BottomSheetHeadless, Button, ButtonGroup, ButtonGroupContext, ButtonGroupHeadless, CalendarCore, Card, CardActions, CardContent, CardHeader, CardHeadless, CardMedia, Checkbox, Chip, ChipHeadless, ChipSet, DateField, DatePicker, DatePickerDocked, DatePickerModal, DatePickerModalInput, Dialog, DialogActions, DialogContent, DialogContext, DialogHeadless, DialogHeadline, Divider, DividerHeadless, Drawer, DrawerIconOnlyContext, DrawerItem, DrawerSection, FAB, FABHeadless, FABMenu, FABMenuContext, FABMenuHeadless, FABMenuItem, HeadlessDrawer, HeadlessDrawerItem, HeadlessMenu, HeadlessMenuDivider, HeadlessMenuItem, HeadlessMenuSection, HeadlessMenuTrigger, HeadlessNavigationBar, HeadlessNavigationBarItem, HeadlessTab, HeadlessTabList, HeadlessTabPanel, IconButton, IconButtonHeadless, List, ListHeadless, ListItem, ListItemHeadless, ListItemLeading, ListItemText, ListItemTrailing, Menu, MenuContext, MenuDivider, MenuItem, MenuSection, MenuTrigger, NavigationBar, NavigationBarItem, Progress, ProgressHeadless, Radio, RadioGroup, RadioGroupHeadless, RadioHeadless, RichTooltip, STATE_LAYER_OPACITY, Search, SearchBar, SearchBarHeadless, SearchView, SearchViewHeadless, Slider, SliderHeadless, Snackbar, SnackbarContext, SnackbarHeadless, SnackbarProvider, SplitButton, SplitButtonHeadless, Switch, TYPOGRAPHY_ELEMENT_MAP, TYPOGRAPHY_USAGE, Tab, TabList, TabPanel, Tabs, TextField, TimePicker, TimePickerDial, TimePickerInput, Tooltip, TooltipOverlayHeadless, TooltipTrigger, TooltipTriggerHeadless, applyStateLayer, badgeVariants2 as badgeVariants, bottomSheetAnimationVariants, bottomSheetHandlePillVariants, bottomSheetHandleWrapperVariants, bottomSheetScrimVariants, bottomSheetVariants, buttonGroupFocusRingVariants, buttonGroupRootVariants, buttonGroupVariants, calendarCellVariants, cardVariants, chipVariants, clockDialContainerVariants, clockDialNumberVariants, clockHandCenterVariants, clockHandHandleVariants, clockHandTrackVariants, cn, datePickerActionButtonVariants, datePickerActionVariants, datePickerContainerVariants, datePickerDividerVariants, datePickerHeaderVariants, datePickerHeadlineVariants, datePickerNavVariants, datePickerRangeIndicatorVariants, datePickerScrimVariants, datePickerSupportingTextVariants, datePickerWeekdayVariants, dividerVariants, fabMenuItemVariants, fabMenuVariants, generateMD3Theme, getColorValue, getConnectedRadiusClasses, getFontFamily, getMD3Color, getResponsiveTypography, getTypographyClassName, getTypographyForElement, getTypographyStyle, getTypographyToken, hexToRgb, listItemVariants, listVariants, periodSelectorContainerVariants, periodSelectorItemVariants, pxToRem, remToPx, rgbToHex, richTooltipVariants, searchBarVariants, searchViewHeaderVariants, searchViewVariants, sliderActiveTrackVariants, sliderContainerVariants, sliderHandleStateLayerVariants, sliderHandleVariants, sliderInactiveTrackVariants, sliderTrackLayoutVariants, splitButtonContainerVariants, splitButtonDropdownVariants, splitButtonPrimaryVariants, splitButtonVariants, timeInputFieldVariants, timePickerActionButtonVariants, timePickerActionRowVariants, timePickerContainerVariants, timePickerHeadlineVariants, timePickerModeToggleVariants, timeSelectorContainerVariants, timeSeparatorVariants, tooltipVariants, truncateText, useBottomSheetContext, useBottomSheetDrag, useButtonGroup, useDialogContext, useFABMenuContext, useMenuContext, useOptionalButtonGroup, useSnackbar, withOpacity, yearItemVariants };
14796
14813
  //# sourceMappingURL=index.js.map
14797
14814
  //# sourceMappingURL=index.js.map