@tinybigui/react 0.4.1 → 0.5.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
@@ -426,218 +426,206 @@ var ButtonHeadless = React.forwardRef(
426
426
  ButtonHeadless.displayName = "ButtonHeadless";
427
427
  var buttonVariants = classVarianceAuthority.cva(
428
428
  [
429
- // Base classes (always applied)
430
- "relative inline-flex items-center justify-center cursor-pointer",
431
- "overflow-hidden rounded-full font-medium",
432
- // Split MD3 transition: spatial (border-radius) uses expressive spring with overshoot;
433
- // effects (color/bg/shadow) use standard effects spring — no overshoot allowed on color.
429
+ // Layout + shape — NO overflow-hidden here (see note above)
430
+ "relative inline-flex items-center justify-center",
431
+ "rounded-full cursor-pointer select-none",
432
+ // Split MD3 transition: spatial (border-radius) expressive spring;
433
+ // effects (color/bg/shadow) standard effects spring.
434
434
  "btn-transition",
435
- "tracking-[0.1px]",
436
- // MD3 spec: +0.1px letter-spacing for label-large
437
- "focus-visible:outline-primary focus-visible:outline-2 focus-visible:outline-offset-2"
435
+ // Disabled — self-targeting data-[x]: selectors
436
+ "data-[disabled]:cursor-not-allowed data-[disabled]:pointer-events-none"
438
437
  ],
439
438
  {
440
439
  variants: {
441
440
  /**
442
- * Button variant (MD3 specification)
441
+ * Button variant (MD3 specification — strict, no color override)
442
+ *
443
+ * Elevation per state follows _md-comp-*-button.scss tokens:
444
+ * Filled/Tonal hover→level-1, focus/pressed→level-0
445
+ * Elevated base→level-1, hover→level-2, focus/pressed→level-1
446
+ * Outlined/Text no elevation
443
447
  */
444
448
  variant: {
445
- filled: "shadow-none hover:shadow-elevation-1",
446
- // MD3: gains elevation on hover
447
- outlined: "bg-transparent border border-outline-variant",
448
- tonal: "",
449
- elevated: "shadow-elevation-1 hover:shadow-elevation-2",
450
- // MD3: level 1 → level 2 on hover
451
- text: "bg-transparent"
452
- },
453
- /**
454
- * Color scheme (MD3 color roles)
455
- */
456
- color: {
457
- primary: "",
458
- secondary: "",
459
- tertiary: "",
460
- error: ""
449
+ /**
450
+ * Filled highest emphasis.
451
+ * MD3: container=primary, label=on-primary, state-layer=on-primary
452
+ * Elevation: 0 base → 1 hover → 0 focus → 0 pressed
453
+ */
454
+ filled: [
455
+ "bg-primary text-on-primary shadow-none",
456
+ // Hover: gains level-1 elevation
457
+ "group-data-[hovered]/button:shadow-elevation-1",
458
+ // Focus/pressed: shadow must explicitly return to level-0
459
+ // (doubled attribute selector → higher specificity than hover)
460
+ "group-data-[focus-visible]/button:shadow-none",
461
+ "group-data-[pressed]/button:group-data-[pressed]/button:shadow-none",
462
+ // Disabled overrides
463
+ "group-data-[disabled]/button:bg-on-surface/12",
464
+ "group-data-[disabled]/button:text-on-surface/38",
465
+ "group-data-[disabled]/button:shadow-none"
466
+ ],
467
+ /**
468
+ * Outlined — medium emphasis. Transparent with border.
469
+ * MD3: container=transparent, outline=outline, label=primary, state-layer=primary
470
+ * Elevation: always 0
471
+ */
472
+ outlined: [
473
+ "bg-transparent border border-outline text-primary",
474
+ // Disabled overrides
475
+ "group-data-[disabled]/button:border-on-surface/12",
476
+ "group-data-[disabled]/button:text-on-surface/38"
477
+ ],
478
+ /**
479
+ * Tonal — secondary emphasis.
480
+ * MD3 name: "Filled tonal". container=secondary-container, label=on-secondary-container
481
+ * Elevation: 0 base → 1 hover → 0 focus → 0 pressed
482
+ */
483
+ tonal: [
484
+ "bg-secondary-container text-on-secondary-container shadow-none",
485
+ // Hover: gains level-1 elevation (same as filled)
486
+ "group-data-[hovered]/button:shadow-elevation-1",
487
+ // Focus/pressed: return to level-0
488
+ "group-data-[focus-visible]/button:shadow-none",
489
+ "group-data-[pressed]/button:group-data-[pressed]/button:shadow-none",
490
+ // Disabled overrides
491
+ "group-data-[disabled]/button:bg-on-surface/12",
492
+ "group-data-[disabled]/button:text-on-surface/38",
493
+ "group-data-[disabled]/button:shadow-none"
494
+ ],
495
+ /**
496
+ * Elevated — separation via shadow.
497
+ * MD3: container=surface-container-low, label=primary
498
+ * Elevation: 1 base → 2 hover → 1 focus → 1 pressed
499
+ */
500
+ elevated: [
501
+ "bg-surface-container-low text-primary shadow-elevation-1",
502
+ // Hover: gains extra elevation
503
+ "group-data-[hovered]/button:shadow-elevation-2",
504
+ // Focus/pressed: return to base level-1
505
+ // (doubled selector wins over single hover selector at same cascade position)
506
+ "group-data-[focus-visible]/button:shadow-elevation-1",
507
+ "group-data-[pressed]/button:group-data-[pressed]/button:shadow-elevation-1",
508
+ // Disabled overrides
509
+ "group-data-[disabled]/button:bg-on-surface/12",
510
+ "group-data-[disabled]/button:text-on-surface/38",
511
+ "group-data-[disabled]/button:shadow-none"
512
+ ],
513
+ /**
514
+ * Text — lowest emphasis.
515
+ * MD3: container=transparent, label=primary, state-layer=primary
516
+ * Elevation: always 0
517
+ */
518
+ text: [
519
+ "bg-transparent text-primary",
520
+ // Disabled overrides
521
+ "group-data-[disabled]/button:text-on-surface/38"
522
+ ]
461
523
  },
462
524
  /**
463
525
  * Button size
526
+ * MD3 spec: small=32dp, medium=40dp, large=56dp
527
+ * Padding: small=16dp, medium=24dp, large=32dp
528
+ * Text variant uses reduced padding: small=12dp, medium=12dp
464
529
  */
465
530
  size: {
466
- small: "h-8 px-4 text-sm gap-2",
467
- medium: "h-10 px-6 text-sm gap-2",
468
- large: "h-12 px-8 text-base gap-3"
531
+ small: "h-8 px-4 gap-1 text-label-medium tracking-[0.1px]",
532
+ medium: "h-10 px-6 gap-2 text-label-large tracking-[0.1px]",
533
+ large: "h-14 px-8 gap-2 text-title-medium"
469
534
  },
470
535
  /**
471
- * Full width variant
536
+ * Full width button (spans container)
472
537
  */
473
538
  fullWidth: {
474
539
  true: "w-full",
475
540
  false: ""
476
- },
477
- /**
478
- * Disabled state (MD3 spec: container 12% opacity, content 38% opacity)
479
- */
480
- disabled: {
481
- true: [
482
- "pointer-events-none cursor-not-allowed",
483
- "bg-on-surface/12",
484
- // MD3: disabled container uses on-surface at 12%
485
- "text-on-surface/38",
486
- // MD3: disabled text/icons use on-surface at 38%
487
- "border-on-surface/12",
488
- // For outlined variant
489
- "shadow-none"
490
- // Remove elevation when disabled
491
- ],
492
- false: ""
493
- },
494
- /**
495
- * Loading state
496
- */
497
- loading: {
498
- true: "cursor-wait",
499
- false: ""
500
541
  }
501
542
  },
502
543
  /**
503
- * Compound variants - combinations of variant + color
544
+ * Compound variants for text variant reduced padding per size
545
+ * MD3: text buttons use 12dp padding (px-3) instead of standard padding
504
546
  */
505
547
  compoundVariants: [
506
- // ====================
507
- // FILLED VARIANTS
508
- // ====================
509
- {
510
- variant: "filled",
511
- color: "primary",
512
- className: "bg-primary text-on-primary"
513
- },
514
- {
515
- variant: "filled",
516
- color: "secondary",
517
- className: "bg-secondary text-on-secondary"
518
- },
519
- {
520
- variant: "filled",
521
- color: "tertiary",
522
- className: "bg-tertiary text-on-tertiary"
523
- },
524
- {
525
- variant: "filled",
526
- color: "error",
527
- className: "bg-error text-on-error"
528
- },
529
- // ====================
530
- // OUTLINED VARIANTS
531
- // ====================
532
- {
533
- variant: "outlined",
534
- color: "primary",
535
- className: "text-primary"
536
- },
537
- {
538
- variant: "outlined",
539
- color: "secondary",
540
- className: "text-secondary"
541
- },
542
- {
543
- variant: "outlined",
544
- color: "tertiary",
545
- className: "text-tertiary"
546
- },
547
- {
548
- variant: "outlined",
549
- color: "error",
550
- className: "text-error"
551
- },
552
- // ====================
553
- // TONAL VARIANTS
554
- // ====================
555
- {
556
- variant: "tonal",
557
- color: "primary",
558
- className: "bg-primary-container text-on-primary-container"
559
- },
560
- {
561
- variant: "tonal",
562
- color: "secondary",
563
- className: "bg-secondary-container text-on-secondary-container"
564
- },
565
- {
566
- variant: "tonal",
567
- color: "tertiary",
568
- className: "bg-tertiary-container text-on-tertiary-container"
569
- },
570
- {
571
- variant: "tonal",
572
- color: "error",
573
- className: "bg-error-container text-on-error-container"
574
- },
575
- // ====================
576
- // ELEVATED VARIANTS
577
- // ====================
578
- {
579
- variant: "elevated",
580
- color: "primary",
581
- className: "bg-surface-container-low text-primary"
582
- },
583
- {
584
- variant: "elevated",
585
- color: "secondary",
586
- className: "bg-surface-container-low text-secondary"
587
- },
588
- {
589
- variant: "elevated",
590
- color: "tertiary",
591
- className: "bg-surface-container-low text-tertiary"
592
- },
593
- {
594
- variant: "elevated",
595
- color: "error",
596
- className: "bg-surface-container-low text-error"
597
- },
598
- // ====================
599
- // TEXT VARIANTS
600
- // ====================
601
- {
602
- variant: "text",
603
- color: "primary",
604
- className: "text-primary hover:bg-primary/[0.08]"
605
- // MD3: text buttons gain primary color at 8% opacity on hover
606
- },
607
- {
608
- variant: "text",
609
- color: "secondary",
610
- className: "text-secondary hover:bg-secondary/[0.08]"
611
- // MD3: text buttons gain secondary color at 8% opacity on hover
612
- },
613
- {
614
- variant: "text",
615
- color: "tertiary",
616
- className: "text-tertiary hover:bg-tertiary/[0.08]"
617
- // MD3: text buttons gain tertiary color at 8% opacity on hover
618
- },
619
- {
620
- variant: "text",
621
- color: "error",
622
- className: "text-error hover:bg-error/[0.08]"
623
- // MD3: text buttons gain error color at 8% opacity on hover
624
- }
548
+ { variant: "text", size: "small", className: "px-3" },
549
+ { variant: "text", size: "medium", className: "px-3" },
550
+ { variant: "text", size: "large", className: "px-4" }
625
551
  ],
626
- /**
627
- * Default variants
628
- */
629
552
  defaultVariants: {
630
553
  variant: "filled",
631
- color: "primary",
632
554
  size: "medium",
633
- fullWidth: false,
634
- disabled: false,
635
- loading: false
555
+ fullWidth: false
636
556
  }
637
557
  }
638
558
  );
559
+ var buttonStateLayerVariants = classVarianceAuthority.cva(
560
+ [
561
+ "absolute inset-0 rounded-[inherit] overflow-hidden pointer-events-none opacity-0",
562
+ // Effects transition for opacity — standard spring, no overshoot
563
+ "transition-opacity duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
564
+ // Hover: 8%
565
+ "group-data-[hovered]/button:opacity-8",
566
+ // Focus: 10%
567
+ "group-data-[focus-visible]/button:opacity-10",
568
+ // Pressed: 10%, doubled selector wins over hover
569
+ "group-data-[pressed]/button:group-data-[pressed]/button:opacity-10",
570
+ // No state layer when disabled
571
+ "group-data-[disabled]/button:hidden"
572
+ ],
573
+ {
574
+ variants: {
575
+ variant: {
576
+ filled: "bg-on-primary",
577
+ outlined: "bg-primary",
578
+ tonal: "bg-on-secondary-container",
579
+ elevated: "bg-primary",
580
+ text: "bg-primary"
581
+ }
582
+ },
583
+ defaultVariants: { variant: "filled" }
584
+ }
585
+ );
586
+ var buttonFocusRingVariants = classVarianceAuthority.cva([
587
+ "pointer-events-none absolute inset-[-3px] rounded-full",
588
+ "outline outline-2 outline-offset-0 outline-secondary",
589
+ // Effects transition — opacity change must not overshoot
590
+ "transition-opacity duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
591
+ "opacity-0",
592
+ "group-data-[focus-visible]/button:opacity-100"
593
+ ]);
594
+ var buttonIconVariants = classVarianceAuthority.cva(
595
+ [
596
+ "relative z-10 inline-flex shrink-0 items-center justify-center",
597
+ "size-[18px]",
598
+ // Color transition uses effects token (no spatial overshoot on color)
599
+ "transition-colors duration-spring-standard-fast-effects ease-spring-standard-fast-effects"
600
+ ],
601
+ {
602
+ variants: {
603
+ hidden: {
604
+ true: "invisible",
605
+ false: ""
606
+ }
607
+ },
608
+ defaultVariants: { hidden: false }
609
+ }
610
+ );
611
+ var buttonLabelVariants = classVarianceAuthority.cva(["relative z-10 inline-flex items-center"]);
612
+ var QUERY = "(prefers-reduced-motion: reduce)";
613
+ function useReducedMotion() {
614
+ const [reduced, setReduced] = React.useState(() => {
615
+ if (typeof window === "undefined") return false;
616
+ return window.matchMedia(QUERY).matches;
617
+ });
618
+ React.useEffect(() => {
619
+ const mql = window.matchMedia(QUERY);
620
+ const handler = (e) => setReduced(e.matches);
621
+ mql.addEventListener("change", handler);
622
+ return () => mql.removeEventListener("change", handler);
623
+ }, []);
624
+ return reduced;
625
+ }
639
626
  function useRipple(options = {}) {
640
627
  const { disabled = false, color = "currentColor", duration = 450 } = options;
628
+ const prefersReducedMotion = useReducedMotion();
641
629
  const [ripples, setRipples] = React.useState([]);
642
630
  const rippleKeyCounter = React.useRef(0);
643
631
  const timersRef = React.useRef([]);
@@ -648,7 +636,7 @@ function useRipple(options = {}) {
648
636
  }, []);
649
637
  const onMouseDown = React.useCallback(
650
638
  (event) => {
651
- if (disabled) return;
639
+ if (disabled || prefersReducedMotion) return;
652
640
  const element = event.currentTarget;
653
641
  const rect = element.getBoundingClientRect();
654
642
  const x = event.clientX - rect.left;
@@ -664,9 +652,9 @@ function useRipple(options = {}) {
664
652
  }, duration);
665
653
  timersRef.current.push(timer);
666
654
  },
667
- [disabled, duration]
655
+ [disabled, duration, prefersReducedMotion]
668
656
  );
669
- const rippleElements = disabled ? null : /* @__PURE__ */ jsxRuntime.jsx(
657
+ const rippleElements = disabled || prefersReducedMotion ? null : /* @__PURE__ */ jsxRuntime.jsx(
670
658
  "span",
671
659
  {
672
660
  "data-ripple-container": true,
@@ -674,7 +662,7 @@ function useRipple(options = {}) {
674
662
  children: ripples.map((ripple) => /* @__PURE__ */ jsxRuntime.jsx(
675
663
  "span",
676
664
  {
677
- className: "animate-ripple absolute rounded-full opacity-12",
665
+ className: "animate-md-ripple absolute rounded-full opacity-12",
678
666
  style: {
679
667
  left: ripple.x,
680
668
  top: ripple.y,
@@ -766,7 +754,7 @@ var Spinner = () => /* @__PURE__ */ jsxRuntime.jsxs(
766
754
  {
767
755
  role: "progressbar",
768
756
  "aria-label": "Loading",
769
- className: "h-4 w-4 animate-spin",
757
+ className: "relative z-10 h-[18px] w-[18px] animate-spin",
770
758
  xmlns: "http://www.w3.org/2000/svg",
771
759
  fill: "none",
772
760
  viewBox: "0 0 24 24",
@@ -787,7 +775,6 @@ var Button = React.forwardRef(
787
775
  ({
788
776
  // Variant props (CVA)
789
777
  variant = "filled",
790
- color = "primary",
791
778
  size = "medium",
792
779
  fullWidth = false,
793
780
  // Content props
@@ -800,64 +787,84 @@ var Button = React.forwardRef(
800
787
  isDisabled = false,
801
788
  // Styling
802
789
  className,
803
- // Other props
790
+ // Other button props
804
791
  tabIndex = 0,
805
792
  type = "button",
806
- onPress,
793
+ // Passed through to ButtonHeadless → useButton
807
794
  ...props
808
795
  }, ref) => {
796
+ const buttonRef = React.useRef(null);
797
+ const resolvedRef = ref ?? buttonRef;
809
798
  const groupCtx = useOptionalButtonGroup();
810
799
  const isConnected = groupCtx?.variant === "connected";
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
- if (icon && trailingIcon) {
818
- console.warn("[Button] Button should have either icon or trailingIcon, not both.");
819
- }
820
- }
821
800
  const isButtonDisabled = isDisabled || loading;
801
+ const [isPressed, setIsPressed] = React.useState(false);
802
+ const handlePressStart = React.useCallback(() => setIsPressed(true), []);
803
+ const handlePressEnd = React.useCallback(() => setIsPressed(false), []);
804
+ const { isHovered, hoverProps } = reactAria.useHover({ isDisabled: isButtonDisabled });
805
+ const { isFocusVisible, focusProps } = reactAria.useFocusRing();
822
806
  const { onMouseDown: handleRipple, ripples } = useRipple({
823
807
  disabled: isButtonDisabled || disableRipple
824
808
  });
809
+ const buttonValue = props.value;
810
+ const isGroupSelected = isConnected && groupCtx && buttonValue ? groupCtx.selectedValues.has(buttonValue) : false;
825
811
  const connectedClasses = isConnected && groupCtx ? [
826
- ...getConnectedRadiusClasses(groupCtx, props?.value),
812
+ ...getConnectedRadiusClasses(groupCtx, buttonValue),
827
813
  groupCtx.enforceMinWidth ? "min-w-12" : ""
828
814
  ] : [];
815
+ const hasIcon = !!icon || !!trailingIcon;
816
+ if (process.env.NODE_ENV === "development") {
817
+ if (!children) {
818
+ console.warn(
819
+ "[Button] Button should have text content. Use IconButton for icon-only buttons."
820
+ );
821
+ }
822
+ }
829
823
  return /* @__PURE__ */ jsxRuntime.jsxs(
830
824
  ButtonHeadless,
831
825
  {
832
- ...props,
833
- ref,
826
+ ...reactAria.mergeProps(
827
+ hoverProps,
828
+ focusProps,
829
+ // Track pressed state via useButton's press lifecycle callbacks,
830
+ // rather than a separate usePress hook, to avoid event handler conflicts.
831
+ { onPressStart: handlePressStart, onPressEnd: handlePressEnd },
832
+ props
833
+ ),
834
+ ref: resolvedRef,
834
835
  type,
835
836
  isDisabled: isButtonDisabled,
836
- ...onPress && { onPress },
837
837
  tabIndex,
838
838
  onMouseDown: handleRipple,
839
+ ...getInteractionDataAttributes({
840
+ isHovered,
841
+ isFocusVisible,
842
+ isPressed,
843
+ isDisabled: isButtonDisabled
844
+ }),
839
845
  "data-variant": variant,
840
- "data-color": color,
846
+ "data-with-icon": hasIcon ? "" : void 0,
847
+ "data-loading": loading ? "" : void 0,
848
+ "data-group-selected": isGroupSelected ? "" : void 0,
841
849
  className: cn(
842
- // Apply CVA variants (includes rounded-full base)
843
- buttonVariants({
844
- variant,
845
- color,
846
- size,
847
- fullWidth,
848
- disabled: isButtonDisabled,
849
- loading
850
- }),
850
+ buttonVariants({ variant, size, fullWidth }),
851
+ // group/button: enables group-data-[x]/button child selectors in all slots
852
+ // (added here, not in CVA, following the Switch pattern)
853
+ "group/button",
854
+ // Asymmetric border-radius easing: expressive when selected, decelerate when not
855
+ isGroupSelected ? "btn-transition-selected" : "",
851
856
  ...connectedClasses,
852
857
  // User custom classes
853
858
  className
854
859
  ),
855
860
  children: [
856
861
  ripples,
857
- icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("relative z-10 inline-flex shrink-0", loading && "invisible"), children: icon }),
858
- loading && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relative z-10", children: /* @__PURE__ */ jsxRuntime.jsx(Spinner, {}) }),
859
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relative z-10 inline-flex items-center", children }),
860
- trailingIcon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("relative z-10 inline-flex shrink-0", loading && "invisible"), children: trailingIcon })
862
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(buttonStateLayerVariants({ variant })), "aria-hidden": "true" }),
863
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(buttonFocusRingVariants()), "aria-hidden": "true" }),
864
+ icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(buttonIconVariants({ hidden: loading })), children: icon }),
865
+ loading && /* @__PURE__ */ jsxRuntime.jsx(Spinner, {}),
866
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(buttonLabelVariants()), children }),
867
+ trailingIcon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(buttonIconVariants({ hidden: loading })), children: trailingIcon })
861
868
  ]
862
869
  }
863
870
  );
@@ -871,6 +878,7 @@ var ButtonGroupHeadless = React.forwardRef(
871
878
  size = "medium",
872
879
  shape = "round",
873
880
  selectionMode,
881
+ isDisabled = false,
874
882
  // Selection — controlled
875
883
  selectedValues: controlledValues,
876
884
  onSelectionChange: onControlledChange,
@@ -892,7 +900,7 @@ var ButtonGroupHeadless = React.forwardRef(
892
900
  const isControlled = controlledValues !== void 0;
893
901
  const selectedValues = isControlled ? controlledValues : uncontrolledValues;
894
902
  const handleSelectionChange = (value) => {
895
- if (!selectionMode) return;
903
+ if (!selectionMode || isDisabled) return;
896
904
  let nextValues;
897
905
  if (selectionMode === "multi") {
898
906
  nextValues = new Set(selectedValues);
@@ -933,6 +941,7 @@ var ButtonGroupHeadless = React.forwardRef(
933
941
  selectionMode,
934
942
  selectedValues,
935
943
  onSelectionChange: handleSelectionChange,
944
+ isDisabled,
936
945
  connectedInnerRadius: getInnerRadius(size),
937
946
  connectedOuterRadius: getOuterRadius(shape, size),
938
947
  enforceMinWidth: variant === "connected" && (size === "extra-small" || size === "small")
@@ -945,9 +954,15 @@ var ButtonGroupHeadless = React.forwardRef(
945
954
  }
946
955
  );
947
956
  ButtonGroupHeadless.displayName = "ButtonGroupHeadless";
948
- var buttonGroupVariants = classVarianceAuthority.cva(
949
- // Base classes applied to every ButtonGroup container
950
- ["items-center"],
957
+ var buttonGroupRootVariants = classVarianceAuthority.cva(
958
+ [
959
+ // Layout
960
+ "items-center",
961
+ // Spatial motion for gap changes — standard spring (calm, utility UI)
962
+ "transition-[gap] duration-spring-standard-fast-spatial ease-spring-standard-fast-spatial",
963
+ // Disabled state — self-targeting data-[x]: selector on root
964
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-38"
965
+ ],
951
966
  {
952
967
  variants: {
953
968
  /**
@@ -995,6 +1010,14 @@ var buttonGroupVariants = classVarianceAuthority.cva(
995
1010
  }
996
1011
  }
997
1012
  );
1013
+ var buttonGroupFocusRingVariants = classVarianceAuthority.cva([
1014
+ "pointer-events-none absolute inset-[-3px] rounded-[inherit]",
1015
+ "outline outline-2 outline-offset-0 outline-secondary",
1016
+ "transition-opacity duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
1017
+ "opacity-0",
1018
+ "group-data-[focus-visible]/button-group:opacity-100"
1019
+ ]);
1020
+ var buttonGroupVariants = buttonGroupRootVariants;
998
1021
  var ButtonGroup = React.forwardRef(
999
1022
  ({
1000
1023
  variant = "standard",
@@ -1004,6 +1027,7 @@ var ButtonGroup = React.forwardRef(
1004
1027
  selectedValues,
1005
1028
  onSelectionChange,
1006
1029
  defaultValue,
1030
+ isDisabled = false,
1007
1031
  children,
1008
1032
  className,
1009
1033
  ...htmlProps
@@ -1020,6 +1044,13 @@ var ButtonGroup = React.forwardRef(
1020
1044
  },
1021
1045
  [ref]
1022
1046
  );
1047
+ const hasSelection = React.useMemo(() => {
1048
+ if (selectedValues) return selectedValues.size > 0;
1049
+ if (defaultValue) {
1050
+ return Array.isArray(defaultValue) ? defaultValue.length > 0 : true;
1051
+ }
1052
+ return false;
1053
+ }, [selectedValues, defaultValue]);
1023
1054
  if (process.env.NODE_ENV === "development") {
1024
1055
  const childArray = Array.isArray(children) ? children : [children];
1025
1056
  for (const child of childArray) {
@@ -1068,7 +1099,12 @@ var ButtonGroup = React.forwardRef(
1068
1099
  selectedValues,
1069
1100
  onSelectionChange,
1070
1101
  defaultValue,
1071
- className: cn(buttonGroupVariants({ variant, size }), className),
1102
+ isDisabled,
1103
+ className: cn(buttonGroupRootVariants({ variant, size }), "group/button-group", className),
1104
+ ...getInteractionDataAttributes({ isDisabled }),
1105
+ "data-connected": variant === "connected" ? "" : void 0,
1106
+ "data-has-selection": hasSelection ? "" : void 0,
1107
+ "data-selection-mode": selectionMode ?? void 0,
1072
1108
  children
1073
1109
  }
1074
1110
  );
@@ -1082,79 +1118,113 @@ var IconButtonHeadless = React.forwardRef(
1082
1118
  tabIndex = 0,
1083
1119
  onMouseDown,
1084
1120
  type,
1085
- selected,
1121
+ isSelected,
1122
+ isToggle = false,
1123
+ isDisabled = false,
1086
1124
  "aria-label": ariaLabel,
1087
1125
  title,
1088
1126
  ...props
1089
1127
  }, forwardedRef) => {
1090
1128
  const internalRef = React.useRef(null);
1091
1129
  const ref = forwardedRef ?? internalRef;
1092
- const { buttonProps } = reactAria.useButton(
1130
+ const { buttonProps, isPressed } = reactAria.useButton(
1093
1131
  {
1094
1132
  ...props,
1095
- // Ensure element type is 'button' for proper semantics
1096
1133
  elementType: "button",
1097
- // Pass aria-label
1098
- "aria-label": ariaLabel
1134
+ "aria-label": ariaLabel,
1135
+ isDisabled
1099
1136
  },
1100
1137
  ref
1101
1138
  );
1139
+ const { isHovered, hoverProps } = reactAria.useHover({ isDisabled });
1140
+ const { isFocusVisible, focusProps } = reactAria.useFocusRing();
1102
1141
  const domProps = utils.filterDOMProps(props);
1103
- const mergedProps = utils.mergeProps(
1104
- buttonProps,
1105
- domProps,
1142
+ const mergedProps = utils.mergeProps(buttonProps, hoverProps, focusProps, domProps, {
1143
+ tabIndex,
1144
+ className,
1145
+ onMouseDown,
1146
+ type: type ?? "button",
1147
+ ...title && { title },
1148
+ // aria-pressed only when acting as a toggle button
1149
+ ...isToggle && { "aria-pressed": isSelected ?? false }
1150
+ });
1151
+ return /* @__PURE__ */ jsxRuntime.jsx(
1152
+ "button",
1106
1153
  {
1107
- tabIndex,
1108
- className,
1109
- onMouseDown,
1110
- type: type ?? "button",
1111
- // Add aria-pressed for toggle buttons (only if selected is defined)
1112
- ...selected !== void 0 && { "aria-pressed": selected },
1113
- // Add title if provided
1114
- ...title && { title }
1154
+ ...mergedProps,
1155
+ ref,
1156
+ type: type === "submit" ? "submit" : type === "reset" ? "reset" : "button",
1157
+ ...getInteractionDataAttributes({
1158
+ isHovered,
1159
+ isFocusVisible,
1160
+ isPressed,
1161
+ ...isToggle ? { isSelected: isSelected ?? false } : {},
1162
+ isDisabled
1163
+ }),
1164
+ "data-toggle": isToggle ? "" : void 0,
1165
+ children
1115
1166
  }
1116
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1117
- );
1118
- return (
1119
- // eslint-disable-next-line react/button-has-type
1120
- /* @__PURE__ */ jsxRuntime.jsx("button", { ...mergedProps, ref, type: type ?? "button", children })
1121
1167
  );
1122
1168
  }
1123
1169
  );
1124
1170
  IconButtonHeadless.displayName = "IconButtonHeadless";
1125
- var iconButtonVariants = classVarianceAuthority.cva(
1171
+ var iconButtonRootVariants = classVarianceAuthority.cva(
1126
1172
  [
1127
- // Base classes (always applied)
1128
- "relative inline-flex items-center justify-center cursor-pointer",
1129
- "overflow-hidden rounded-full",
1130
- // Circular shape
1131
- // Spatial (border-radius, transform): expressive fast spring350ms, visible overshoot
1132
- "transition-all duration-expressive-fast-spatial ease-expressive-fast-spatial",
1133
- "focus-visible:outline-primary focus-visible:outline-2 focus-visible:outline-offset-2",
1134
- // State layers — effects token: opacity only, no overshoot
1135
- "before:absolute before:inset-0 before:rounded-[inherit]",
1136
- "before:transition-opacity before:duration-spring-standard-fast-effects before:ease-spring-standard-fast-effects",
1137
- "before:bg-current before:opacity-0",
1138
- "hover:before:opacity-8",
1139
- "focus-visible:before:opacity-12",
1140
- "active:before:opacity-12"
1173
+ // Layout
1174
+ "relative inline-flex items-center justify-center",
1175
+ "cursor-pointer select-none",
1176
+ "overflow-hidden",
1177
+ // Corner radius driven by CSS variableset per shape×size in compoundVariants.
1178
+ // Fallback 9999px is only reached if both shape and size props are absent,
1179
+ // which cannot happen in normal usage.
1180
+ "rounded-[var(--ib-radius,9999px)]",
1181
+ // Split MD3 transition via the existing btn-transition utility:
1182
+ // border-radius → emphasized-decelerate (no overshoot, no sharp-corner flash)
1183
+ // color/bg/border/opacity → standard-fast-effects (no overshoot on effects)
1184
+ // This is identical to the approach used by Button/connected ButtonGroup and is
1185
+ // the standard fix for the 9999px overshoot problem documented in styles.css.
1186
+ "btn-transition",
1187
+ // Background + border + text driven from CSS role variables
1188
+ "bg-[var(--ib-bg,transparent)]",
1189
+ "border border-[var(--ib-border,transparent)]",
1190
+ "text-[var(--ib-fg,currentColor)]",
1191
+ // Toggle: off state (data-toggle present but data-selected absent)
1192
+ // Uses doubly-chained selector to beat single-chain specificity of defaults
1193
+ "data-[toggle]:bg-[var(--ib-bg-off,var(--ib-bg,transparent))]",
1194
+ "data-[toggle]:text-[var(--ib-fg-off,var(--ib-fg,currentColor))]",
1195
+ // Selected state
1196
+ "data-[selected]:bg-[var(--ib-bg-on,var(--ib-bg,transparent))]",
1197
+ "data-[selected]:text-[var(--ib-fg-on,var(--ib-fg,currentColor))]",
1198
+ "data-[selected]:border-transparent",
1199
+ // Press shape-morph: radius collapses to --ib-radius-press on press
1200
+ // (only has visual effect when --ib-radius-press differs from --ib-radius)
1201
+ "data-[pressed]:rounded-[var(--ib-radius-press,var(--ib-radius,9999px))]",
1202
+ // Focus ring (outline, not a state layer — stays outside overflow-hidden
1203
+ // because it's drawn as outline on the root element itself)
1204
+ "outline-none",
1205
+ "group-data-[focus-visible]/icon-button:outline-2",
1206
+ "group-data-[focus-visible]/icon-button:outline-offset-2",
1207
+ "group-data-[focus-visible]/icon-button:outline-secondary",
1208
+ // Disabled — content opacity 38%, container handled per variant via CSS vars
1209
+ "data-[disabled]:cursor-not-allowed data-[disabled]:pointer-events-none",
1210
+ "data-[disabled]:text-on-surface/38"
1211
+ // Filled/tonal/outlined-selected backgrounds collapse to on-surface/12 — set
1212
+ // via compoundVariants on the root for variants that have a container.
1213
+ // For variants with transparent bg (standard, outlined-unselected) we do nothing.
1141
1214
  ],
1142
1215
  {
1143
1216
  variants: {
1144
1217
  /**
1145
- * Button variant (MD3 specification)
1218
+ * Visual style variant (MD3 icon button types)
1146
1219
  */
1147
1220
  variant: {
1148
- standard: "bg-transparent",
1149
- // No background
1150
- filled: "shadow-none",
1151
- // Solid background
1152
- tonal: "",
1153
- // Container background
1154
- outlined: "bg-transparent border border-outline"
1221
+ standard: "",
1222
+ filled: "data-[disabled]:bg-on-surface/12",
1223
+ tonal: "data-[disabled]:bg-on-surface/12",
1224
+ outlined: ""
1155
1225
  },
1156
1226
  /**
1157
- * Color scheme (MD3 color roles)
1227
+ * Color scheme sets CSS role variables via compoundVariants.
1158
1228
  */
1159
1229
  color: {
1160
1230
  primary: "",
@@ -1163,180 +1233,400 @@ var iconButtonVariants = classVarianceAuthority.cva(
1163
1233
  error: ""
1164
1234
  },
1165
1235
  /**
1166
- * Button size (square dimensions)
1236
+ * Size tier (M3 Expressive 5-tier)
1167
1237
  */
1168
1238
  size: {
1169
- small: "h-8 w-8",
1170
- // 32×32px
1171
- medium: "h-10 w-10",
1172
- // 40×40px (default)
1173
- large: "h-12 w-12"
1174
- // 48×48px
1239
+ xsmall: "h-8",
1240
+ small: "h-10",
1241
+ medium: "h-14",
1242
+ large: "h-24",
1243
+ xlarge: "h-[8.5rem]"
1175
1244
  },
1176
1245
  /**
1177
- * Selected state (for toggle buttons)
1246
+ * Width variant adjusts container width
1178
1247
  */
1179
- selected: {
1180
- true: "",
1181
- false: ""
1248
+ width: {
1249
+ narrow: "",
1250
+ default: "",
1251
+ wide: ""
1182
1252
  },
1183
1253
  /**
1184
- * Disabled state
1254
+ * Shape — base values only; per-size radii set via compoundVariants below.
1255
+ *
1256
+ * round: --ib-radius = half the container height (true circle), set per size.
1257
+ * --ib-radius-press = square corner for that size (set per size).
1258
+ * Morph distance is small (e.g. 28px → 16px for medium), so the
1259
+ * emphasized-decelerate curve from btn-transition produces a smooth,
1260
+ * non-overshooting transition. The old 9999px fallback caused the
1261
+ * spring to overshoot below 0 = sharp-corner flash.
1262
+ * square: --ib-radius = size-tiered MD3 corner, set per size. No press morph.
1185
1263
  */
1186
- isDisabled: {
1187
- true: "pointer-events-none cursor-not-allowed opacity-38",
1188
- false: ""
1264
+ shape: {
1265
+ round: [],
1266
+ square: []
1189
1267
  }
1190
1268
  },
1191
- /**
1192
- * Compound variants - combinations of variant + color + selected
1193
- */
1194
1269
  compoundVariants: [
1195
- // ====================
1196
- // STANDARD VARIANTS
1197
- // ====================
1198
- {
1199
- variant: "standard",
1200
- selected: false,
1201
- className: "text-on-surface-variant"
1202
- },
1270
+ // ══════════════════════════════════════════════════════════════════════
1271
+ // SIZE × WIDTH — container width
1272
+ // ══════════════════════════════════════════════════════════════════════
1273
+ { size: "xsmall", width: "narrow", className: "w-6" },
1274
+ { size: "xsmall", width: "default", className: "w-8" },
1275
+ { size: "xsmall", width: "wide", className: "w-10" },
1276
+ { size: "small", width: "narrow", className: "w-8" },
1277
+ { size: "small", width: "default", className: "w-10" },
1278
+ { size: "small", width: "wide", className: "w-13" },
1279
+ { size: "medium", width: "narrow", className: "w-12" },
1280
+ { size: "medium", width: "default", className: "w-14" },
1281
+ { size: "medium", width: "wide", className: "w-18" },
1282
+ { size: "large", width: "narrow", className: "w-18" },
1283
+ { size: "large", width: "default", className: "w-24" },
1284
+ { size: "large", width: "wide", className: "w-32" },
1285
+ { size: "xlarge", width: "narrow", className: "w-24" },
1286
+ { size: "xlarge", width: "default", className: "w-[8.5rem]" },
1287
+ { size: "xlarge", width: "wide", className: "w-42" },
1288
+ // ══════════════════════════════════════════════════════════════════════
1289
+ // SHAPE × SIZE — corner radii for both round and square shapes
1290
+ // ══════════════════════════════════════════════════════════════════════
1291
+ //
1292
+ // Round rest radius = half container height (true circle).
1293
+ // Using the exact half-height keeps the morph distance small, so the
1294
+ // no-overshoot emphasized-decelerate curve in btn-transition produces a
1295
+ // smooth animation. Using 9999px was the original cause of the sharp-
1296
+ // corner flash (the spring overshoots below 0 before settling).
1297
+ //
1298
+ // xsmall h-8 = 32px → half = 16px = 1rem
1299
+ // small h-10 = 40px → half = 20px = 1.25rem
1300
+ // medium h-14 = 56px → half = 28px = 1.75rem
1301
+ // large h-24 = 96px → half = 48px = 3rem
1302
+ // xlarge h-34 = 136px → half = 68px = 4.25rem
1303
+ //
1304
+ // Round press-morph target = MD3 square corner for that size tier.
1305
+ // Square rest radius = same MD3 corner (no morph).
1306
+ // ── round: rest radius (half height) ──────────────────────────────────
1307
+ { shape: "round", size: "xsmall", className: "[--ib-radius:1rem]" },
1308
+ { shape: "round", size: "small", className: "[--ib-radius:1.25rem]" },
1309
+ { shape: "round", size: "medium", className: "[--ib-radius:1.75rem]" },
1310
+ { shape: "round", size: "large", className: "[--ib-radius:3rem]" },
1311
+ { shape: "round", size: "xlarge", className: "[--ib-radius:4.25rem]" },
1312
+ // ── round: press-morph target (square corner for that size) ───────────
1313
+ { shape: "round", size: "xsmall", className: "[--ib-radius-press:0.75rem]" },
1314
+ { shape: "round", size: "small", className: "[--ib-radius-press:0.75rem]" },
1315
+ { shape: "round", size: "medium", className: "[--ib-radius-press:1rem]" },
1316
+ { shape: "round", size: "large", className: "[--ib-radius-press:1.75rem]" },
1317
+ { shape: "round", size: "xlarge", className: "[--ib-radius-press:1.75rem]" },
1318
+ // ── square: rest radius (MD3 shape scale) ─────────────────────────────
1319
+ // xsmall / small → 12px (0.75rem), medium → 16px (1rem), large / xlarge → 28px (1.75rem)
1320
+ { shape: "square", size: "xsmall", className: "[--ib-radius:0.75rem]" },
1321
+ { shape: "square", size: "small", className: "[--ib-radius:0.75rem]" },
1322
+ { shape: "square", size: "medium", className: "[--ib-radius:1rem]" },
1323
+ { shape: "square", size: "large", className: "[--ib-radius:1.75rem]" },
1324
+ { shape: "square", size: "xlarge", className: "[--ib-radius:1.75rem]" },
1325
+ // ══════════════════════════════════════════════════════════════════════
1326
+ // VARIANT × COLOR — CSS role variable assignments
1327
+ // Only variant × color (design-time decisions); no state variants here.
1328
+ // ══════════════════════════════════════════════════════════════════════
1329
+ // ── STANDARD ──────────────────────────────────────────────────────────
1330
+ // Non-toggle standard: transparent bg, on-surface-variant fg
1331
+ // Selected: primary fg
1332
+ // State layer: on-surface-variant (unselected) / primary (selected)
1203
1333
  {
1204
1334
  variant: "standard",
1205
- selected: true,
1206
- className: "text-primary"
1207
- },
1208
- // ====================
1209
- // FILLED VARIANTS (UNSELECTED)
1210
- // ====================
1211
- {
1212
- variant: "filled",
1213
1335
  color: "primary",
1214
- selected: false,
1215
- className: "bg-primary text-on-primary"
1336
+ className: [
1337
+ "[--ib-bg:transparent]",
1338
+ "[--ib-fg:var(--color-on-surface-variant)]",
1339
+ "[--ib-fg-on:var(--color-primary)]",
1340
+ "[--ib-sl:var(--color-on-surface-variant)]",
1341
+ // toggle-off same as non-toggle
1342
+ "[--ib-bg-off:transparent]",
1343
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1344
+ // toggle-on: selected
1345
+ "[--ib-bg-on:transparent]"
1346
+ ]
1216
1347
  },
1217
1348
  {
1218
- variant: "filled",
1349
+ variant: "standard",
1219
1350
  color: "secondary",
1220
- selected: false,
1221
- className: "bg-secondary text-on-secondary"
1351
+ className: [
1352
+ "[--ib-bg:transparent]",
1353
+ "[--ib-fg:var(--color-on-surface-variant)]",
1354
+ "[--ib-fg-on:var(--color-secondary)]",
1355
+ "[--ib-sl:var(--color-on-surface-variant)]",
1356
+ "[--ib-bg-off:transparent]",
1357
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1358
+ "[--ib-bg-on:transparent]"
1359
+ ]
1222
1360
  },
1223
1361
  {
1224
- variant: "filled",
1362
+ variant: "standard",
1225
1363
  color: "tertiary",
1226
- selected: false,
1227
- className: "bg-tertiary text-on-tertiary"
1364
+ className: [
1365
+ "[--ib-bg:transparent]",
1366
+ "[--ib-fg:var(--color-on-surface-variant)]",
1367
+ "[--ib-fg-on:var(--color-tertiary)]",
1368
+ "[--ib-sl:var(--color-on-surface-variant)]",
1369
+ "[--ib-bg-off:transparent]",
1370
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1371
+ "[--ib-bg-on:transparent]"
1372
+ ]
1228
1373
  },
1229
1374
  {
1230
- variant: "filled",
1375
+ variant: "standard",
1231
1376
  color: "error",
1232
- selected: false,
1233
- className: "bg-error text-on-error"
1377
+ className: [
1378
+ "[--ib-bg:transparent]",
1379
+ "[--ib-fg:var(--color-on-surface-variant)]",
1380
+ "[--ib-fg-on:var(--color-error)]",
1381
+ "[--ib-sl:var(--color-on-surface-variant)]",
1382
+ "[--ib-bg-off:transparent]",
1383
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1384
+ "[--ib-bg-on:transparent]"
1385
+ ]
1234
1386
  },
1235
- // ====================
1236
- // FILLED VARIANTS (SELECTED - uses container colors)
1237
- // ====================
1387
+ // ── FILLED ────────────────────────────────────────────────────────────
1388
+ // Non-toggle: bg primary / fg on-primary
1389
+ // Toggle off: bg surface-container-highest / fg primary
1390
+ // Toggle on (selected): bg primary / fg on-primary
1391
+ // State layer: on-primary (non-toggle / selected), primary (toggle-off)
1238
1392
  {
1239
1393
  variant: "filled",
1240
1394
  color: "primary",
1241
- selected: true,
1242
- className: "bg-primary-container text-on-primary-container"
1395
+ className: [
1396
+ "[--ib-bg:var(--color-primary)]",
1397
+ "[--ib-fg:var(--color-on-primary)]",
1398
+ "[--ib-sl:var(--color-on-primary)]",
1399
+ "[--ib-bg-off:var(--color-surface-container-highest)]",
1400
+ "[--ib-fg-off:var(--color-primary)]",
1401
+ "[--ib-bg-on:var(--color-primary)]",
1402
+ "[--ib-fg-on:var(--color-on-primary)]"
1403
+ ]
1243
1404
  },
1244
1405
  {
1245
1406
  variant: "filled",
1246
1407
  color: "secondary",
1247
- selected: true,
1248
- className: "bg-secondary-container text-on-secondary-container"
1408
+ className: [
1409
+ "[--ib-bg:var(--color-secondary)]",
1410
+ "[--ib-fg:var(--color-on-secondary)]",
1411
+ "[--ib-sl:var(--color-on-secondary)]",
1412
+ "[--ib-bg-off:var(--color-surface-container-highest)]",
1413
+ "[--ib-fg-off:var(--color-secondary)]",
1414
+ "[--ib-bg-on:var(--color-secondary)]",
1415
+ "[--ib-fg-on:var(--color-on-secondary)]"
1416
+ ]
1249
1417
  },
1250
1418
  {
1251
1419
  variant: "filled",
1252
1420
  color: "tertiary",
1253
- selected: true,
1254
- className: "bg-tertiary-container text-on-tertiary-container"
1421
+ className: [
1422
+ "[--ib-bg:var(--color-tertiary)]",
1423
+ "[--ib-fg:var(--color-on-tertiary)]",
1424
+ "[--ib-sl:var(--color-on-tertiary)]",
1425
+ "[--ib-bg-off:var(--color-surface-container-highest)]",
1426
+ "[--ib-fg-off:var(--color-tertiary)]",
1427
+ "[--ib-bg-on:var(--color-tertiary)]",
1428
+ "[--ib-fg-on:var(--color-on-tertiary)]"
1429
+ ]
1255
1430
  },
1256
1431
  {
1257
1432
  variant: "filled",
1258
1433
  color: "error",
1259
- selected: true,
1260
- className: "bg-error-container text-on-error-container"
1434
+ className: [
1435
+ "[--ib-bg:var(--color-error)]",
1436
+ "[--ib-fg:var(--color-on-error)]",
1437
+ "[--ib-sl:var(--color-on-error)]",
1438
+ "[--ib-bg-off:var(--color-surface-container-highest)]",
1439
+ "[--ib-fg-off:var(--color-error)]",
1440
+ "[--ib-bg-on:var(--color-error)]",
1441
+ "[--ib-fg-on:var(--color-on-error)]"
1442
+ ]
1261
1443
  },
1262
- // ====================
1263
- // TONAL VARIANTS (UNSELECTED)
1264
- // ====================
1444
+ // ── TONAL ─────────────────────────────────────────────────────────────
1445
+ // Non-toggle: bg secondary-container / fg on-secondary-container
1446
+ // Toggle off: bg surface-container-highest / fg on-surface-variant
1447
+ // Toggle on (selected): bg secondary-container / fg on-secondary-container
1265
1448
  {
1266
1449
  variant: "tonal",
1267
1450
  color: "primary",
1268
- selected: false,
1269
- className: "bg-secondary-container text-on-secondary-container"
1451
+ className: [
1452
+ "[--ib-bg:var(--color-secondary-container)]",
1453
+ "[--ib-fg:var(--color-on-secondary-container)]",
1454
+ "[--ib-sl:var(--color-on-secondary-container)]",
1455
+ "[--ib-bg-off:var(--color-surface-container-highest)]",
1456
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1457
+ "[--ib-bg-on:var(--color-secondary-container)]",
1458
+ "[--ib-fg-on:var(--color-on-secondary-container)]"
1459
+ ]
1270
1460
  },
1271
1461
  {
1272
1462
  variant: "tonal",
1273
1463
  color: "secondary",
1274
- selected: false,
1275
- className: "bg-secondary-container text-on-secondary-container"
1464
+ className: [
1465
+ "[--ib-bg:var(--color-secondary-container)]",
1466
+ "[--ib-fg:var(--color-on-secondary-container)]",
1467
+ "[--ib-sl:var(--color-on-secondary-container)]",
1468
+ "[--ib-bg-off:var(--color-surface-container-highest)]",
1469
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1470
+ "[--ib-bg-on:var(--color-secondary-container)]",
1471
+ "[--ib-fg-on:var(--color-on-secondary-container)]"
1472
+ ]
1276
1473
  },
1277
1474
  {
1278
1475
  variant: "tonal",
1279
1476
  color: "tertiary",
1280
- selected: false,
1281
- className: "bg-tertiary-container text-on-tertiary-container"
1477
+ className: [
1478
+ "[--ib-bg:var(--color-tertiary-container)]",
1479
+ "[--ib-fg:var(--color-on-tertiary-container)]",
1480
+ "[--ib-sl:var(--color-on-tertiary-container)]",
1481
+ "[--ib-bg-off:var(--color-surface-container-highest)]",
1482
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1483
+ "[--ib-bg-on:var(--color-tertiary-container)]",
1484
+ "[--ib-fg-on:var(--color-on-tertiary-container)]"
1485
+ ]
1282
1486
  },
1283
1487
  {
1284
1488
  variant: "tonal",
1285
1489
  color: "error",
1286
- selected: false,
1287
- className: "bg-error-container text-on-error-container"
1490
+ className: [
1491
+ "[--ib-bg:var(--color-error-container)]",
1492
+ "[--ib-fg:var(--color-on-error-container)]",
1493
+ "[--ib-sl:var(--color-on-error-container)]",
1494
+ "[--ib-bg-off:var(--color-surface-container-highest)]",
1495
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1496
+ "[--ib-bg-on:var(--color-error-container)]",
1497
+ "[--ib-fg-on:var(--color-on-error-container)]"
1498
+ ]
1288
1499
  },
1289
- // ====================
1290
- // TONAL VARIANTS (SELECTED - uses tertiary container)
1291
- // ====================
1500
+ // ── OUTLINED ──────────────────────────────────────────────────────────
1501
+ // Non-toggle: transparent bg, border-outline, on-surface-variant fg
1502
+ // Toggle off: same as non-toggle
1503
+ // Toggle on (selected): inverse-surface bg, inverse-on-surface fg, no border
1504
+ // Disabled: border becomes on-surface/12 (set via Tailwind utility on root)
1292
1505
  {
1293
- variant: "tonal",
1294
- selected: true,
1295
- className: "bg-tertiary-container text-on-tertiary-container"
1506
+ variant: "outlined",
1507
+ color: "primary",
1508
+ className: [
1509
+ "[--ib-bg:transparent]",
1510
+ "[--ib-fg:var(--color-on-surface-variant)]",
1511
+ "[--ib-sl:var(--color-on-surface-variant)]",
1512
+ "[--ib-border:var(--color-outline)]",
1513
+ "[--ib-bg-off:transparent]",
1514
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1515
+ "[--ib-bg-on:var(--color-inverse-surface)]",
1516
+ "[--ib-fg-on:var(--color-inverse-on-surface)]",
1517
+ // Disabled outlined border
1518
+ "data-[disabled]:border-on-surface/12"
1519
+ ]
1296
1520
  },
1297
- // ====================
1298
- // OUTLINED VARIANTS (UNSELECTED)
1299
- // ====================
1300
1521
  {
1301
1522
  variant: "outlined",
1302
- selected: false,
1303
- className: "text-on-surface-variant"
1523
+ color: "secondary",
1524
+ className: [
1525
+ "[--ib-bg:transparent]",
1526
+ "[--ib-fg:var(--color-on-surface-variant)]",
1527
+ "[--ib-sl:var(--color-on-surface-variant)]",
1528
+ "[--ib-border:var(--color-outline)]",
1529
+ "[--ib-bg-off:transparent]",
1530
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1531
+ "[--ib-bg-on:var(--color-inverse-surface)]",
1532
+ "[--ib-fg-on:var(--color-inverse-on-surface)]",
1533
+ "data-[disabled]:border-on-surface/12"
1534
+ ]
1304
1535
  },
1305
- // ====================
1306
- // OUTLINED VARIANTS (SELECTED - uses inverse colors)
1307
- // ====================
1308
1536
  {
1309
1537
  variant: "outlined",
1310
- selected: true,
1311
- className: "bg-inverse-surface text-inverse-on-surface border-transparent"
1538
+ color: "tertiary",
1539
+ className: [
1540
+ "[--ib-bg:transparent]",
1541
+ "[--ib-fg:var(--color-on-surface-variant)]",
1542
+ "[--ib-sl:var(--color-on-surface-variant)]",
1543
+ "[--ib-border:var(--color-outline)]",
1544
+ "[--ib-bg-off:transparent]",
1545
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1546
+ "[--ib-bg-on:var(--color-inverse-surface)]",
1547
+ "[--ib-fg-on:var(--color-inverse-on-surface)]",
1548
+ "data-[disabled]:border-on-surface/12"
1549
+ ]
1550
+ },
1551
+ {
1552
+ variant: "outlined",
1553
+ color: "error",
1554
+ className: [
1555
+ "[--ib-bg:transparent]",
1556
+ "[--ib-fg:var(--color-on-surface-variant)]",
1557
+ "[--ib-sl:var(--color-on-surface-variant)]",
1558
+ "[--ib-border:var(--color-outline)]",
1559
+ "[--ib-bg-off:transparent]",
1560
+ "[--ib-fg-off:var(--color-on-surface-variant)]",
1561
+ "[--ib-bg-on:var(--color-inverse-surface)]",
1562
+ "[--ib-fg-on:var(--color-inverse-on-surface)]",
1563
+ "data-[disabled]:border-on-surface/12"
1564
+ ]
1312
1565
  }
1313
1566
  ],
1314
- /**
1315
- * Default variants
1316
- */
1317
1567
  defaultVariants: {
1318
1568
  variant: "standard",
1319
1569
  color: "primary",
1320
1570
  size: "medium",
1321
- selected: false,
1322
- isDisabled: false
1571
+ width: "default",
1572
+ shape: "round"
1573
+ }
1574
+ }
1575
+ );
1576
+ var iconButtonStateLayerVariants = classVarianceAuthority.cva([
1577
+ "absolute inset-0 rounded-[inherit] pointer-events-none opacity-0",
1578
+ "bg-[var(--ib-sl,currentColor)]",
1579
+ // Effects transition (opacity — no spatial overshoot)
1580
+ "transition-opacity duration-spring-standard-fast-effects ease-spring-standard-fast-effects",
1581
+ // Interaction opacities (MD3: hover 8%, focus/pressed 10%)
1582
+ "group-data-[hovered]/icon-button:opacity-8",
1583
+ "group-data-[focus-visible]/icon-button:opacity-10",
1584
+ "group-data-[pressed]/icon-button:opacity-10",
1585
+ // No state layer when disabled
1586
+ "group-data-[disabled]/icon-button:hidden"
1587
+ ]);
1588
+ var iconButtonIconVariants = classVarianceAuthority.cva(
1589
+ [
1590
+ "relative z-10 inline-flex shrink-0 items-center justify-center",
1591
+ "transition-colors duration-spring-standard-fast-effects ease-spring-standard-fast-effects"
1592
+ ],
1593
+ {
1594
+ variants: {
1595
+ size: {
1596
+ xsmall: "size-5",
1597
+ // 20dp
1598
+ small: "size-6",
1599
+ // 24dp
1600
+ medium: "size-6",
1601
+ // 24dp
1602
+ large: "size-8",
1603
+ // 32dp
1604
+ xlarge: "size-10"
1605
+ // 40dp
1606
+ }
1607
+ },
1608
+ defaultVariants: {
1609
+ size: "medium"
1323
1610
  }
1324
1611
  }
1325
1612
  );
1326
1613
  var IconButton = React.forwardRef(
1327
1614
  ({
1328
- // Variant props (CVA)
1615
+ // Variant props (CVA / design-time)
1329
1616
  variant = "standard",
1330
1617
  color = "primary",
1331
1618
  size = "medium",
1619
+ width = "default",
1620
+ shape = "round",
1332
1621
  // IconButton specific props
1333
1622
  children,
1623
+ selectedIcon,
1334
1624
  value,
1335
1625
  selected,
1336
1626
  disableRipple = false,
1337
1627
  className,
1338
1628
  // React Aria props
1339
- isDisabled: propIsDisabled = false,
1629
+ isDisabled = false,
1340
1630
  onPress,
1341
1631
  onMouseDown,
1342
1632
  "aria-label": ariaLabel,
@@ -1355,7 +1645,8 @@ var IconButton = React.forwardRef(
1355
1645
  console.warn("[IconButton] IconButton should have an icon as children.");
1356
1646
  }
1357
1647
  }
1358
- const isDisabled = propIsDisabled;
1648
+ const isToggle = selected !== void 0;
1649
+ const isSelected = isToggle ? selected ?? false : false;
1359
1650
  const { onMouseDown: handleRipple, ripples } = useRipple({
1360
1651
  disabled: isDisabled || disableRipple
1361
1652
  });
@@ -1368,44 +1659,42 @@ var IconButton = React.forwardRef(
1368
1659
  onMouseDown: mergedOnMouseDown,
1369
1660
  isDisabled
1370
1661
  });
1662
+ const isGroupSelected = isConnected && groupCtx && value ? groupCtx.selectedValues.has(value) : false;
1371
1663
  const connectedClasses = isConnected && groupCtx ? [
1372
1664
  ...getConnectedRadiusClasses(groupCtx, value),
1373
1665
  groupCtx.enforceMinWidth ? "min-w-12" : ""
1374
1666
  ] : [];
1667
+ const iconNode = isToggle && isSelected && selectedIcon ? selectedIcon : children;
1375
1668
  return /* @__PURE__ */ jsxRuntime.jsxs(
1376
1669
  IconButtonHeadless,
1377
1670
  {
1378
1671
  ref,
1379
1672
  className: cn(
1380
- // Base classes
1381
- "relative inline-flex items-center justify-center",
1382
- "overflow-hidden rounded-full",
1383
- // Circular shape (overridden by connected group classes)
1384
- // Spatial (border-radius, transform): expressive fast spring — 350ms, visible overshoot
1385
- "duration-expressive-fast-spatial ease-expressive-fast-spatial transition-all",
1386
- "focus-visible:outline-primary focus-visible:outline-2 focus-visible:outline-offset-2",
1387
- // State layers (hover, focus, active) — effects token: opacity, no overshoot
1388
- "before:absolute before:inset-0 before:rounded-[inherit]",
1389
- "before:duration-spring-standard-fast-effects before:ease-spring-standard-fast-effects before:transition-opacity",
1390
- "before:bg-current before:opacity-0",
1391
- "hover:before:opacity-8",
1392
- "focus-visible:before:opacity-12",
1393
- "active:before:opacity-12",
1394
- // CVA variants
1395
- iconButtonVariants({ variant, color, size, selected: selected ?? false, isDisabled }),
1673
+ // Root CVA — sets CSS role variables, dimensions, shape, transitions
1674
+ iconButtonRootVariants({ variant, color, size, width, shape }),
1675
+ // Group scope for child slot selectors
1676
+ "group/icon-button",
1677
+ // ButtonGroup asymmetric border-radius easing (connected selection morph)
1678
+ isGroupSelected ? "btn-transition-selected" : "",
1396
1679
  ...connectedClasses,
1397
- // User custom classes
1680
+ // Consumer custom classes
1398
1681
  className
1399
1682
  ),
1400
1683
  "aria-label": ariaLabel,
1684
+ isSelected,
1685
+ isToggle,
1401
1686
  "data-variant": variant,
1402
1687
  "data-color": color,
1403
- ...selected !== void 0 && { selected },
1688
+ "data-size": size,
1689
+ "data-width": width,
1690
+ "data-shape": shape,
1691
+ "data-group-selected": isGroupSelected ? "" : void 0,
1404
1692
  ...title && { title },
1405
1693
  ...mergedPropsValue,
1406
1694
  children: [
1695
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: iconButtonStateLayerVariants(), "aria-hidden": "true", "data-state-layer": "" }),
1407
1696
  ripples,
1408
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relative z-10 inline-flex shrink-0", children })
1697
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: iconButtonIconVariants({ size }), "data-icon-slot": "", "aria-hidden": "true", children: iconNode })
1409
1698
  ]
1410
1699
  }
1411
1700
  );
@@ -4791,20 +5080,6 @@ var BadgeContent = React.forwardRef(
4791
5080
  }
4792
5081
  );
4793
5082
  BadgeContent.displayName = "BadgeContent";
4794
- var QUERY = "(prefers-reduced-motion: reduce)";
4795
- function useReducedMotion() {
4796
- const [reduced, setReduced] = React.useState(() => {
4797
- if (typeof window === "undefined") return false;
4798
- return window.matchMedia(QUERY).matches;
4799
- });
4800
- React.useEffect(() => {
4801
- const mql = window.matchMedia(QUERY);
4802
- const handler = (e) => setReduced(e.matches);
4803
- mql.addEventListener("change", handler);
4804
- return () => mql.removeEventListener("change", handler);
4805
- }, []);
4806
- return reduced;
4807
- }
4808
5083
  var Badge = React.forwardRef(
4809
5084
  ({
4810
5085
  count,
@@ -14924,6 +15199,8 @@ exports.bottomSheetHandlePillVariants = bottomSheetHandlePillVariants;
14924
15199
  exports.bottomSheetHandleWrapperVariants = bottomSheetHandleWrapperVariants;
14925
15200
  exports.bottomSheetScrimVariants = bottomSheetScrimVariants;
14926
15201
  exports.bottomSheetVariants = bottomSheetVariants;
15202
+ exports.buttonGroupFocusRingVariants = buttonGroupFocusRingVariants;
15203
+ exports.buttonGroupRootVariants = buttonGroupRootVariants;
14927
15204
  exports.buttonGroupVariants = buttonGroupVariants;
14928
15205
  exports.calendarCellVariants = calendarCellVariants;
14929
15206
  exports.cardVariants = cardVariants;