Back to buttons
"use client";

import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import * as React from "react";

export type MultiButtonVariant = "default" | "outline" | "secondary" | "ghost";
export type MultiButtonSize = "sm" | "md" | "lg";

export type MultiButtonItem = {
  id: string;
  icon: React.ElementType<{ className?: string }>;
  label: React.ReactNode;
  ariaLabel?: string;
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
  disabled?: boolean;
  className?: string;
  /** Alias kept for callers that want a per-action hover treatment. */
  hoverClassName?: string;
};

export type MultiButtonGroupProps = React.HTMLAttributes<HTMLDivElement> & {
  children: React.ReactNode;
};

type MultiButtonRootProps = Omit<
  React.HTMLAttributes<HTMLDivElement>,
  | "onAnimationStart"
  | "onAnimationEnd"
  | "onClick"
  | "onDrag"
  | "onDragStart"
  | "onDragEnd"
>;

type MultiButtonSharedProps = MultiButtonRootProps & {
  items: MultiButtonItem[];
  /** Optional items used to reserve the same expanded label width as another row. */
  syncWidthTo?: MultiButtonItem[];
  /** Optional CSS color or token reference used for a faint active-item tint. */
  highlightColor?: string;
  /** Use the more expressive SVG metaball treatment instead of the default pill morph. */
  gooey?: boolean;
  variant?: MultiButtonVariant;
  size?: MultiButtonSize;
};

export type MultiButtonProps = MultiButtonSharedProps;

export type CompactMultiButtonProps = MultiButtonSharedProps & {
  selectedId: string;
  /** Keep the compact control icon-only when it opens. */
  iconOnly?: boolean;
  /** Optional independent icon shown while the compact control is at rest. */
  restIcon?: MultiButtonItem["icon"];
  /** Accessible label for the optional rest trigger. Defaults to "Open actions". */
  restAriaLabel?: string;
};

type MultiButtonGroupContextValue = {
  register: (id: string, labelWidth: number) => void;
  unregister: (id: string) => void;
  sharedLabelWidth: number;
};

const MultiButtonGroupContext = React.createContext<
  MultiButtonGroupContextValue | undefined
>(undefined);

const SIZE_CONFIG: Record<
  MultiButtonSize,
  { cell: number; icon: string; minHeight: string; text: string }
> = {
  sm: { cell: 40, icon: "size-3.5", minHeight: "h-10", text: "text-xs" },
  md: { cell: 40, icon: "size-4", minHeight: "h-10", text: "text-sm" },
  lg: {
    cell: 40,
    icon: "size-[18px]",
    minHeight: "h-10",
    text: "text-sm",
  },
};

const VARIANT_CLASSES: Record<MultiButtonVariant, string> = {
  default:
    "bg-primary text-primary-foreground shadow-sm ring-1 ring-primary/20",
  outline:
    "bg-background text-foreground shadow-xs ring-1 ring-inset ring-border/80",
  secondary: "bg-secondary text-secondary-foreground shadow-xs",
  ghost: "bg-muted/50 text-foreground ring-1 ring-border/60",
};

const ITEM_HOVER_CLASSES: Record<MultiButtonVariant, string> = {
  default: "hover:bg-primary-foreground/10 active:bg-primary-foreground/20",
  outline: "hover:bg-accent hover:text-accent-foreground active:bg-accent/80",
  secondary:
    "hover:bg-secondary-foreground/10 active:bg-secondary-foreground/20",
  ghost: "hover:bg-accent hover:text-accent-foreground active:bg-accent/80",
};

const DIVIDER_CLASSES: Record<MultiButtonVariant, string> = {
  default: "bg-primary-foreground/20",
  outline: "bg-border/70",
  secondary: "bg-secondary-foreground/15",
  ghost: "bg-border/70",
};

const GOOEY_BLOB_FILLS: Record<MultiButtonVariant, string> = {
  default: "var(--primary)",
  outline: "var(--background)",
  secondary: "var(--secondary)",
  ghost: "color-mix(in oklab, var(--muted) 50%, transparent)",
};

const GOOEY_TEXT_CLASSES: Record<MultiButtonVariant, string> = {
  default: "text-primary-foreground",
  outline: "text-foreground",
  secondary: "text-secondary-foreground",
  ghost: "text-foreground",
};

const LABEL_SPRING = {
  type: "spring",
  stiffness: 520,
  damping: 32,
} as const;

const CONTEXTUAL_ICON_TRANSITION = {
  type: "spring",
  duration: 0.3,
  bounce: 0,
} as const;

const COMPACT_EXPAND_DURATION_MS = 200;
const GOOEY_BOUNCE_DURATION_MS = 300;
const GOOEY_BOUNCE_TRANSITION = {
  duration: GOOEY_BOUNCE_DURATION_MS / 1000,
  ease: [0.3, 0.7, 0.4, 1.5],
} as const;
const GOOEY_CLOSE_TRANSITION = {
  duration: COMPACT_EXPAND_DURATION_MS / 1000,
  ease: [0.3, 0.7, 0.4, 1],
} as const;

const GOOEY_RAIL_TRANSITION_CLASSES = {
  expanded: "[transition:width_300ms_cubic-bezier(0.3,0.7,0.4,1.5)]",
  collapsed: "[transition:width_200ms_cubic-bezier(0.3,0.7,0.4,1)]",
} as const;

const GOOEY_ITEM_TRANSITION_CLASSES = {
  expanded:
    "[transition:width_300ms_cubic-bezier(0.3,0.7,0.4,1.5),background-color_150ms_ease,color_150ms_ease,scale_120ms_ease]",
  collapsed:
    "[transition:width_200ms_cubic-bezier(0.3,0.7,0.4,1),background-color_150ms_ease,color_150ms_ease,scale_120ms_ease]",
} as const;

const GOOEY_ICON_TRANSITION_CLASSES = {
  expanded: "[transition:transform_300ms_cubic-bezier(0.3,0.7,0.4,1.5)]",
  collapsed: "[transition:transform_200ms_cubic-bezier(0.3,0.7,0.4,1)]",
} as const;

const GOOEY_DIVIDER_TRANSITION_CLASSES = {
  expanded:
    "[transition:width_300ms_cubic-bezier(0.3,0.7,0.4,1.5),opacity_150ms_ease]",
  collapsed:
    "[transition:width_200ms_cubic-bezier(0.3,0.7,0.4,1),opacity_150ms_ease]",
} as const;

function gooeyPhase(expanded: boolean) {
  return expanded ? "expanded" : "collapsed";
}

function gooeyMotionTransition(reduceMotion: boolean, expanded: boolean) {
  if (reduceMotion) return { duration: 0 } as const;
  return expanded ? GOOEY_BOUNCE_TRANSITION : GOOEY_CLOSE_TRANSITION;
}

function railClassName({
  className,
  compact,
  expanded,
  gooey,
  minHeight,
  variant,
}: {
  className?: string;
  compact: boolean;
  expanded: boolean;
  gooey: boolean;
  minHeight: string;
  variant: MultiButtonVariant;
}) {
  const phase = gooeyPhase(expanded);
  const surface = gooey
    ? `overflow-visible ${GOOEY_TEXT_CLASSES[variant]}`
    : `overflow-hidden ${VARIANT_CLASSES[variant]}`;
  const widthTransition = compact
    ? gooey
      ? GOOEY_RAIL_TRANSITION_CLASSES[phase]
      : "[transition:width_200ms_ease-out]"
    : "";

  return `relative inline-flex items-stretch rounded-full ${minHeight} ${surface} ${widthTransition} motion-reduce:[transition:none] ${className ?? ""}`;
}

function useMultiButtonInteractions() {
  const reduceMotion = Boolean(useReducedMotion());
  const filterId = React.useId().replace(/:/g, "");
  const [hoveredId, setHoveredId] = React.useState<string | null>(null);
  const [touchExpandedId, setTouchExpandedId] = React.useState<string | null>(
    null,
  );
  const containerRef = React.useRef<HTMLDivElement>(null);

  return {
    containerRef,
    filterId,
    hoveredId,
    reduceMotion,
    setHoveredId,
    setTouchExpandedId,
    touchExpandedId,
  };
}

function useOutsidePointerDown(
  active: boolean,
  containerRef: React.RefObject<HTMLDivElement | null>,
  onOutside: () => void,
) {
  React.useEffect(() => {
    if (!active) return;
    // The rail may be portaled into a preview iframe, whose document is
    // different from the page document running this component.
    const ownerDocument = containerRef.current?.ownerDocument ?? document;
    const handlePointerDown = (event: PointerEvent) => {
      if (!containerRef.current?.contains(event.target as Node)) onOutside();
    };
    ownerDocument.addEventListener("pointerdown", handlePointerDown);
    return () =>
      ownerDocument.removeEventListener("pointerdown", handlePointerDown);
  }, [active, containerRef, onOutside]);
}

function labelText(label: React.ReactNode) {
  return typeof label === "string" || typeof label === "number"
    ? label.toString()
    : "";
}

function estimateLabelWidth(label: React.ReactNode, size: MultiButtonSize) {
  const textWidth = labelText(label).length * (size === "sm" ? 6.5 : 7.5);
  return Math.ceil(textWidth) + (size === "lg" ? 10 : size === "md" ? 8 : 6);
}

function maxLabelWidth(items: MultiButtonItem[], size: MultiButtonSize) {
  return items.reduce(
    (max, item) => Math.max(max, estimateLabelWidth(item.label, size)),
    0,
  );
}

function estimatedLabelWidths(items: MultiButtonItem[], size: MultiButtonSize) {
  return Object.fromEntries(
    items.map((item) => [item.id, estimateLabelWidth(item.label, size)]),
  );
}

function equalLabelWidths(
  current: Record<string, number>,
  next: Record<string, number>,
) {
  const currentIds = Object.keys(current);
  const nextIds = Object.keys(next);
  return (
    currentIds.length === nextIds.length &&
    nextIds.every((id) => current[id] === next[id])
  );
}

function measuredLabelWidth(
  element: HTMLElement | undefined,
  fallback: number,
) {
  if (!element) return fallback;
  const style = getComputedStyle(element);
  const marginLeft = Number.parseFloat(style.marginLeft) || 0;
  const marginRight = Number.parseFloat(style.marginRight) || 0;
  const measured = Math.ceil(
    element.getBoundingClientRect().width + marginLeft + marginRight,
  );
  return measured > 0 ? measured : fallback;
}

function itemAriaLabel(item: MultiButtonItem) {
  return item.ariaLabel ?? (labelText(item.label) || item.id);
}

function useLabelWidths(
  items: MultiButtonItem[],
  syncWidthTo: MultiButtonItem[] | undefined,
  size: MultiButtonSize,
) {
  const group = React.useContext(MultiButtonGroupContext);
  const instanceId = React.useId();
  const reserveItems = syncWidthTo ?? items;
  const itemEstimates = React.useMemo(
    () => estimatedLabelWidths(items, size),
    [items, size],
  );
  const estimatedReserveWidth = React.useMemo(
    () => maxLabelWidth(reserveItems, size),
    [reserveItems, size],
  );
  const [labelWidths, setLabelWidths] =
    React.useState<Record<string, number>>(itemEstimates);
  const [ownReservedWidth, setOwnReservedWidth] = React.useState(
    estimatedReserveWidth,
  );
  const measurementRef = React.useRef<HTMLDivElement>(null);
  const register = group?.register;
  const unregister = group?.unregister;

  const measureLabels = React.useCallback(() => {
    const itemLabels = measurementRef.current?.querySelectorAll<HTMLElement>(
      '[data-slot="multi-button-item-label-measure"]',
    );
    const nextLabelWidths = Object.fromEntries(
      items.map((item, index) => [
        item.id,
        measuredLabelWidth(itemLabels?.[index], itemEstimates[item.id] ?? 0),
      ]),
    );
    setLabelWidths((current) =>
      equalLabelWidths(current, nextLabelWidths) ? current : nextLabelWidths,
    );

    const reserveLabels = measurementRef.current?.querySelectorAll<HTMLElement>(
      '[data-slot="multi-button-reserve-label-measure"]',
    );
    const measuredReserveWidth = reserveItems.reduce(
      (largest, item, index) =>
        Math.max(
          largest,
          measuredLabelWidth(
            reserveLabels?.[index],
            estimateLabelWidth(item.label, size),
          ),
        ),
      0,
    );
    setOwnReservedWidth((current) =>
      current === measuredReserveWidth ? current : measuredReserveWidth,
    );
  }, [itemEstimates, items, reserveItems, size]);

  React.useLayoutEffect(() => {
    const measurementNode = measurementRef.current;
    if (!measurementNode) return;

    let active = true;
    const measure = () => {
      if (active) measureLabels();
    };
    measure();

    const observer =
      typeof ResizeObserver === "undefined"
        ? undefined
        : new ResizeObserver(measure);
    const labels = measurementNode.querySelectorAll<HTMLElement>(
      '[data-slot$="-label-measure"]',
    );
    labels.forEach((label) => {
      observer?.observe(label);
    });

    void document.fonts?.ready.then(measure);

    return () => {
      active = false;
      observer?.disconnect();
    };
  }, [measureLabels]);

  React.useLayoutEffect(() => {
    if (!register || !unregister) return;
    register(instanceId, ownReservedWidth);
    return () => unregister(instanceId);
  }, [instanceId, ownReservedWidth, register, unregister]);

  return {
    labelWidths,
    reservedLabelWidth: group
      ? Math.max(group.sharedLabelWidth, ownReservedWidth)
      : ownReservedWidth,
    measurementRef,
    reserveItems,
  };
}

function MultiButtonLabelMeasurement({
  items,
  reserveItems,
  measurementRef,
  size,
}: {
  items: MultiButtonItem[];
  reserveItems: MultiButtonItem[];
  measurementRef: React.RefObject<HTMLDivElement | null>;
  size: MultiButtonSize;
}) {
  return (
    <div
      ref={measurementRef}
      aria-hidden="true"
      className="pointer-events-none invisible absolute top-0 left-0 flex h-0 w-0 overflow-hidden"
    >
      {items.map((item) => (
        <span
          key={`item-${item.id}`}
          data-slot="multi-button-item-label-measure"
          className={`-ml-1 shrink-0 whitespace-nowrap pr-2 font-medium leading-none ${SIZE_CONFIG[size].text}`}
        >
          {item.label}
        </span>
      ))}
      {reserveItems.map((item) => (
        <span
          key={`reserve-${item.id}`}
          data-slot="multi-button-reserve-label-measure"
          className={`-ml-1 shrink-0 whitespace-nowrap pr-2 font-medium leading-none ${SIZE_CONFIG[size].text}`}
        >
          {item.label}
        </span>
      ))}
    </div>
  );
}

function actionWidth(
  item: MultiButtonItem,
  activeId: string | null,
  itemCount: number,
  cellWidth: number,
  labelWidths: Record<string, number>,
  reservedLabelWidth: number,
) {
  if (activeId === null) {
    return itemCount > 0
      ? cellWidth + reservedLabelWidth / itemCount
      : cellWidth;
  }

  const activeLabelWidth = labelWidths[activeId] ?? reservedLabelWidth;
  if (item.id === activeId) return cellWidth + activeLabelWidth;

  const remainingLabelSpace = Math.max(
    0,
    reservedLabelWidth - activeLabelWidth,
  );
  return itemCount > 1
    ? cellWidth + remainingLabelSpace / (itemCount - 1)
    : cellWidth;
}

type MultiButtonBlobGeometry = {
  item: MultiButtonItem;
  width: number;
  x: number;
};

function useMultiButtonLayout({
  activeId,
  items,
  reserveLabelSpace = true,
  size,
  syncWidthTo,
}: {
  activeId: string | null;
  items: MultiButtonItem[];
  reserveLabelSpace?: boolean;
  size: MultiButtonSize;
  syncWidthTo?: MultiButtonItem[];
}) {
  const cfg = SIZE_CONFIG[size];
  const { labelWidths, reservedLabelWidth, measurementRef, reserveItems } =
    useLabelWidths(items, syncWidthTo, size);
  const effectiveLabelWidth = reserveLabelSpace ? reservedLabelWidth : 0;
  const dividerWidth = Math.max(0, items.length - 1);
  const expandedWidth =
    items.length > 0
      ? items.length * cfg.cell + effectiveLabelWidth + dividerWidth
      : cfg.cell;
  const itemWidths = items.map((item) =>
    actionWidth(
      item,
      activeId,
      items.length,
      cfg.cell,
      labelWidths,
      effectiveLabelWidth,
    ),
  );
  let blobX = 0;
  const blobGeometries = items.map((item, index) => {
    const geometry = {
      item,
      width: itemWidths[index] ?? cfg.cell,
      x: blobX,
    };
    blobX += geometry.width + (index < items.length - 1 ? 1 : 0);
    return geometry;
  });

  return {
    blobGeometries,
    cfg,
    expandedWidth,
    itemWidths,
    measurementRef,
    reserveItems,
  };
}

function MultiButtonBlobLayer({
  activeId,
  cellWidth,
  expanded,
  filterId,
  geometries,
  highlightColor,
  reduceMotion,
  selectedId,
  variant,
}: {
  activeId: string | null;
  cellWidth: number;
  expanded: boolean;
  filterId: string;
  geometries: MultiButtonBlobGeometry[];
  highlightColor?: string;
  reduceMotion: boolean;
  selectedId: string | undefined;
  variant: MultiButtonVariant;
}) {
  const baseFill = GOOEY_BLOB_FILLS[variant];

  return (
    <svg
      data-slot="multi-button-blob"
      aria-hidden="true"
      className="pointer-events-none absolute top-0 left-0 z-base drop-shadow-sm"
      style={{ overflow: "visible" }}
      width="100%"
      height={cellWidth}
    >
      <defs>
        <filter
          id={filterId}
          x="-100%"
          y="-400%"
          width="300%"
          height="900%"
          colorInterpolationFilters="sRGB"
        >
          <feGaussianBlur in="SourceGraphic" stdDeviation="6" result="blur" />
          <feColorMatrix
            in="blur"
            mode="matrix"
            values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 20 -10"
          />
        </filter>
      </defs>
      <g filter={reduceMotion ? undefined : `url(#${filterId})`}>
        {geometries.map(({ item, width, x }) => {
          const selected = item.id === selectedId;
          const active = item.id === activeId;
          const fill =
            active && highlightColor
              ? `color-mix(in oklch, ${baseFill} 86%, ${highlightColor})`
              : baseFill;

          return (
            <motion.rect
              key={item.id}
              x={0}
              y={0}
              height={cellWidth}
              rx={cellWidth / 2}
              fill={fill}
              style={{
                transformBox: "fill-box",
                transformOrigin: "center",
              }}
              initial={false}
              animate={
                expanded
                  ? { x, width, scale: 1, opacity: 1 }
                  : selected
                    ? { x: 0, width: cellWidth, scale: 1, opacity: 1 }
                    : {
                        x: 0,
                        width: cellWidth,
                        scale: 0.2,
                        opacity: 0,
                      }
              }
              transition={gooeyMotionTransition(reduceMotion, expanded)}
            />
          );
        })}
      </g>
    </svg>
  );
}

function labelMotion(reduceMotion: boolean) {
  return {
    initial: reduceMotion
      ? false
      : { opacity: 0, scale: 0.25, filter: "blur(4px)" },
    animate: { opacity: 1, scale: 1, filter: "blur(0px)" },
    exit: reduceMotion
      ? undefined
      : { opacity: 0, scale: 0.25, filter: "blur(4px)" },
    transition: reduceMotion
      ? { duration: 0 }
      : {
          opacity: LABEL_SPRING,
          scale: LABEL_SPRING,
          filter: { duration: 0.15, ease: "easeOut" },
        },
  } as const;
}

type MultiButtonLabelProps = {
  item: MultiButtonItem;
  reduceMotion: boolean;
  textClass: string;
};

const MultiButtonLabel = React.forwardRef<
  HTMLSpanElement,
  MultiButtonLabelProps
>(({ item, reduceMotion, textClass }, ref) => (
  <motion.span
    ref={ref}
    {...labelMotion(reduceMotion)}
    className={`relative z-raised -ml-1 shrink-0 whitespace-nowrap pr-2 font-medium leading-none ${textClass}`}
  >
    {item.label}
  </motion.span>
));
MultiButtonLabel.displayName = "MultiButtonLabel";

type MultiButtonItemButtonProps = {
  item: MultiButtonItem;
  active: boolean;
  accessible?: boolean;
  ariaLabel?: string;
  disclosureExpanded?: boolean;
  disabled: boolean;
  highlighted: boolean;
  gooey?: boolean;
  gooeyExpanded?: boolean;
  transparent?: boolean;
  size: MultiButtonSize;
  variant: MultiButtonVariant;
  width: number;
  iconOffset: number;
  reduceMotion: boolean;
  restIcon?: MultiButtonItem["icon"];
  showRestIcon?: boolean;
  visible?: boolean;
  onTouchAction: (
    event: React.PointerEvent<HTMLButtonElement>,
    id: string,
  ) => void;
  onHover: (id: string | null) => void;
  onAction?: (event: React.MouseEvent<HTMLButtonElement>) => void;
};

type MultiButtonItemIconProps = {
  actionIcon: MultiButtonItem["icon"];
  gooey: boolean;
  gooeyExpanded: boolean;
  iconClassName: string;
  iconOffset: number;
  laneWidth: number;
  reduceMotion: boolean;
  restIcon?: MultiButtonItem["icon"];
  showRestIcon: boolean;
};

function MultiButtonItemIcon({
  actionIcon: ActionIcon,
  gooey,
  gooeyExpanded,
  iconClassName,
  iconOffset,
  laneWidth,
  reduceMotion,
  restIcon: RestIcon,
  showRestIcon,
}: MultiButtonItemIconProps) {
  const phase = gooeyPhase(gooeyExpanded);
  const iconTransition = reduceMotion
    ? ({ duration: 0 } as const)
    : CONTEXTUAL_ICON_TRANSITION;
  const visibleState = { opacity: 1, scale: 1, filter: "blur(0px)" };
  const hiddenState = { opacity: 0, scale: 0.25, filter: "blur(4px)" };

  return (
    <span
      data-slot="multi-button-icon"
      className={`relative z-raised flex h-full shrink-0 items-center justify-center motion-reduce:[transition:none] ${gooey ? GOOEY_ICON_TRANSITION_CLASSES[phase] : "[transition:transform_200ms_ease-out]"}`}
      style={{
        width: `${laneWidth}px`,
        transform: `translateX(${iconOffset}px)`,
      }}
      aria-hidden="true"
    >
      {RestIcon ? (
        <>
          <motion.span
            data-slot="multi-button-action-icon"
            className="absolute inset-0 flex items-center justify-center"
            initial={false}
            animate={showRestIcon ? hiddenState : visibleState}
            transition={iconTransition}
          >
            <ActionIcon className={iconClassName} />
          </motion.span>
          <motion.span
            data-slot="multi-button-rest-icon"
            className="absolute inset-0 flex items-center justify-center"
            initial={false}
            animate={showRestIcon ? visibleState : hiddenState}
            transition={iconTransition}
          >
            <RestIcon className={iconClassName} />
          </motion.span>
        </>
      ) : (
        <ActionIcon className={iconClassName} />
      )}
    </span>
  );
}

function MultiButtonItemButton({
  item,
  active,
  accessible = true,
  ariaLabel,
  disclosureExpanded,
  disabled,
  highlighted,
  gooey = false,
  gooeyExpanded = true,
  transparent = false,
  size,
  variant,
  width,
  iconOffset,
  reduceMotion,
  restIcon,
  showRestIcon = false,
  visible = true,
  onTouchAction,
  onHover,
  onAction,
}: MultiButtonItemButtonProps) {
  const cfg = SIZE_CONFIG[size];
  const phase = gooeyPhase(gooeyExpanded);

  return (
    <motion.button
      type="button"
      data-state={active ? "open" : "closed"}
      data-multi-button-item-id={item.id}
      aria-label={ariaLabel ?? itemAriaLabel(item)}
      aria-expanded={disclosureExpanded}
      aria-hidden={accessible ? undefined : true}
      tabIndex={accessible ? undefined : -1}
      disabled={disabled}
      onClick={(event) => {
        if (item.disabled) return;
        item.onClick?.(event);
        onAction?.(event);
      }}
      onPointerDown={(event) => onTouchAction(event, item.id)}
      onMouseEnter={() => onHover(item.id)}
      onMouseLeave={() => onHover(null)}
      onFocus={() => onHover(item.id)}
      onBlur={() => onHover(null)}
      initial={false}
      animate={
        gooey
          ? {
              opacity: visible ? 1 : 0,
              scale: visible ? 1 : 0.4,
            }
          : undefined
      }
      transition={
        gooey ? gooeyMotionTransition(reduceMotion, gooeyExpanded) : undefined
      }
      style={{ width: `${width}px` }}
      className={`relative isolate flex shrink-0 cursor-pointer items-center justify-start overflow-hidden ${cfg.minHeight} focus:outline-none focus-visible:z-raised focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset disabled:cursor-not-allowed disabled:opacity-40 motion-reduce:[transition:none] ${accessible ? "" : "pointer-events-none"} ${gooey ? GOOEY_ITEM_TRANSITION_CLASSES[phase] : "[transition:width_200ms_ease-out,background-color_150ms_ease,color_150ms_ease,scale_120ms_ease]"} ${reduceMotion ? "" : "active:scale-[0.96]"} ${transparent ? "" : ITEM_HOVER_CLASSES[variant]} ${item.hoverClassName ?? ""} ${item.className ?? ""}`}
    >
      <span
        data-slot="multi-button-highlight"
        aria-hidden="true"
        className={`pointer-events-none absolute inset-0 z-base bg-[var(--multi-button-highlight)] [transition:opacity_150ms_ease] motion-reduce:[transition:none] ${highlighted ? "opacity-[0.14]" : "opacity-0"}`}
      />
      <MultiButtonItemIcon
        actionIcon={item.icon}
        gooey={gooey}
        gooeyExpanded={gooeyExpanded}
        iconClassName={cfg.icon}
        iconOffset={iconOffset}
        laneWidth={cfg.cell}
        reduceMotion={reduceMotion}
        restIcon={restIcon}
        showRestIcon={showRestIcon}
      />
      <AnimatePresence initial={false} mode="popLayout">
        {active && (
          <MultiButtonLabel
            key={item.id}
            item={item}
            reduceMotion={reduceMotion}
            textClass={cfg.text}
          />
        )}
      </AnimatePresence>
    </motion.button>
  );
}

function MultiButtonDivider({
  activeId,
  compact,
  expanded,
  gooey,
  variant,
}: {
  activeId: string | null;
  compact: boolean;
  expanded: boolean;
  gooey: boolean;
  variant: MultiButtonVariant;
}) {
  if (!compact) {
    return (
      <span
        aria-hidden="true"
        className={`my-2 w-px shrink-0 transition-opacity duration-150 motion-reduce:[transition:none] ${DIVIDER_CLASSES[variant]} ${activeId ? "opacity-0" : "opacity-100"}`}
      />
    );
  }

  const phase = gooeyPhase(expanded);

  return (
    <span
      aria-hidden="true"
      style={{
        width: `${expanded ? 1 : 0}px`,
        opacity: expanded && activeId === null ? 1 : 0,
      }}
      className={`my-2 shrink-0 motion-reduce:[transition:none] ${gooey ? GOOEY_DIVIDER_TRANSITION_CLASSES[phase] : "[transition:width_200ms_ease-out,opacity_200ms_ease-out]"} ${DIVIDER_CLASSES[variant]}`}
    />
  );
}

type MultiButtonItemsProps = {
  activeId: string | null;
  compact: boolean;
  expanded: boolean;
  gooey: boolean;
  highlightColor?: string;
  itemWidths: number[];
  interactionReady: boolean;
  items: MultiButtonItem[];
  onAction?: (event: React.MouseEvent<HTMLButtonElement>) => void;
  onHover: (id: string | null) => void;
  onTouchAction: (
    event: React.PointerEvent<HTMLButtonElement>,
    id: string,
  ) => void;
  reduceMotion: boolean;
  restAriaLabel?: string;
  restIcon?: MultiButtonItem["icon"];
  selectedId?: string;
  size: MultiButtonSize;
  variant: MultiButtonVariant;
};

function MultiButtonItems({
  activeId,
  compact,
  expanded,
  gooey,
  highlightColor,
  itemWidths,
  interactionReady,
  items,
  onAction,
  onHover,
  onTouchAction,
  reduceMotion,
  restAriaLabel,
  restIcon,
  selectedId,
  size,
  variant,
}: MultiButtonItemsProps) {
  const cfg = SIZE_CONFIG[size];

  return items.map((item, index) => {
    const active = activeId === item.id;
    const selected = selectedId === item.id;
    const collapsedSelectedTrigger = compact && selected && !expanded;
    const width =
      compact && !expanded
        ? selected
          ? cfg.cell
          : 0
        : (itemWidths[index] ?? cfg.cell);
    const iconOffset =
      compact && !expanded
        ? 0
        : active
          ? 0
          : Math.max(0, (width - cfg.cell) / 2);

    return (
      <React.Fragment key={item.id}>
        {index > 0 && (
          <MultiButtonDivider
            activeId={activeId}
            compact={compact}
            expanded={expanded}
            gooey={gooey}
            variant={variant}
          />
        )}
        <MultiButtonItemButton
          item={item}
          active={active}
          accessible={!compact || interactionReady || selected}
          ariaLabel={
            collapsedSelectedTrigger
              ? (restAriaLabel ?? (restIcon ? "Open actions" : undefined))
              : undefined
          }
          disclosureExpanded={compact && selected ? expanded : undefined}
          disabled={Boolean(item.disabled && !collapsedSelectedTrigger)}
          highlighted={!gooey && Boolean(highlightColor) && active}
          gooey={gooey}
          gooeyExpanded={expanded}
          transparent={gooey}
          size={size}
          variant={variant}
          width={width}
          iconOffset={iconOffset}
          reduceMotion={reduceMotion}
          restIcon={compact && selected ? restIcon : undefined}
          showRestIcon={compact && selected && Boolean(restIcon) && !expanded}
          visible={!compact || expanded || selected}
          onTouchAction={onTouchAction}
          onHover={onHover}
          onAction={onAction}
        />
      </React.Fragment>
    );
  });
}

type MultiButtonRailContentProps = MultiButtonItemsProps & {
  blobGeometries: MultiButtonBlobGeometry[];
  cellWidth: number;
  expandedWidth: number;
  filterId: string;
  measurementRef: React.RefObject<HTMLDivElement | null>;
  reserveItems: MultiButtonItem[];
};

function MultiButtonRailContent({
  activeId,
  blobGeometries,
  cellWidth,
  compact,
  expanded,
  filterId,
  gooey,
  highlightColor,
  itemWidths,
  interactionReady,
  items,
  measurementRef,
  onAction,
  onHover,
  onTouchAction,
  reduceMotion,
  restAriaLabel,
  restIcon,
  reserveItems,
  selectedId,
  size,
  variant,
}: MultiButtonRailContentProps) {
  return (
    <>
      {gooey && (
        <MultiButtonBlobLayer
          activeId={activeId}
          cellWidth={cellWidth}
          expanded={expanded}
          filterId={filterId}
          geometries={blobGeometries}
          highlightColor={highlightColor}
          reduceMotion={reduceMotion}
          selectedId={selectedId}
          variant={variant}
        />
      )}
      <MultiButtonLabelMeasurement
        items={items}
        reserveItems={reserveItems}
        measurementRef={measurementRef}
        size={size}
      />
      <MultiButtonItems
        activeId={activeId}
        compact={compact}
        expanded={expanded}
        gooey={gooey}
        highlightColor={highlightColor}
        itemWidths={itemWidths}
        interactionReady={interactionReady}
        items={items}
        onAction={onAction}
        onHover={onHover}
        onTouchAction={onTouchAction}
        reduceMotion={reduceMotion}
        restAriaLabel={restAriaLabel}
        restIcon={restIcon}
        selectedId={selectedId}
        size={size}
        variant={variant}
      />
    </>
  );
}

type MultiButtonRailProps = MultiButtonRailContentProps & {
  className?: string;
  containerRef: React.RefObject<HTMLDivElement | null>;
  containerWidth: number;
  forwardedRef: React.ForwardedRef<HTMLDivElement>;
  rootProps: MultiButtonRootProps;
  slot: "multi-button" | "compact-multi-button";
  style?: React.CSSProperties;
};

function MultiButtonRail({
  className,
  compact,
  containerRef,
  containerWidth,
  expanded,
  forwardedRef,
  gooey,
  highlightColor,
  rootProps,
  size,
  slot,
  style,
  variant,
  ...contentProps
}: MultiButtonRailProps) {
  const cfg = SIZE_CONFIG[size];

  return (
    <motion.div
      ref={(node) => {
        containerRef.current = node;
        if (typeof forwardedRef === "function") forwardedRef(node);
        else if (forwardedRef) forwardedRef.current = node;
      }}
      data-slot={slot}
      role="group"
      aria-expanded={compact ? expanded : undefined}
      style={
        {
          ...style,
          width: `${containerWidth}px`,
          "--multi-button-highlight": highlightColor,
        } as React.CSSProperties
      }
      className={railClassName({
        className,
        compact,
        expanded,
        gooey,
        minHeight: cfg.minHeight,
        variant,
      })}
      {...rootProps}
    >
      <MultiButtonRailContent
        {...contentProps}
        compact={compact}
        expanded={expanded}
        gooey={gooey}
        highlightColor={highlightColor}
        size={size}
        variant={variant}
      />
    </motion.div>
  );
}

const MultiButtonGroup = React.forwardRef<
  HTMLDivElement,
  MultiButtonGroupProps
>(({ children, className, ...props }, ref) => {
  const [registry, setRegistry] = React.useState<Record<string, number>>({});

  const register = React.useCallback((id: string, width: number) => {
    setRegistry((current) =>
      current[id] === width ? current : { ...current, [id]: width },
    );
  }, []);

  const unregister = React.useCallback((id: string) => {
    setRegistry((current) => {
      if (!(id in current)) return current;
      const next = { ...current };
      delete next[id];
      return next;
    });
  }, []);

  const sharedLabelWidth = Math.max(0, ...Object.values(registry));
  const context = React.useMemo(
    () => ({ register, unregister, sharedLabelWidth }),
    [register, unregister, sharedLabelWidth],
  );

  return (
    <MultiButtonGroupContext.Provider value={context}>
      <div
        ref={ref}
        data-slot="multi-button-group"
        className={`contents ${className ?? ""}`}
        {...props}
      >
        {children}
      </div>
    </MultiButtonGroupContext.Provider>
  );
});
MultiButtonGroup.displayName = "MultiButtonGroup";

const MultiButton = React.forwardRef<HTMLDivElement, MultiButtonProps>(
  (
    {
      items,
      syncWidthTo,
      highlightColor,
      gooey = false,
      variant = "default",
      size = "md",
      className,
      style,
      ...props
    },
    ref,
  ) => {
    const {
      containerRef,
      filterId,
      hoveredId,
      reduceMotion,
      setHoveredId,
      setTouchExpandedId,
      touchExpandedId,
    } = useMultiButtonInteractions();
    const activeId = hoveredId ?? touchExpandedId;
    const {
      blobGeometries,
      cfg,
      expandedWidth,
      itemWidths,
      measurementRef,
      reserveItems,
    } = useMultiButtonLayout({ activeId, items, size, syncWidthTo });

    const collapseTouchAction = React.useCallback(
      () => setTouchExpandedId(null),
      [setTouchExpandedId],
    );
    useOutsidePointerDown(
      Boolean(touchExpandedId),
      containerRef,
      collapseTouchAction,
    );

    const onTouchAction = (
      event: React.PointerEvent<HTMLButtonElement>,
      id: string,
    ) => {
      if (event.pointerType !== "touch") return;
      if (touchExpandedId !== id) {
        setTouchExpandedId(id);
      }
    };

    return (
      <MultiButtonRail
        activeId={activeId}
        blobGeometries={blobGeometries}
        cellWidth={cfg.cell}
        className={className}
        compact={false}
        containerRef={containerRef}
        containerWidth={expandedWidth}
        expanded
        expandedWidth={expandedWidth}
        filterId={filterId}
        forwardedRef={ref}
        gooey={gooey}
        highlightColor={highlightColor}
        itemWidths={itemWidths}
        interactionReady
        items={items}
        measurementRef={measurementRef}
        onHover={setHoveredId}
        onTouchAction={onTouchAction}
        reduceMotion={reduceMotion}
        reserveItems={reserveItems}
        rootProps={props}
        size={size}
        slot="multi-button"
        style={style}
        variant={variant}
      />
    );
  },
);
MultiButton.displayName = "MultiButton";

const CompactMultiButton = React.forwardRef<
  HTMLDivElement,
  CompactMultiButtonProps
>(
  (
    {
      items,
      selectedId,
      iconOnly = false,
      restIcon,
      restAriaLabel,
      syncWidthTo,
      highlightColor,
      gooey = false,
      variant = "default",
      size = "md",
      className,
      style,
      onMouseEnter: onMouseEnterProp,
      onMouseLeave: onMouseLeaveProp,
      onFocusCapture: onFocusCaptureProp,
      onBlurCapture: onBlurCaptureProp,
      onTransitionEnd: onTransitionEndProp,
      ...rootProps
    },
    ref,
  ) => {
    const {
      containerRef,
      filterId,
      hoveredId,
      reduceMotion,
      setHoveredId,
      setTouchExpandedId,
      touchExpandedId,
    } = useMultiButtonInteractions();
    const [mouseExpanded, setMouseExpanded] = React.useState(false);
    const [touchExpanded, setTouchExpanded] = React.useState(false);
    const [focusExpanded, setFocusExpanded] = React.useState(false);
    const [compactRailReady, setCompactRailReady] = React.useState(false);
    const focusRestoreFrameRef = React.useRef<number | null>(null);
    const suppressFocusExpansionRef = React.useRef(false);
    const selectedItem =
      items.find((item) => item.id === selectedId) ?? items[0];
    const isExpanded = mouseExpanded || touchExpanded || focusExpanded;
    const activeId =
      iconOnly || !isExpanded || !compactRailReady
        ? null
        : (hoveredId ?? touchExpandedId);
    const {
      blobGeometries,
      cfg,
      expandedWidth,
      itemWidths,
      measurementRef,
      reserveItems,
    } = useMultiButtonLayout({
      activeId,
      items,
      reserveLabelSpace: !iconOnly,
      size,
      syncWidthTo,
    });
    const containerWidth = isExpanded ? expandedWidth : cfg.cell;
    const expansionStateRef = React.useRef({
      expanded: isExpanded,
      expandedWidth,
    });

    React.useLayoutEffect(() => {
      expansionStateRef.current = { expanded: isExpanded, expandedWidth };
      if (!isExpanded) {
        setCompactRailReady(false);
        return;
      }

      if (reduceMotion) {
        setCompactRailReady(true);
        return;
      }

      setCompactRailReady(expandedWidth === cfg.cell);
    }, [cfg.cell, expandedWidth, isExpanded, reduceMotion]);

    React.useEffect(
      () => () => {
        if (focusRestoreFrameRef.current !== null) {
          cancelAnimationFrame(focusRestoreFrameRef.current);
        }
      },
      [],
    );

    const handleRailTransitionEnd = React.useCallback(
      (event: React.TransitionEvent<HTMLDivElement>) => {
        if (
          event.target !== event.currentTarget ||
          (event.propertyName && event.propertyName !== "width") ||
          reduceMotion
        ) {
          return;
        }

        const expected = expansionStateRef.current;
        const inlineWidth = Number.parseFloat(event.currentTarget.style.width);
        const renderedWidth = event.currentTarget.getBoundingClientRect().width;
        if (
          !expected.expanded ||
          Math.abs(inlineWidth - expected.expandedWidth) > 0.5 ||
          (renderedWidth > 0 &&
            Math.abs(renderedWidth - expected.expandedWidth) > 1)
        ) {
          return;
        }

        setCompactRailReady(true);
      },
      [reduceMotion],
    );

    const collapseTouchAction = React.useCallback(() => {
      setTouchExpanded(false);
      setTouchExpandedId(null);
    }, [setTouchExpandedId]);
    useOutsidePointerDown(touchExpanded, containerRef, collapseTouchAction);

    const onTouchAction = (
      event: React.PointerEvent<HTMLButtonElement>,
      id: string,
    ) => {
      if (event.pointerType && event.pointerType !== "touch") return;
      if (!touchExpanded) {
        event.preventDefault();
        setTouchExpanded(true);
        setTouchExpandedId(id);
      } else {
        setTouchExpandedId(id);
      }
    };

    const collapseAfterAction = (
      event: React.MouseEvent<HTMLButtonElement>,
    ) => {
      if (event.currentTarget.dataset.multiButtonItemId !== selectedItem?.id) {
        const selectedButton = Array.from(
          containerRef.current?.querySelectorAll<HTMLButtonElement>(
            "button[data-multi-button-item-id]",
          ) ?? [],
        ).find(
          (button) => button.dataset.multiButtonItemId === selectedItem?.id,
        );
        selectedButton?.focus({ preventScroll: true });

        if (focusRestoreFrameRef.current !== null) {
          cancelAnimationFrame(focusRestoreFrameRef.current);
        }
        focusRestoreFrameRef.current = requestAnimationFrame(() => {
          focusRestoreFrameRef.current = null;
          const visibleButton = Array.from(
            containerRef.current?.querySelectorAll<HTMLButtonElement>(
              "button[data-multi-button-item-id]",
            ) ?? [],
          ).find((button) => !button.hasAttribute("aria-hidden"));

          if (visibleButton && document.activeElement !== visibleButton) {
            suppressFocusExpansionRef.current = true;
            visibleButton.focus({ preventScroll: true });
          }
        });
      }

      setMouseExpanded(false);
      setTouchExpanded(false);
      setFocusExpanded(false);
      setCompactRailReady(false);
      setHoveredId(null);
      setTouchExpandedId(null);
    };

    return (
      <MultiButtonRail
        activeId={activeId}
        blobGeometries={blobGeometries}
        cellWidth={cfg.cell}
        className={className}
        compact
        containerRef={containerRef}
        containerWidth={containerWidth}
        expanded={isExpanded}
        expandedWidth={expandedWidth}
        filterId={filterId}
        forwardedRef={ref}
        gooey={gooey}
        highlightColor={highlightColor}
        itemWidths={itemWidths}
        interactionReady={compactRailReady}
        items={items}
        measurementRef={measurementRef}
        onAction={collapseAfterAction}
        onHover={setHoveredId}
        onTouchAction={onTouchAction}
        reduceMotion={reduceMotion}
        restAriaLabel={restAriaLabel}
        restIcon={restIcon}
        reserveItems={reserveItems}
        rootProps={{
          ...rootProps,
          onMouseEnter: (event) => {
            setMouseExpanded(true);
            onMouseEnterProp?.(event);
          },
          onMouseLeave: (event) => {
            setMouseExpanded(false);
            setHoveredId(null);
            onMouseLeaveProp?.(event);
          },
          onFocusCapture: (event) => {
            if (suppressFocusExpansionRef.current) {
              suppressFocusExpansionRef.current = false;
            } else {
              setFocusExpanded(true);
            }
            onFocusCaptureProp?.(event);
          },
          onBlurCapture: (event) => {
            if (
              !event.relatedTarget ||
              !event.currentTarget.contains(event.relatedTarget as Node)
            ) {
              setFocusExpanded(false);
              setHoveredId(null);
            }
            onBlurCaptureProp?.(event);
          },
          onTransitionEnd: (event) => {
            handleRailTransitionEnd(event);
            onTransitionEndProp?.(event);
          },
        }}
        selectedId={selectedItem?.id}
        size={size}
        slot="compact-multi-button"
        style={style}
        variant={variant}
      />
    );
  },
);
CompactMultiButton.displayName = "CompactMultiButton";

export { CompactMultiButton, MultiButton, MultiButtonGroup };

Multi Button

By Lucas Bassetti

Try the interaction, copy the React source files, or download the complete example.

About Multi Button

Multi Button is an animated React and Tailwind CSS button by Lucas Bassetti. Try the interaction, copy the React source, or download the example with creator credit.

Effects and categories
Hover effects, Animated
Using this example
Copy a TSX file or download the original source and standalone demo. Test keyboard focus, motion and contrast in your own project.

Usage, Tailwind and licensing questions

Read the source files

import-examples/multi-button.tsx

import {MultiButton} from "@/packages/lab/src/multi-button/multi-button";import {Copy,Download,Share2} from "lucide-react";export default function Demo(){return <MultiButton items={[{id:"copy",label:"Copy",icon:Copy,onClick:()=>{}},{id:"download",label:"Download",icon:Download,onClick:()=>{}},{id:"share",label:"Share",icon:Share2,onClick:()=>{}}]}/>}

packages/lab/src/multi-button/multi-button.tsx

"use client";

import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import * as React from "react";

export type MultiButtonVariant = "default" | "outline" | "secondary" | "ghost";
export type MultiButtonSize = "sm" | "md" | "lg";

export type MultiButtonItem = {
  id: string;
  icon: React.ElementType<{ className?: string }>;
  label: React.ReactNode;
  ariaLabel?: string;
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
  disabled?: boolean;
  className?: string;
  /** Alias kept for callers that want a per-action hover treatment. */
  hoverClassName?: string;
};

export type MultiButtonGroupProps = React.HTMLAttributes<HTMLDivElement> & {
  children: React.ReactNode;
};

type MultiButtonRootProps = Omit<
  React.HTMLAttributes<HTMLDivElement>,
  | "onAnimationStart"
  | "onAnimationEnd"
  | "onClick"
  | "onDrag"
  | "onDragStart"
  | "onDragEnd"
>;

type MultiButtonSharedProps = MultiButtonRootProps & {
  items: MultiButtonItem[];
  /** Optional items used to reserve the same expanded label width as another row. */
  syncWidthTo?: MultiButtonItem[];
  /** Optional CSS color or token reference used for a faint active-item tint. */
  highlightColor?: string;
  /** Use the more expressive SVG metaball treatment instead of the default pill morph. */
  gooey?: boolean;
  variant?: MultiButtonVariant;
  size?: MultiButtonSize;
};

export type MultiButtonProps = MultiButtonSharedProps;

export type CompactMultiButtonProps = MultiButtonSharedProps & {
  selectedId: string;
  /** Keep the compact control icon-only when it opens. */
  iconOnly?: boolean;
  /** Optional independent icon shown while the compact control is at rest. */
  restIcon?: MultiButtonItem["icon"];
  /** Accessible label for the optional rest trigger. Defaults to "Open actions". */
  restAriaLabel?: string;
};

type MultiButtonGroupContextValue = {
  register: (id: string, labelWidth: number) => void;
  unregister: (id: string) => void;
  sharedLabelWidth: number;
};

const MultiButtonGroupContext = React.createContext<
  MultiButtonGroupContextValue | undefined
>(undefined);

const SIZE_CONFIG: Record<
  MultiButtonSize,
  { cell: number; icon: string; minHeight: string; text: string }
> = {
  sm: { cell: 40, icon: "size-3.5", minHeight: "h-10", text: "text-xs" },
  md: { cell: 40, icon: "size-4", minHeight: "h-10", text: "text-sm" },
  lg: {
    cell: 40,
    icon: "size-[18px]",
    minHeight: "h-10",
    text: "text-sm",
  },
};

const VARIANT_CLASSES: Record<MultiButtonVariant, string> = {
  default:
    "bg-primary text-primary-foreground shadow-sm ring-1 ring-primary/20",
  outline:
    "bg-background text-foreground shadow-xs ring-1 ring-inset ring-border/80",
  secondary: "bg-secondary text-secondary-foreground shadow-xs",
  ghost: "bg-muted/50 text-foreground ring-1 ring-border/60",
};

const ITEM_HOVER_CLASSES: Record<MultiButtonVariant, string> = {
  default: "hover:bg-primary-foreground/10 active:bg-primary-foreground/20",
  outline: "hover:bg-accent hover:text-accent-foreground active:bg-accent/80",
  secondary:
    "hover:bg-secondary-foreground/10 active:bg-secondary-foreground/20",
  ghost: "hover:bg-accent hover:text-accent-foreground active:bg-accent/80",
};

const DIVIDER_CLASSES: Record<MultiButtonVariant, string> = {
  default: "bg-primary-foreground/20",
  outline: "bg-border/70",
  secondary: "bg-secondary-foreground/15",
  ghost: "bg-border/70",
};

const GOOEY_BLOB_FILLS: Record<MultiButtonVariant, string> = {
  default: "var(--primary)",
  outline: "var(--background)",
  secondary: "var(--secondary)",
  ghost: "color-mix(in oklab, var(--muted) 50%, transparent)",
};

const GOOEY_TEXT_CLASSES: Record<MultiButtonVariant, string> = {
  default: "text-primary-foreground",
  outline: "text-foreground",
  secondary: "text-secondary-foreground",
  ghost: "text-foreground",
};

const LABEL_SPRING = {
  type: "spring",
  stiffness: 520,
  damping: 32,
} as const;

const CONTEXTUAL_ICON_TRANSITION = {
  type: "spring",
  duration: 0.3,
  bounce: 0,
} as const;

const COMPACT_EXPAND_DURATION_MS = 200;
const GOOEY_BOUNCE_DURATION_MS = 300;
const GOOEY_BOUNCE_TRANSITION = {
  duration: GOOEY_BOUNCE_DURATION_MS / 1000,
  ease: [0.3, 0.7, 0.4, 1.5],
} as const;
const GOOEY_CLOSE_TRANSITION = {
  duration: COMPACT_EXPAND_DURATION_MS / 1000,
  ease: [0.3, 0.7, 0.4, 1],
} as const;

const GOOEY_RAIL_TRANSITION_CLASSES = {
  expanded: "[transition:width_300ms_cubic-bezier(0.3,0.7,0.4,1.5)]",
  collapsed: "[transition:width_200ms_cubic-bezier(0.3,0.7,0.4,1)]",
} as const;

const GOOEY_ITEM_TRANSITION_CLASSES = {
  expanded:
    "[transition:width_300ms_cubic-bezier(0.3,0.7,0.4,1.5),background-color_150ms_ease,color_150ms_ease,scale_120ms_ease]",
  collapsed:
    "[transition:width_200ms_cubic-bezier(0.3,0.7,0.4,1),background-color_150ms_ease,color_150ms_ease,scale_120ms_ease]",
} as const;

const GOOEY_ICON_TRANSITION_CLASSES = {
  expanded: "[transition:transform_300ms_cubic-bezier(0.3,0.7,0.4,1.5)]",
  collapsed: "[transition:transform_200ms_cubic-bezier(0.3,0.7,0.4,1)]",
} as const;

const GOOEY_DIVIDER_TRANSITION_CLASSES = {
  expanded:
    "[transition:width_300ms_cubic-bezier(0.3,0.7,0.4,1.5),opacity_150ms_ease]",
  collapsed:
    "[transition:width_200ms_cubic-bezier(0.3,0.7,0.4,1),opacity_150ms_ease]",
} as const;

function gooeyPhase(expanded: boolean) {
  return expanded ? "expanded" : "collapsed";
}

function gooeyMotionTransition(reduceMotion: boolean, expanded: boolean) {
  if (reduceMotion) return { duration: 0 } as const;
  return expanded ? GOOEY_BOUNCE_TRANSITION : GOOEY_CLOSE_TRANSITION;
}

function railClassName({
  className,
  compact,
  expanded,
  gooey,
  minHeight,
  variant,
}: {
  className?: string;
  compact: boolean;
  expanded: boolean;
  gooey: boolean;
  minHeight: string;
  variant: MultiButtonVariant;
}) {
  const phase = gooeyPhase(expanded);
  const surface = gooey
    ? `overflow-visible ${GOOEY_TEXT_CLASSES[variant]}`
    : `overflow-hidden ${VARIANT_CLASSES[variant]}`;
  const widthTransition = compact
    ? gooey
      ? GOOEY_RAIL_TRANSITION_CLASSES[phase]
      : "[transition:width_200ms_ease-out]"
    : "";

  return `relative inline-flex items-stretch rounded-full ${minHeight} ${surface} ${widthTransition} motion-reduce:[transition:none] ${className ?? ""}`;
}

function useMultiButtonInteractions() {
  const reduceMotion = Boolean(useReducedMotion());
  const filterId = React.useId().replace(/:/g, "");
  const [hoveredId, setHoveredId] = React.useState<string | null>(null);
  const [touchExpandedId, setTouchExpandedId] = React.useState<string | null>(
    null,
  );
  const containerRef = React.useRef<HTMLDivElement>(null);

  return {
    containerRef,
    filterId,
    hoveredId,
    reduceMotion,
    setHoveredId,
    setTouchExpandedId,
    touchExpandedId,
  };
}

function useOutsidePointerDown(
  active: boolean,
  containerRef: React.RefObject<HTMLDivElement | null>,
  onOutside: () => void,
) {
  React.useEffect(() => {
    if (!active) return;
    // The rail may be portaled into a preview iframe, whose document is
    // different from the page document running this component.
    const ownerDocument = containerRef.current?.ownerDocument ?? document;
    const handlePointerDown = (event: PointerEvent) => {
      if (!containerRef.current?.contains(event.target as Node)) onOutside();
    };
    ownerDocument.addEventListener("pointerdown", handlePointerDown);
    return () =>
      ownerDocument.removeEventListener("pointerdown", handlePointerDown);
  }, [active, containerRef, onOutside]);
}

function labelText(label: React.ReactNode) {
  return typeof label === "string" || typeof label === "number"
    ? label.toString()
    : "";
}

function estimateLabelWidth(label: React.ReactNode, size: MultiButtonSize) {
  const textWidth = labelText(label).length * (size === "sm" ? 6.5 : 7.5);
  return Math.ceil(textWidth) + (size === "lg" ? 10 : size === "md" ? 8 : 6);
}

function maxLabelWidth(items: MultiButtonItem[], size: MultiButtonSize) {
  return items.reduce(
    (max, item) => Math.max(max, estimateLabelWidth(item.label, size)),
    0,
  );
}

function estimatedLabelWidths(items: MultiButtonItem[], size: MultiButtonSize) {
  return Object.fromEntries(
    items.map((item) => [item.id, estimateLabelWidth(item.label, size)]),
  );
}

function equalLabelWidths(
  current: Record<string, number>,
  next: Record<string, number>,
) {
  const currentIds = Object.keys(current);
  const nextIds = Object.keys(next);
  return (
    currentIds.length === nextIds.length &&
    nextIds.every((id) => current[id] === next[id])
  );
}

function measuredLabelWidth(
  element: HTMLElement | undefined,
  fallback: number,
) {
  if (!element) return fallback;
  const style = getComputedStyle(element);
  const marginLeft = Number.parseFloat(style.marginLeft) || 0;
  const marginRight = Number.parseFloat(style.marginRight) || 0;
  const measured = Math.ceil(
    element.getBoundingClientRect().width + marginLeft + marginRight,
  );
  return measured > 0 ? measured : fallback;
}

function itemAriaLabel(item: MultiButtonItem) {
  return item.ariaLabel ?? (labelText(item.label) || item.id);
}

function useLabelWidths(
  items: MultiButtonItem[],
  syncWidthTo: MultiButtonItem[] | undefined,
  size: MultiButtonSize,
) {
  const group = React.useContext(MultiButtonGroupContext);
  const instanceId = React.useId();
  const reserveItems = syncWidthTo ?? items;
  const itemEstimates = React.useMemo(
    () => estimatedLabelWidths(items, size),
    [items, size],
  );
  const estimatedReserveWidth = React.useMemo(
    () => maxLabelWidth(reserveItems, size),
    [reserveItems, size],
  );
  const [labelWidths, setLabelWidths] =
    React.useState<Record<string, number>>(itemEstimates);
  const [ownReservedWidth, setOwnReservedWidth] = React.useState(
    estimatedReserveWidth,
  );
  const measurementRef = React.useRef<HTMLDivElement>(null);
  const register = group?.register;
  const unregister = group?.unregister;

  const measureLabels = React.useCallback(() => {
    const itemLabels = measurementRef.current?.querySelectorAll<HTMLElement>(
      '[data-slot="multi-button-item-label-measure"]',
    );
    const nextLabelWidths = Object.fromEntries(
      items.map((item, index) => [
        item.id,
        measuredLabelWidth(itemLabels?.[index], itemEstimates[item.id] ?? 0),
      ]),
    );
    setLabelWidths((current) =>
      equalLabelWidths(current, nextLabelWidths) ? current : nextLabelWidths,
    );

    const reserveLabels = measurementRef.current?.querySelectorAll<HTMLElement>(
      '[data-slot="multi-button-reserve-label-measure"]',
    );
    const measuredReserveWidth = reserveItems.reduce(
      (largest, item, index) =>
        Math.max(
          largest,
          measuredLabelWidth(
            reserveLabels?.[index],
            estimateLabelWidth(item.label, size),
          ),
        ),
      0,
    );
    setOwnReservedWidth((current) =>
      current === measuredReserveWidth ? current : measuredReserveWidth,
    );
  }, [itemEstimates, items, reserveItems, size]);

  React.useLayoutEffect(() => {
    const measurementNode = measurementRef.current;
    if (!measurementNode) return;

    let active = true;
    const measure = () => {
      if (active) measureLabels();
    };
    measure();

    const observer =
      typeof ResizeObserver === "undefined"
        ? undefined
        : new ResizeObserver(measure);
    const labels = measurementNode.querySelectorAll<HTMLElement>(
      '[data-slot$="-label-measure"]',
    );
    labels.forEach((label) => {
      observer?.observe(label);
    });

    void document.fonts?.ready.then(measure);

    return () => {
      active = false;
      observer?.disconnect();
    };
  }, [measureLabels]);

  React.useLayoutEffect(() => {
    if (!register || !unregister) return;
    register(instanceId, ownReservedWidth);
    return () => unregister(instanceId);
  }, [instanceId, ownReservedWidth, register, unregister]);

  return {
    labelWidths,
    reservedLabelWidth: group
      ? Math.max(group.sharedLabelWidth, ownReservedWidth)
      : ownReservedWidth,
    measurementRef,
    reserveItems,
  };
}

function MultiButtonLabelMeasurement({
  items,
  reserveItems,
  measurementRef,
  size,
}: {
  items: MultiButtonItem[];
  reserveItems: MultiButtonItem[];
  measurementRef: React.RefObject<HTMLDivElement | null>;
  size: MultiButtonSize;
}) {
  return (
    <div
      ref={measurementRef}
      aria-hidden="true"
      className="pointer-events-none invisible absolute top-0 left-0 flex h-0 w-0 overflow-hidden"
    >
      {items.map((item) => (
        <span
          key={`item-${item.id}`}
          data-slot="multi-button-item-label-measure"
          className={`-ml-1 shrink-0 whitespace-nowrap pr-2 font-medium leading-none ${SIZE_CONFIG[size].text}`}
        >
          {item.label}
        </span>
      ))}
      {reserveItems.map((item) => (
        <span
          key={`reserve-${item.id}`}
          data-slot="multi-button-reserve-label-measure"
          className={`-ml-1 shrink-0 whitespace-nowrap pr-2 font-medium leading-none ${SIZE_CONFIG[size].text}`}
        >
          {item.label}
        </span>
      ))}
    </div>
  );
}

function actionWidth(
  item: MultiButtonItem,
  activeId: string | null,
  itemCount: number,
  cellWidth: number,
  labelWidths: Record<string, number>,
  reservedLabelWidth: number,
) {
  if (activeId === null) {
    return itemCount > 0
      ? cellWidth + reservedLabelWidth / itemCount
      : cellWidth;
  }

  const activeLabelWidth = labelWidths[activeId] ?? reservedLabelWidth;
  if (item.id === activeId) return cellWidth + activeLabelWidth;

  const remainingLabelSpace = Math.max(
    0,
    reservedLabelWidth - activeLabelWidth,
  );
  return itemCount > 1
    ? cellWidth + remainingLabelSpace / (itemCount - 1)
    : cellWidth;
}

type MultiButtonBlobGeometry = {
  item: MultiButtonItem;
  width: number;
  x: number;
};

function useMultiButtonLayout({
  activeId,
  items,
  reserveLabelSpace = true,
  size,
  syncWidthTo,
}: {
  activeId: string | null;
  items: MultiButtonItem[];
  reserveLabelSpace?: boolean;
  size: MultiButtonSize;
  syncWidthTo?: MultiButtonItem[];
}) {
  const cfg = SIZE_CONFIG[size];
  const { labelWidths, reservedLabelWidth, measurementRef, reserveItems } =
    useLabelWidths(items, syncWidthTo, size);
  const effectiveLabelWidth = reserveLabelSpace ? reservedLabelWidth : 0;
  const dividerWidth = Math.max(0, items.length - 1);
  const expandedWidth =
    items.length > 0
      ? items.length * cfg.cell + effectiveLabelWidth + dividerWidth
      : cfg.cell;
  const itemWidths = items.map((item) =>
    actionWidth(
      item,
      activeId,
      items.length,
      cfg.cell,
      labelWidths,
      effectiveLabelWidth,
    ),
  );
  let blobX = 0;
  const blobGeometries = items.map((item, index) => {
    const geometry = {
      item,
      width: itemWidths[index] ?? cfg.cell,
      x: blobX,
    };
    blobX += geometry.width + (index < items.length - 1 ? 1 : 0);
    return geometry;
  });

  return {
    blobGeometries,
    cfg,
    expandedWidth,
    itemWidths,
    measurementRef,
    reserveItems,
  };
}

function MultiButtonBlobLayer({
  activeId,
  cellWidth,
  expanded,
  filterId,
  geometries,
  highlightColor,
  reduceMotion,
  selectedId,
  variant,
}: {
  activeId: string | null;
  cellWidth: number;
  expanded: boolean;
  filterId: string;
  geometries: MultiButtonBlobGeometry[];
  highlightColor?: string;
  reduceMotion: boolean;
  selectedId: string | undefined;
  variant: MultiButtonVariant;
}) {
  const baseFill = GOOEY_BLOB_FILLS[variant];

  return (
    <svg
      data-slot="multi-button-blob"
      aria-hidden="true"
      className="pointer-events-none absolute top-0 left-0 z-base drop-shadow-sm"
      style={{ overflow: "visible" }}
      width="100%"
      height={cellWidth}
    >
      <defs>
        <filter
          id={filterId}
          x="-100%"
          y="-400%"
          width="300%"
          height="900%"
          colorInterpolationFilters="sRGB"
        >
          <feGaussianBlur in="SourceGraphic" stdDeviation="6" result="blur" />
          <feColorMatrix
            in="blur"
            mode="matrix"
            values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 20 -10"
          />
        </filter>
      </defs>
      <g filter={reduceMotion ? undefined : `url(#${filterId})`}>
        {geometries.map(({ item, width, x }) => {
          const selected = item.id === selectedId;
          const active = item.id === activeId;
          const fill =
            active && highlightColor
              ? `color-mix(in oklch, ${baseFill} 86%, ${highlightColor})`
              : baseFill;

          return (
            <motion.rect
              key={item.id}
              x={0}
              y={0}
              height={cellWidth}
              rx={cellWidth / 2}
              fill={fill}
              style={{
                transformBox: "fill-box",
                transformOrigin: "center",
              }}
              initial={false}
              animate={
                expanded
                  ? { x, width, scale: 1, opacity: 1 }
                  : selected
                    ? { x: 0, width: cellWidth, scale: 1, opacity: 1 }
                    : {
                        x: 0,
                        width: cellWidth,
                        scale: 0.2,
                        opacity: 0,
                      }
              }
              transition={gooeyMotionTransition(reduceMotion, expanded)}
            />
          );
        })}
      </g>
    </svg>
  );
}

function labelMotion(reduceMotion: boolean) {
  return {
    initial: reduceMotion
      ? false
      : { opacity: 0, scale: 0.25, filter: "blur(4px)" },
    animate: { opacity: 1, scale: 1, filter: "blur(0px)" },
    exit: reduceMotion
      ? undefined
      : { opacity: 0, scale: 0.25, filter: "blur(4px)" },
    transition: reduceMotion
      ? { duration: 0 }
      : {
          opacity: LABEL_SPRING,
          scale: LABEL_SPRING,
          filter: { duration: 0.15, ease: "easeOut" },
        },
  } as const;
}

type MultiButtonLabelProps = {
  item: MultiButtonItem;
  reduceMotion: boolean;
  textClass: string;
};

const MultiButtonLabel = React.forwardRef<
  HTMLSpanElement,
  MultiButtonLabelProps
>(({ item, reduceMotion, textClass }, ref) => (
  <motion.span
    ref={ref}
    {...labelMotion(reduceMotion)}
    className={`relative z-raised -ml-1 shrink-0 whitespace-nowrap pr-2 font-medium leading-none ${textClass}`}
  >
    {item.label}
  </motion.span>
));
MultiButtonLabel.displayName = "MultiButtonLabel";

type MultiButtonItemButtonProps = {
  item: MultiButtonItem;
  active: boolean;
  accessible?: boolean;
  ariaLabel?: string;
  disclosureExpanded?: boolean;
  disabled: boolean;
  highlighted: boolean;
  gooey?: boolean;
  gooeyExpanded?: boolean;
  transparent?: boolean;
  size: MultiButtonSize;
  variant: MultiButtonVariant;
  width: number;
  iconOffset: number;
  reduceMotion: boolean;
  restIcon?: MultiButtonItem["icon"];
  showRestIcon?: boolean;
  visible?: boolean;
  onTouchAction: (
    event: React.PointerEvent<HTMLButtonElement>,
    id: string,
  ) => void;
  onHover: (id: string | null) => void;
  onAction?: (event: React.MouseEvent<HTMLButtonElement>) => void;
};

type MultiButtonItemIconProps = {
  actionIcon: MultiButtonItem["icon"];
  gooey: boolean;
  gooeyExpanded: boolean;
  iconClassName: string;
  iconOffset: number;
  laneWidth: number;
  reduceMotion: boolean;
  restIcon?: MultiButtonItem["icon"];
  showRestIcon: boolean;
};

function MultiButtonItemIcon({
  actionIcon: ActionIcon,
  gooey,
  gooeyExpanded,
  iconClassName,
  iconOffset,
  laneWidth,
  reduceMotion,
  restIcon: RestIcon,
  showRestIcon,
}: MultiButtonItemIconProps) {
  const phase = gooeyPhase(gooeyExpanded);
  const iconTransition = reduceMotion
    ? ({ duration: 0 } as const)
    : CONTEXTUAL_ICON_TRANSITION;
  const visibleState = { opacity: 1, scale: 1, filter: "blur(0px)" };
  const hiddenState = { opacity: 0, scale: 0.25, filter: "blur(4px)" };

  return (
    <span
      data-slot="multi-button-icon"
      className={`relative z-raised flex h-full shrink-0 items-center justify-center motion-reduce:[transition:none] ${gooey ? GOOEY_ICON_TRANSITION_CLASSES[phase] : "[transition:transform_200ms_ease-out]"}`}
      style={{
        width: `${laneWidth}px`,
        transform: `translateX(${iconOffset}px)`,
      }}
      aria-hidden="true"
    >
      {RestIcon ? (
        <>
          <motion.span
            data-slot="multi-button-action-icon"
            className="absolute inset-0 flex items-center justify-center"
            initial={false}
            animate={showRestIcon ? hiddenState : visibleState}
            transition={iconTransition}
          >
            <ActionIcon className={iconClassName} />
          </motion.span>
          <motion.span
            data-slot="multi-button-rest-icon"
            className="absolute inset-0 flex items-center justify-center"
            initial={false}
            animate={showRestIcon ? visibleState : hiddenState}
            transition={iconTransition}
          >
            <RestIcon className={iconClassName} />
          </motion.span>
        </>
      ) : (
        <ActionIcon className={iconClassName} />
      )}
    </span>
  );
}

function MultiButtonItemButton({
  item,
  active,
  accessible = true,
  ariaLabel,
  disclosureExpanded,
  disabled,
  highlighted,
  gooey = false,
  gooeyExpanded = true,
  transparent = false,
  size,
  variant,
  width,
  iconOffset,
  reduceMotion,
  restIcon,
  showRestIcon = false,
  visible = true,
  onTouchAction,
  onHover,
  onAction,
}: MultiButtonItemButtonProps) {
  const cfg = SIZE_CONFIG[size];
  const phase = gooeyPhase(gooeyExpanded);

  return (
    <motion.button
      type="button"
      data-state={active ? "open" : "closed"}
      data-multi-button-item-id={item.id}
      aria-label={ariaLabel ?? itemAriaLabel(item)}
      aria-expanded={disclosureExpanded}
      aria-hidden={accessible ? undefined : true}
      tabIndex={accessible ? undefined : -1}
      disabled={disabled}
      onClick={(event) => {
        if (item.disabled) return;
        item.onClick?.(event);
        onAction?.(event);
      }}
      onPointerDown={(event) => onTouchAction(event, item.id)}
      onMouseEnter={() => onHover(item.id)}
      onMouseLeave={() => onHover(null)}
      onFocus={() => onHover(item.id)}
      onBlur={() => onHover(null)}
      initial={false}
      animate={
        gooey
          ? {
              opacity: visible ? 1 : 0,
              scale: visible ? 1 : 0.4,
            }
          : undefined
      }
      transition={
        gooey ? gooeyMotionTransition(reduceMotion, gooeyExpanded) : undefined
      }
      style={{ width: `${width}px` }}
      className={`relative isolate flex shrink-0 cursor-pointer items-center justify-start overflow-hidden ${cfg.minHeight} focus:outline-none focus-visible:z-raised focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset disabled:cursor-not-allowed disabled:opacity-40 motion-reduce:[transition:none] ${accessible ? "" : "pointer-events-none"} ${gooey ? GOOEY_ITEM_TRANSITION_CLASSES[phase] : "[transition:width_200ms_ease-out,background-color_150ms_ease,color_150ms_ease,scale_120ms_ease]"} ${reduceMotion ? "" : "active:scale-[0.96]"} ${transparent ? "" : ITEM_HOVER_CLASSES[variant]} ${item.hoverClassName ?? ""} ${item.className ?? ""}`}
    >
      <span
        data-slot="multi-button-highlight"
        aria-hidden="true"
        className={`pointer-events-none absolute inset-0 z-base bg-[var(--multi-button-highlight)] [transition:opacity_150ms_ease] motion-reduce:[transition:none] ${highlighted ? "opacity-[0.14]" : "opacity-0"}`}
      />
      <MultiButtonItemIcon
        actionIcon={item.icon}
        gooey={gooey}
        gooeyExpanded={gooeyExpanded}
        iconClassName={cfg.icon}
        iconOffset={iconOffset}
        laneWidth={cfg.cell}
        reduceMotion={reduceMotion}
        restIcon={restIcon}
        showRestIcon={showRestIcon}
      />
      <AnimatePresence initial={false} mode="popLayout">
        {active && (
          <MultiButtonLabel
            key={item.id}
            item={item}
            reduceMotion={reduceMotion}
            textClass={cfg.text}
          />
        )}
      </AnimatePresence>
    </motion.button>
  );
}

function MultiButtonDivider({
  activeId,
  compact,
  expanded,
  gooey,
  variant,
}: {
  activeId: string | null;
  compact: boolean;
  expanded: boolean;
  gooey: boolean;
  variant: MultiButtonVariant;
}) {
  if (!compact) {
    return (
      <span
        aria-hidden="true"
        className={`my-2 w-px shrink-0 transition-opacity duration-150 motion-reduce:[transition:none] ${DIVIDER_CLASSES[variant]} ${activeId ? "opacity-0" : "opacity-100"}`}
      />
    );
  }

  const phase = gooeyPhase(expanded);

  return (
    <span
      aria-hidden="true"
      style={{
        width: `${expanded ? 1 : 0}px`,
        opacity: expanded && activeId === null ? 1 : 0,
      }}
      className={`my-2 shrink-0 motion-reduce:[transition:none] ${gooey ? GOOEY_DIVIDER_TRANSITION_CLASSES[phase] : "[transition:width_200ms_ease-out,opacity_200ms_ease-out]"} ${DIVIDER_CLASSES[variant]}`}
    />
  );
}

type MultiButtonItemsProps = {
  activeId: string | null;
  compact: boolean;
  expanded: boolean;
  gooey: boolean;
  highlightColor?: string;
  itemWidths: number[];
  interactionReady: boolean;
  items: MultiButtonItem[];
  onAction?: (event: React.MouseEvent<HTMLButtonElement>) => void;
  onHover: (id: string | null) => void;
  onTouchAction: (
    event: React.PointerEvent<HTMLButtonElement>,
    id: string,
  ) => void;
  reduceMotion: boolean;
  restAriaLabel?: string;
  restIcon?: MultiButtonItem["icon"];
  selectedId?: string;
  size: MultiButtonSize;
  variant: MultiButtonVariant;
};

function MultiButtonItems({
  activeId,
  compact,
  expanded,
  gooey,
  highlightColor,
  itemWidths,
  interactionReady,
  items,
  onAction,
  onHover,
  onTouchAction,
  reduceMotion,
  restAriaLabel,
  restIcon,
  selectedId,
  size,
  variant,
}: MultiButtonItemsProps) {
  const cfg = SIZE_CONFIG[size];

  return items.map((item, index) => {
    const active = activeId === item.id;
    const selected = selectedId === item.id;
    const collapsedSelectedTrigger = compact && selected && !expanded;
    const width =
      compact && !expanded
        ? selected
          ? cfg.cell
          : 0
        : (itemWidths[index] ?? cfg.cell);
    const iconOffset =
      compact && !expanded
        ? 0
        : active
          ? 0
          : Math.max(0, (width - cfg.cell) / 2);

    return (
      <React.Fragment key={item.id}>
        {index > 0 && (
          <MultiButtonDivider
            activeId={activeId}
            compact={compact}
            expanded={expanded}
            gooey={gooey}
            variant={variant}
          />
        )}
        <MultiButtonItemButton
          item={item}
          active={active}
          accessible={!compact || interactionReady || selected}
          ariaLabel={
            collapsedSelectedTrigger
              ? (restAriaLabel ?? (restIcon ? "Open actions" : undefined))
              : undefined
          }
          disclosureExpanded={compact && selected ? expanded : undefined}
          disabled={Boolean(item.disabled && !collapsedSelectedTrigger)}
          highlighted={!gooey && Boolean(highlightColor) && active}
          gooey={gooey}
          gooeyExpanded={expanded}
          transparent={gooey}
          size={size}
          variant={variant}
          width={width}
          iconOffset={iconOffset}
          reduceMotion={reduceMotion}
          restIcon={compact && selected ? restIcon : undefined}
          showRestIcon={compact && selected && Boolean(restIcon) && !expanded}
          visible={!compact || expanded || selected}
          onTouchAction={onTouchAction}
          onHover={onHover}
          onAction={onAction}
        />
      </React.Fragment>
    );
  });
}

type MultiButtonRailContentProps = MultiButtonItemsProps & {
  blobGeometries: MultiButtonBlobGeometry[];
  cellWidth: number;
  expandedWidth: number;
  filterId: string;
  measurementRef: React.RefObject<HTMLDivElement | null>;
  reserveItems: MultiButtonItem[];
};

function MultiButtonRailContent({
  activeId,
  blobGeometries,
  cellWidth,
  compact,
  expanded,
  filterId,
  gooey,
  highlightColor,
  itemWidths,
  interactionReady,
  items,
  measurementRef,
  onAction,
  onHover,
  onTouchAction,
  reduceMotion,
  restAriaLabel,
  restIcon,
  reserveItems,
  selectedId,
  size,
  variant,
}: MultiButtonRailContentProps) {
  return (
    <>
      {gooey && (
        <MultiButtonBlobLayer
          activeId={activeId}
          cellWidth={cellWidth}
          expanded={expanded}
          filterId={filterId}
          geometries={blobGeometries}
          highlightColor={highlightColor}
          reduceMotion={reduceMotion}
          selectedId={selectedId}
          variant={variant}
        />
      )}
      <MultiButtonLabelMeasurement
        items={items}
        reserveItems={reserveItems}
        measurementRef={measurementRef}
        size={size}
      />
      <MultiButtonItems
        activeId={activeId}
        compact={compact}
        expanded={expanded}
        gooey={gooey}
        highlightColor={highlightColor}
        itemWidths={itemWidths}
        interactionReady={interactionReady}
        items={items}
        onAction={onAction}
        onHover={onHover}
        onTouchAction={onTouchAction}
        reduceMotion={reduceMotion}
        restAriaLabel={restAriaLabel}
        restIcon={restIcon}
        selectedId={selectedId}
        size={size}
        variant={variant}
      />
    </>
  );
}

type MultiButtonRailProps = MultiButtonRailContentProps & {
  className?: string;
  containerRef: React.RefObject<HTMLDivElement | null>;
  containerWidth: number;
  forwardedRef: React.ForwardedRef<HTMLDivElement>;
  rootProps: MultiButtonRootProps;
  slot: "multi-button" | "compact-multi-button";
  style?: React.CSSProperties;
};

function MultiButtonRail({
  className,
  compact,
  containerRef,
  containerWidth,
  expanded,
  forwardedRef,
  gooey,
  highlightColor,
  rootProps,
  size,
  slot,
  style,
  variant,
  ...contentProps
}: MultiButtonRailProps) {
  const cfg = SIZE_CONFIG[size];

  return (
    <motion.div
      ref={(node) => {
        containerRef.current = node;
        if (typeof forwardedRef === "function") forwardedRef(node);
        else if (forwardedRef) forwardedRef.current = node;
      }}
      data-slot={slot}
      role="group"
      aria-expanded={compact ? expanded : undefined}
      style={
        {
          ...style,
          width: `${containerWidth}px`,
          "--multi-button-highlight": highlightColor,
        } as React.CSSProperties
      }
      className={railClassName({
        className,
        compact,
        expanded,
        gooey,
        minHeight: cfg.minHeight,
        variant,
      })}
      {...rootProps}
    >
      <MultiButtonRailContent
        {...contentProps}
        compact={compact}
        expanded={expanded}
        gooey={gooey}
        highlightColor={highlightColor}
        size={size}
        variant={variant}
      />
    </motion.div>
  );
}

const MultiButtonGroup = React.forwardRef<
  HTMLDivElement,
  MultiButtonGroupProps
>(({ children, className, ...props }, ref) => {
  const [registry, setRegistry] = React.useState<Record<string, number>>({});

  const register = React.useCallback((id: string, width: number) => {
    setRegistry((current) =>
      current[id] === width ? current : { ...current, [id]: width },
    );
  }, []);

  const unregister = React.useCallback((id: string) => {
    setRegistry((current) => {
      if (!(id in current)) return current;
      const next = { ...current };
      delete next[id];
      return next;
    });
  }, []);

  const sharedLabelWidth = Math.max(0, ...Object.values(registry));
  const context = React.useMemo(
    () => ({ register, unregister, sharedLabelWidth }),
    [register, unregister, sharedLabelWidth],
  );

  return (
    <MultiButtonGroupContext.Provider value={context}>
      <div
        ref={ref}
        data-slot="multi-button-group"
        className={`contents ${className ?? ""}`}
        {...props}
      >
        {children}
      </div>
    </MultiButtonGroupContext.Provider>
  );
});
MultiButtonGroup.displayName = "MultiButtonGroup";

const MultiButton = React.forwardRef<HTMLDivElement, MultiButtonProps>(
  (
    {
      items,
      syncWidthTo,
      highlightColor,
      gooey = false,
      variant = "default",
      size = "md",
      className,
      style,
      ...props
    },
    ref,
  ) => {
    const {
      containerRef,
      filterId,
      hoveredId,
      reduceMotion,
      setHoveredId,
      setTouchExpandedId,
      touchExpandedId,
    } = useMultiButtonInteractions();
    const activeId = hoveredId ?? touchExpandedId;
    const {
      blobGeometries,
      cfg,
      expandedWidth,
      itemWidths,
      measurementRef,
      reserveItems,
    } = useMultiButtonLayout({ activeId, items, size, syncWidthTo });

    const collapseTouchAction = React.useCallback(
      () => setTouchExpandedId(null),
      [setTouchExpandedId],
    );
    useOutsidePointerDown(
      Boolean(touchExpandedId),
      containerRef,
      collapseTouchAction,
    );

    const onTouchAction = (
      event: React.PointerEvent<HTMLButtonElement>,
      id: string,
    ) => {
      if (event.pointerType !== "touch") return;
      if (touchExpandedId !== id) {
        setTouchExpandedId(id);
      }
    };

    return (
      <MultiButtonRail
        activeId={activeId}
        blobGeometries={blobGeometries}
        cellWidth={cfg.cell}
        className={className}
        compact={false}
        containerRef={containerRef}
        containerWidth={expandedWidth}
        expanded
        expandedWidth={expandedWidth}
        filterId={filterId}
        forwardedRef={ref}
        gooey={gooey}
        highlightColor={highlightColor}
        itemWidths={itemWidths}
        interactionReady
        items={items}
        measurementRef={measurementRef}
        onHover={setHoveredId}
        onTouchAction={onTouchAction}
        reduceMotion={reduceMotion}
        reserveItems={reserveItems}
        rootProps={props}
        size={size}
        slot="multi-button"
        style={style}
        variant={variant}
      />
    );
  },
);
MultiButton.displayName = "MultiButton";

const CompactMultiButton = React.forwardRef<
  HTMLDivElement,
  CompactMultiButtonProps
>(
  (
    {
      items,
      selectedId,
      iconOnly = false,
      restIcon,
      restAriaLabel,
      syncWidthTo,
      highlightColor,
      gooey = false,
      variant = "default",
      size = "md",
      className,
      style,
      onMouseEnter: onMouseEnterProp,
      onMouseLeave: onMouseLeaveProp,
      onFocusCapture: onFocusCaptureProp,
      onBlurCapture: onBlurCaptureProp,
      onTransitionEnd: onTransitionEndProp,
      ...rootProps
    },
    ref,
  ) => {
    const {
      containerRef,
      filterId,
      hoveredId,
      reduceMotion,
      setHoveredId,
      setTouchExpandedId,
      touchExpandedId,
    } = useMultiButtonInteractions();
    const [mouseExpanded, setMouseExpanded] = React.useState(false);
    const [touchExpanded, setTouchExpanded] = React.useState(false);
    const [focusExpanded, setFocusExpanded] = React.useState(false);
    const [compactRailReady, setCompactRailReady] = React.useState(false);
    const focusRestoreFrameRef = React.useRef<number | null>(null);
    const suppressFocusExpansionRef = React.useRef(false);
    const selectedItem =
      items.find((item) => item.id === selectedId) ?? items[0];
    const isExpanded = mouseExpanded || touchExpanded || focusExpanded;
    const activeId =
      iconOnly || !isExpanded || !compactRailReady
        ? null
        : (hoveredId ?? touchExpandedId);
    const {
      blobGeometries,
      cfg,
      expandedWidth,
      itemWidths,
      measurementRef,
      reserveItems,
    } = useMultiButtonLayout({
      activeId,
      items,
      reserveLabelSpace: !iconOnly,
      size,
      syncWidthTo,
    });
    const containerWidth = isExpanded ? expandedWidth : cfg.cell;
    const expansionStateRef = React.useRef({
      expanded: isExpanded,
      expandedWidth,
    });

    React.useLayoutEffect(() => {
      expansionStateRef.current = { expanded: isExpanded, expandedWidth };
      if (!isExpanded) {
        setCompactRailReady(false);
        return;
      }

      if (reduceMotion) {
        setCompactRailReady(true);
        return;
      }

      setCompactRailReady(expandedWidth === cfg.cell);
    }, [cfg.cell, expandedWidth, isExpanded, reduceMotion]);

    React.useEffect(
      () => () => {
        if (focusRestoreFrameRef.current !== null) {
          cancelAnimationFrame(focusRestoreFrameRef.current);
        }
      },
      [],
    );

    const handleRailTransitionEnd = React.useCallback(
      (event: React.TransitionEvent<HTMLDivElement>) => {
        if (
          event.target !== event.currentTarget ||
          (event.propertyName && event.propertyName !== "width") ||
          reduceMotion
        ) {
          return;
        }

        const expected = expansionStateRef.current;
        const inlineWidth = Number.parseFloat(event.currentTarget.style.width);
        const renderedWidth = event.currentTarget.getBoundingClientRect().width;
        if (
          !expected.expanded ||
          Math.abs(inlineWidth - expected.expandedWidth) > 0.5 ||
          (renderedWidth > 0 &&
            Math.abs(renderedWidth - expected.expandedWidth) > 1)
        ) {
          return;
        }

        setCompactRailReady(true);
      },
      [reduceMotion],
    );

    const collapseTouchAction = React.useCallback(() => {
      setTouchExpanded(false);
      setTouchExpandedId(null);
    }, [setTouchExpandedId]);
    useOutsidePointerDown(touchExpanded, containerRef, collapseTouchAction);

    const onTouchAction = (
      event: React.PointerEvent<HTMLButtonElement>,
      id: string,
    ) => {
      if (event.pointerType && event.pointerType !== "touch") return;
      if (!touchExpanded) {
        event.preventDefault();
        setTouchExpanded(true);
        setTouchExpandedId(id);
      } else {
        setTouchExpandedId(id);
      }
    };

    const collapseAfterAction = (
      event: React.MouseEvent<HTMLButtonElement>,
    ) => {
      if (event.currentTarget.dataset.multiButtonItemId !== selectedItem?.id) {
        const selectedButton = Array.from(
          containerRef.current?.querySelectorAll<HTMLButtonElement>(
            "button[data-multi-button-item-id]",
          ) ?? [],
        ).find(
          (button) => button.dataset.multiButtonItemId === selectedItem?.id,
        );
        selectedButton?.focus({ preventScroll: true });

        if (focusRestoreFrameRef.current !== null) {
          cancelAnimationFrame(focusRestoreFrameRef.current);
        }
        focusRestoreFrameRef.current = requestAnimationFrame(() => {
          focusRestoreFrameRef.current = null;
          const visibleButton = Array.from(
            containerRef.current?.querySelectorAll<HTMLButtonElement>(
              "button[data-multi-button-item-id]",
            ) ?? [],
          ).find((button) => !button.hasAttribute("aria-hidden"));

          if (visibleButton && document.activeElement !== visibleButton) {
            suppressFocusExpansionRef.current = true;
            visibleButton.focus({ preventScroll: true });
          }
        });
      }

      setMouseExpanded(false);
      setTouchExpanded(false);
      setFocusExpanded(false);
      setCompactRailReady(false);
      setHoveredId(null);
      setTouchExpandedId(null);
    };

    return (
      <MultiButtonRail
        activeId={activeId}
        blobGeometries={blobGeometries}
        cellWidth={cfg.cell}
        className={className}
        compact
        containerRef={containerRef}
        containerWidth={containerWidth}
        expanded={isExpanded}
        expandedWidth={expandedWidth}
        filterId={filterId}
        forwardedRef={ref}
        gooey={gooey}
        highlightColor={highlightColor}
        itemWidths={itemWidths}
        interactionReady={compactRailReady}
        items={items}
        measurementRef={measurementRef}
        onAction={collapseAfterAction}
        onHover={setHoveredId}
        onTouchAction={onTouchAction}
        reduceMotion={reduceMotion}
        restAriaLabel={restAriaLabel}
        restIcon={restIcon}
        reserveItems={reserveItems}
        rootProps={{
          ...rootProps,
          onMouseEnter: (event) => {
            setMouseExpanded(true);
            onMouseEnterProp?.(event);
          },
          onMouseLeave: (event) => {
            setMouseExpanded(false);
            setHoveredId(null);
            onMouseLeaveProp?.(event);
          },
          onFocusCapture: (event) => {
            if (suppressFocusExpansionRef.current) {
              suppressFocusExpansionRef.current = false;
            } else {
              setFocusExpanded(true);
            }
            onFocusCaptureProp?.(event);
          },
          onBlurCapture: (event) => {
            if (
              !event.relatedTarget ||
              !event.currentTarget.contains(event.relatedTarget as Node)
            ) {
              setFocusExpanded(false);
              setHoveredId(null);
            }
            onBlurCaptureProp?.(event);
          },
          onTransitionEnd: (event) => {
            handleRailTransitionEnd(event);
            onTransitionEndProp?.(event);
          },
        }}
        selectedId={selectedItem?.id}
        size={size}
        slot="compact-multi-button"
        style={style}
        variant={variant}
      />
    );
  },
);
CompactMultiButton.displayName = "CompactMultiButton";

export { CompactMultiButton, MultiButton, MultiButtonGroup };

packages/components/theme/light.css

:root,
.light {
  /* shadcn default — neutral */
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);

  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);

  --popover: oklch(1 0 0);
  --popover-foreground: oklch(0.145 0 0);

  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);

  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);

  --accent: oklch(0.97 0 0);
  --accent-foreground: oklch(0.205 0 0);

  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);

  --destructive: oklch(0.577 0.245 27.325);

  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);

  --chart-1: oklch(0.646 0.222 41.116);
  --chart-2: oklch(0.6 0.118 184.704);
  --chart-3: oklch(0.398 0.07 227.392);
  --chart-4: oklch(0.828 0.189 84.429);
  --chart-5: oklch(0.769 0.188 70.08);

  --sidebar: oklch(0.985 0 0);
  --sidebar-foreground: oklch(0.145 0 0);

  --sidebar-primary: oklch(0.205 0 0);
  --sidebar-primary-foreground: oklch(0.985 0 0);

  --sidebar-accent: oklch(0.97 0 0);
  --sidebar-accent-foreground: oklch(0.205 0 0);

  --sidebar-border: oklch(0.922 0 0);
  --sidebar-ring: oklch(0.708 0 0);

  /* Typography — set --font-geist-* in your app (see @godui/components/fonts) */
  --font-sans: var(
    --font-geist-sans,
    "Geist",
    ui-sans-serif,
    system-ui,
    -apple-system,
    BlinkMacSystemFont,
    "Segoe UI",
    Roboto,
    "Helvetica Neue",
    Arial,
    "Noto Sans",
    sans-serif
  );

  --font-serif: var(
    --font-geist-sans,
    "Geist",
    ui-serif,
    Georgia,
    Cambria,
    "Times New Roman",
    Times,
    serif
  );

  --font-mono: var(
    --font-geist-mono,
    "Geist Mono",
    ui-monospace,
    SFMono-Regular,
    Menlo,
    Monaco,
    Consolas,
    "Liberation Mono",
    "Courier New",
    monospace
  );

  /* Radius */
  --radius: 0.625rem;

  /* Shadows */
  --shadow-2xs: 0 1px 2px rgb(0 0 0 / 0.02);

  --shadow-xs: 0 1px 3px rgb(0 0 0 / 0.04);

  --shadow-sm: 0 2px 6px rgb(0 0 0 / 0.05);

  --shadow: 0 4px 12px rgb(0 0 0 / 0.06);

  --shadow-md: 0 8px 20px rgb(0 0 0 / 0.08);

  --shadow-lg: 0 16px 32px rgb(0 0 0 / 0.1);

  --shadow-xl: 0 24px 48px rgb(0 0 0 / 0.12);

  --shadow-2xl: 0 40px 80px rgb(0 0 0 / 0.16);

  /* Syntax Highlighting */
  --syntax-plain: oklch(0.2 0.036 266);
  --syntax-punctuation: oklch(0.54 0.022 266);
  --syntax-keyword: oklch(0.52 0.19 302);
  --syntax-property: oklch(0.56 0.11 90);
  --syntax-string: oklch(0.52 0.106 168);
  --syntax-comment: oklch(0.63 0.016 266);
  --syntax-number: oklch(0.54 0.115 235);

  /* Inline code — track neutral theme tokens */
  --inline-code-bg: var(--muted);
  --inline-code-fg: var(--foreground);
  --inline-code-border: var(--border);

  /* Code blocks */
  --code-block: oklch(0.978 0 0);
  --code-block-foreground: oklch(0.205 0 0);
  --code-block-border: oklch(0.922 0 0);

  /* Search — track neutral theme tokens */
  --search-bg: var(--card);
  --search-fg: var(--muted-foreground);
  --search-border: var(--border);
  --search-kbd-bg: var(--muted);
  --search-kbd-fg: var(--muted-foreground);
  --search-placeholder: var(--muted-foreground);
  --search-highlight-bg: var(--primary);
  --search-highlight-fg: var(--primary-foreground);

  /* Theme panel — track neutral theme tokens */
  --theme-panel-bg: var(--card);
  --theme-panel-border: var(--border);
  --theme-panel-fg: var(--muted-foreground);
  --theme-panel-active-bg: var(--muted);
  --theme-panel-active-fg: var(--foreground);

  /* Z-index */
  --z-base: 0;
  --z-raised: 10;
  --z-overlay: 20;
  --z-sticky: 30;
  --z-popover: 40;
  --z-modal: 50;
  --z-toast: 60;

  /* GOD UI aliases */
  --color-bg: var(--background);
  --color-fg: var(--foreground);
  --color-border: var(--border);

  --color-primary: var(--primary);
  --color-primary-fg: var(--primary-foreground);

  --color-secondary-bg: var(--secondary);
  --color-secondary-fg: var(--secondary-foreground);

  --color-destructive: var(--destructive);
  --color-destructive-fg: oklch(1 0 0);

  /* Effects — sRGB triplets tracking --primary for rgba() consumers */
  --glow-highlight: 61, 102, 223;
  --color-primary-rgb: 61, 102, 223;

  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);

  /* GOD UI Brand — sapphire → light → divine gold */
  --god-gradient: linear-gradient(
    135deg,
    oklch(0.55 0.19 266) 0%,
    oklch(1 0 0) 50%,
    oklch(0.85 0.12 90) 100%
  );

  --god-glow:
    0 0 24px oklch(0.55 0.19 266 / 0.14), 0 0 80px oklch(0.85 0.12 90 / 0.06);

  /* Rainbow — sapphire, iris, aqua, rose, gold */
  --rainbow-1: oklch(0.55 0.19 266);
  --rainbow-2: oklch(0.68 0.16 305);
  --rainbow-3: oklch(0.82 0.1 185);
  --rainbow-4: oklch(0.78 0.12 14);
  --rainbow-5: oklch(0.8 0.13 90);

  --rainbow-speed: 2s;
}

/* Wide-gamut (P3) displays — restore the fuller chroma these tokens were
   authored with; the sRGB-clamped values above remain the fallback. */
@media (color-gamut: p3) {
  :root,
  .light {
    --syntax-string: oklch(0.52 0.13 168);
    --syntax-number: oklch(0.54 0.13 235);
  }
}

packages/components/theme/dark.css

@media (prefers-color-scheme: dark) {
  :root:not(.light) {
    /* shadcn default — neutral dark */
    --background: oklch(0.145 0 0);
    --foreground: oklch(0.985 0 0);

    --card: oklch(0.205 0 0);
    --card-foreground: oklch(0.985 0 0);

    --popover: oklch(0.205 0 0);
    --popover-foreground: oklch(0.985 0 0);

    --primary: oklch(0.922 0 0);
    --primary-foreground: oklch(0.205 0 0);

    --secondary: oklch(0.269 0 0);
    --secondary-foreground: oklch(0.985 0 0);

    --accent: oklch(0.269 0 0);
    --accent-foreground: oklch(0.985 0 0);

    --muted: oklch(0.269 0 0);
    --muted-foreground: oklch(0.708 0 0);

    --destructive: oklch(0.704 0.191 22.216);

    --border: oklch(1 0 0 / 10%);
    --input: oklch(1 0 0 / 15%);
    --ring: oklch(0.556 0 0);

    --chart-1: oklch(0.488 0.243 264.376);
    --chart-2: oklch(0.696 0.17 162.48);
    --chart-3: oklch(0.769 0.188 70.08);
    --chart-4: oklch(0.627 0.265 303.9);
    --chart-5: oklch(0.645 0.246 16.439);

    --sidebar: oklch(0.205 0 0);
    --sidebar-foreground: oklch(0.985 0 0);

    --sidebar-primary: oklch(0.488 0.243 264.376);
    --sidebar-primary-foreground: oklch(0.985 0 0);

    --sidebar-accent: oklch(0.269 0 0);
    --sidebar-accent-foreground: oklch(0.985 0 0);

    --sidebar-border: oklch(1 0 0 / 10%);
    --sidebar-ring: oklch(0.556 0 0);

    /* Syntax Highlighting */
    --syntax-plain: oklch(0.935 0.01 266);
    --syntax-punctuation: oklch(0.65 0.02 266);
    --syntax-keyword: oklch(0.78 0.135 302);
    --syntax-property: oklch(0.83 0.12 90);
    --syntax-string: oklch(0.8 0.13 168);
    --syntax-comment: oklch(0.58 0.02 266);
    --syntax-number: oklch(0.8 0.12 224);

    /* Code blocks */
    --code-block: oklch(0.185 0 0);
    --code-block-foreground: oklch(0.985 0 0);
    --code-block-border: oklch(0.27 0 0);

    /* Inline code — track neutral theme tokens */
    --inline-code-bg: var(--muted);
    --inline-code-fg: var(--foreground);
    --inline-code-border: var(--border);

    /* Search — track neutral theme tokens */
    --search-bg: var(--card);
    --search-fg: var(--muted-foreground);
    --search-border: var(--border);
    --search-kbd-bg: var(--muted);
    --search-kbd-fg: var(--muted-foreground);
    --search-placeholder: var(--muted-foreground);
    --search-highlight-bg: var(--primary);
    --search-highlight-fg: var(--primary-foreground);

    /* Theme panel — track neutral theme tokens */
    --theme-panel-bg: var(--card);
    --theme-panel-border: var(--border);
    --theme-panel-fg: var(--muted-foreground);
    --theme-panel-active-bg: var(--muted);
    --theme-panel-active-fg: var(--foreground);

    /* Shadows */
    --shadow-2xs: 0 1px 2px rgb(0 0 0 / 0.12);

    --shadow-xs: 0 1px 3px rgb(0 0 0 / 0.15);

    --shadow-sm: 0 2px 6px rgb(0 0 0 / 0.2);

    --shadow: 0 4px 12px rgb(0 0 0 / 0.25);

    --shadow-md: 0 8px 20px rgb(0 0 0 / 0.3);

    --shadow-lg: 0 16px 32px rgb(0 0 0 / 0.35);

    --shadow-xl: 0 24px 48px rgb(0 0 0 / 0.4);

    --shadow-2xl: 0 40px 80px rgb(0 0 0 / 0.5);

    /* GOD UI aliases */
    --color-bg: var(--background);
    --color-fg: var(--foreground);
    --color-border: var(--border);

    --color-primary: var(--primary);
    --color-primary-fg: var(--primary-foreground);

    --color-secondary-bg: var(--secondary);
    --color-secondary-fg: var(--secondary-foreground);

    --color-destructive: var(--destructive);
    --color-destructive-fg: oklch(1 0 0);

    /* Effects — sRGB triplets tracking --primary for rgba() consumers */
    --glow-highlight: 66, 108, 229;
    --color-primary-rgb: 66, 108, 229;

    --radius-md: calc(var(--radius) - 2px);
    --radius-lg: var(--radius);

    /* GOD UI Brand — sapphire → light → divine gold */
    --god-gradient: linear-gradient(
      135deg,
      oklch(0.57 0.19 266) 0%,
      oklch(1 0 0) 50%,
      oklch(0.86 0.12 90) 100%
    );

    --god-glow:
      0 0 24px oklch(0.57 0.19 266 / 0.28), 0 0 80px oklch(0.86 0.12 90 / 0.12);

    --rainbow-1: oklch(0.72 0.145 266);
    --rainbow-2: oklch(0.74 0.16 305);
    --rainbow-3: oklch(0.84 0.1 185);
    --rainbow-4: oklch(0.8 0.116 14);
    --rainbow-5: oklch(0.84 0.13 90);

    --rainbow-speed: 2s;
  }
}

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);

  --card: oklch(0.205 0 0);
  --card-foreground: oklch(0.985 0 0);

  --popover: oklch(0.205 0 0);
  --popover-foreground: oklch(0.985 0 0);

  --primary: oklch(0.922 0 0);
  --primary-foreground: oklch(0.205 0 0);

  --secondary: oklch(0.269 0 0);
  --secondary-foreground: oklch(0.985 0 0);

  --accent: oklch(0.269 0 0);
  --accent-foreground: oklch(0.985 0 0);

  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);

  --destructive: oklch(0.704 0.191 22.216);

  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 15%);
  --ring: oklch(0.556 0 0);

  --chart-1: oklch(0.488 0.243 264.376);
  --chart-2: oklch(0.696 0.17 162.48);
  --chart-3: oklch(0.769 0.188 70.08);
  --chart-4: oklch(0.627 0.265 303.9);
  --chart-5: oklch(0.645 0.246 16.439);

  --sidebar: oklch(0.205 0 0);
  --sidebar-foreground: oklch(0.985 0 0);

  --sidebar-primary: oklch(0.488 0.243 264.376);
  --sidebar-primary-foreground: oklch(0.985 0 0);

  --sidebar-accent: oklch(0.269 0 0);
  --sidebar-accent-foreground: oklch(0.985 0 0);

  --sidebar-border: oklch(1 0 0 / 10%);
  --sidebar-ring: oklch(0.556 0 0);

  --syntax-plain: oklch(0.935 0.01 266);
  --syntax-punctuation: oklch(0.65 0.02 266);
  --syntax-keyword: oklch(0.78 0.135 302);
  --syntax-property: oklch(0.83 0.12 90);
  --syntax-string: oklch(0.8 0.13 168);
  --syntax-comment: oklch(0.58 0.02 266);
  --syntax-number: oklch(0.8 0.12 224);

  --code-block: oklch(0.185 0 0);
  --code-block-foreground: oklch(0.985 0 0);
  --code-block-border: oklch(0.27 0 0);

  --inline-code-bg: var(--muted);
  --inline-code-fg: var(--foreground);
  --inline-code-border: var(--border);

  --search-bg: var(--card);
  --search-fg: var(--muted-foreground);
  --search-border: var(--border);
  --search-kbd-bg: var(--muted);
  --search-kbd-fg: var(--muted-foreground);
  --search-placeholder: var(--muted-foreground);
  --search-highlight-bg: var(--primary);
  --search-highlight-fg: var(--primary-foreground);

  --theme-panel-bg: var(--card);
  --theme-panel-border: var(--border);
  --theme-panel-fg: var(--muted-foreground);
  --theme-panel-active-bg: var(--muted);
  --theme-panel-active-fg: var(--foreground);

  --shadow-2xs: 0 1px 2px rgb(0 0 0 / 0.12);
  --shadow-xs: 0 1px 3px rgb(0 0 0 / 0.15);
  --shadow-sm: 0 2px 6px rgb(0 0 0 / 0.2);
  --shadow: 0 4px 12px rgb(0 0 0 / 0.25);
  --shadow-md: 0 8px 20px rgb(0 0 0 / 0.3);
  --shadow-lg: 0 16px 32px rgb(0 0 0 / 0.35);
  --shadow-xl: 0 24px 48px rgb(0 0 0 / 0.4);
  --shadow-2xl: 0 40px 80px rgb(0 0 0 / 0.5);

  --color-bg: var(--background);
  --color-fg: var(--foreground);
  --color-border: var(--border);

  --color-primary: var(--primary);
  --color-primary-fg: var(--primary-foreground);

  --color-secondary-bg: var(--secondary);
  --color-secondary-fg: var(--secondary-foreground);

  --color-destructive: var(--destructive);
  --color-destructive-fg: oklch(1 0 0);

  --glow-highlight: 66, 108, 229;
  --color-primary-rgb: 66, 108, 229;

  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);

  --god-gradient: linear-gradient(
    135deg,
    oklch(0.57 0.19 266) 0%,
    oklch(1 0 0) 50%,
    oklch(0.86 0.12 90) 100%
  );

  --god-glow:
    0 0 24px oklch(0.57 0.19 266 / 0.28), 0 0 80px oklch(0.86 0.12 90 / 0.12);

  --rainbow-1: oklch(0.72 0.145 266);
  --rainbow-2: oklch(0.74 0.16 305);
  --rainbow-3: oklch(0.84 0.1 185);
  --rainbow-4: oklch(0.8 0.116 14);
  --rainbow-5: oklch(0.84 0.13 90);
  --rainbow-speed: 2s;
}

/* Wide-gamut (P3) displays — restore the fuller chroma these tokens were
   authored with; the sRGB-clamped values above remain the fallback. */
@media (color-gamut: p3) {
  @media (prefers-color-scheme: dark) {
    :root:not(.light) {
      --syntax-keyword: oklch(0.78 0.14 302);
      --rainbow-1: oklch(0.72 0.15 266);
      --rainbow-4: oklch(0.8 0.12 14);
    }
  }

  .dark {
    --syntax-keyword: oklch(0.78 0.14 302);
    --rainbow-1: oklch(0.72 0.15 266);
    --rainbow-4: oklch(0.8 0.12 14);
  }
}

packages/components/styles.css

@import "tailwindcss";
@import "./theme/light.css";
@import "./theme/dark.css";

@source "./src";

@custom-variant dark (&:where(.dark, .dark *));

@layer base {
  :is(code, kbd, samp, pre) {
    font-family: var(--font-mono);
  }
}

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-card: var(--card);
  --color-card-foreground: var(--card-foreground);
  --color-popover: var(--popover);
  --color-popover-foreground: var(--popover-foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  --color-secondary: var(--secondary);
  --color-secondary-foreground: var(--secondary-foreground);
  --color-muted: var(--muted);
  --color-muted-foreground: var(--muted-foreground);
  --color-accent: var(--accent);
  --color-accent-foreground: var(--accent-foreground);
  --color-destructive: var(--destructive);
  --color-border: var(--border);
  --color-input: var(--input);
  --color-ring: var(--ring);
  --color-chart-1: var(--chart-1);
  --color-chart-2: var(--chart-2);
  --color-chart-3: var(--chart-3);
  --color-chart-4: var(--chart-4);
  --color-chart-5: var(--chart-5);
  --color-sidebar: var(--sidebar);
  --color-sidebar-foreground: var(--sidebar-foreground);
  --color-sidebar-primary: var(--sidebar-primary);
  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
  --color-sidebar-accent: var(--sidebar-accent);
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
  --color-sidebar-border: var(--sidebar-border);
  --color-sidebar-ring: var(--sidebar-ring);

  --font-sans: var(--font-sans);
  --font-mono: var(--font-mono);
  --font-serif: var(--font-serif);

  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);

  /* Shared button size scale — single source of truth for all buttons (sm/md/lg) */
  --button-py-sm: 0.5rem;
  --button-px-sm: 1rem;
  --button-text-sm: 0.875rem;
  --button-leading-sm: 1.25rem;
  --button-radius-sm: var(--radius-md);

  --button-py-md: 0.75rem;
  --button-px-md: 1.5rem;
  --button-text-md: 0.875rem;
  --button-leading-md: 1.25rem;
  --button-radius-md: var(--radius-lg);

  --button-py-lg: 1rem;
  --button-px-lg: 2rem;
  --button-text-lg: 1rem;
  --button-leading-lg: 1.5rem;
  --button-radius-lg: var(--radius-xl);

  --shadow-2xs: var(--shadow-2xs);
  --shadow-xs: var(--shadow-xs);
  --shadow-sm: var(--shadow-sm);
  --shadow: var(--shadow);
  --shadow-md: var(--shadow-md);
  --shadow-lg: var(--shadow-lg);
  --shadow-xl: var(--shadow-xl);
  --shadow-2xl: var(--shadow-2xl);

  --z-index-base: var(--z-base);
  --z-index-raised: var(--z-raised);
  --z-index-overlay: var(--z-overlay);
  --z-index-sticky: var(--z-sticky);
  --z-index-popover: var(--z-popover);
  --z-index-modal: var(--z-modal);
  --z-index-toast: var(--z-toast);
}

/* ── godui-motion ─────────────────────────────────────────────────────────────
 * Shared motion tokens for core components. GPU-only: every keyframe animates
 * opacity / scale / translate. Mirrored in the `godui-motion` registry entry.
 * Springs are CSS linear() approximations (stiffness k / damping c noted). */
@theme inline {
  /* k=500 c=40, settles ≈374ms */
  --ease-spring-snappy: linear(
    0,
    0.0536,
    0.1667,
    0.3015,
    0.436,
    0.5582,
    0.663,
    0.7491,
    0.8194,
    0.8719,
    0.9113,
    0.9403,
    0.9612,
    0.9758,
    0.9857,
    0.9923,
    0.9967,
    0.9992,
    1.0006,
    1.0013,
    1.0016,
    1.0016,
    1.0014,
    1.0012,
    1
  );
  /* k=260 c=32, settles ≈563ms */
  --ease-spring-smooth: linear(
    0,
    0.0575,
    0.1789,
    0.3182,
    0.4518,
    0.5693,
    0.6676,
    0.747,
    0.8095,
    0.8579,
    0.8949,
    0.9227,
    0.9431,
    0.9586,
    0.9701,
    0.9784,
    0.9845,
    0.9889,
    0.9921,
    0.9943,
    0.996,
    0.9972,
    0.998,
    0.9986,
    1
  );
  /* k=380 c=18, settles ≈771ms, ~19% overshoot */
  --ease-spring-bouncy: linear(
    0,
    0.1642,
    0.4963,
    0.8272,
    1.0624,
    1.1772,
    1.1892,
    1.1415,
    1.0741,
    1.0143,
    0.9772,
    0.963,
    0.9656,
    0.9772,
    0.9902,
    1.0003,
    1.0058,
    1.0073,
    1.006,
    1.0035,
    1.0011,
    0.9995,
    0.9987,
    0.9986,
    1
  );
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);

  --animate-godui-fade-scale-in: godui-fade-scale-in var(--godui-duration-base)
    var(--ease-spring-snappy) backwards;
  --animate-godui-fade-scale-out: godui-fade-scale-out
    var(--godui-duration-fast) var(--ease-out-expo) both;
  --animate-godui-slide-in-from-top: godui-slide-in-from-top
    var(--godui-duration-base) var(--ease-spring-snappy) backwards;
  --animate-godui-slide-in-from-right: godui-slide-in-from-right
    var(--godui-duration-slow) var(--ease-spring-smooth) backwards;
  --animate-godui-slide-in-from-bottom: godui-slide-in-from-bottom
    var(--godui-duration-base) var(--ease-spring-snappy) backwards;
  --animate-godui-slide-in-from-left: godui-slide-in-from-left
    var(--godui-duration-slow) var(--ease-spring-smooth) backwards;
  --animate-godui-slide-out-to-top: godui-slide-out-to-top
    var(--godui-duration-fast) var(--ease-out-expo) both;
  --animate-godui-slide-out-to-right: godui-slide-out-to-right
    var(--godui-duration-base) var(--ease-out-expo) both;
  --animate-godui-slide-out-to-bottom: godui-slide-out-to-bottom
    var(--godui-duration-fast) var(--ease-out-expo) both;
  --animate-godui-slide-out-to-left: godui-slide-out-to-left
    var(--godui-duration-base) var(--ease-out-expo) both;
  --animate-godui-pop: godui-pop var(--godui-duration-base)
    var(--ease-spring-bouncy);
  --animate-godui-fade-in: godui-fade-in var(--godui-duration-base)
    var(--ease-spring-snappy) backwards;
  --animate-godui-fade-out: godui-fade-out var(--godui-duration-fast)
    var(--ease-out-expo) both;
  --animate-godui-popover-in: godui-popover-in var(--godui-duration-base)
    var(--ease-spring-snappy) backwards;
  --animate-godui-popover-out: godui-popover-out var(--godui-duration-fast)
    var(--ease-out-expo) both;
  /* Holds a closing Accordion / Collapsible panel (see godui-reveal-hold). */
  --animate-godui-reveal-hold: godui-reveal-hold var(--godui-duration-base)
    linear;
}

:root {
  --godui-duration-fast: 150ms;
  --godui-duration-base: 260ms;
  --godui-duration-slow: 380ms;
  --godui-enter-scale: 0.96;
  --godui-enter-distance: 0.5rem;
  --godui-pop-scale: 0.96;
  /* Popover drift toward the trigger; components set these per Radix side. */
  --godui-enter-x: 0px;
  --godui-enter-y: 0px;
  /* Movement multiplier: 1 normally, 0 under reduced motion. Set only here, so
   * a component's local --godui-enter-distance can't outrank reduced motion. */
  --godui-motion: 1;
}

/* Reduced motion: nothing moves or scales; animations become short fades. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --godui-enter-scale: 1;
    --godui-enter-distance: 0px;
    --godui-pop-scale: 1;
    --godui-motion: 0;
    --godui-duration-base: 150ms;
    --godui-duration-slow: 150ms;
  }
}

@keyframes godui-fade-scale-in {
  from {
    opacity: 0;
    scale: calc(1 - (1 - var(--godui-enter-scale)) * var(--godui-motion));
  }
}
@keyframes godui-fade-scale-out {
  to {
    opacity: 0;
    scale: calc(1 - (1 - var(--godui-enter-scale)) * var(--godui-motion));
  }
}
@keyframes godui-slide-in-from-top {
  from {
    opacity: 0;
    translate: 0 calc(var(--godui-enter-distance) * -1 * var(--godui-motion));
  }
}
@keyframes godui-slide-in-from-right {
  from {
    opacity: 0;
    translate: calc(var(--godui-enter-distance) * var(--godui-motion)) 0;
  }
}
@keyframes godui-slide-in-from-bottom {
  from {
    opacity: 0;
    translate: 0 calc(var(--godui-enter-distance) * var(--godui-motion));
  }
}
@keyframes godui-slide-in-from-left {
  from {
    opacity: 0;
    translate: calc(var(--godui-enter-distance) * -1 * var(--godui-motion)) 0;
  }
}
@keyframes godui-slide-out-to-top {
  to {
    opacity: 0;
    translate: 0 calc(var(--godui-enter-distance) * -1 * var(--godui-motion));
  }
}
@keyframes godui-slide-out-to-right {
  to {
    opacity: 0;
    translate: calc(var(--godui-enter-distance) * var(--godui-motion)) 0;
  }
}
@keyframes godui-slide-out-to-bottom {
  to {
    opacity: 0;
    translate: 0 calc(var(--godui-enter-distance) * var(--godui-motion));
  }
}
@keyframes godui-slide-out-to-left {
  to {
    opacity: 0;
    translate: calc(var(--godui-enter-distance) * -1 * var(--godui-motion)) 0;
  }
}
@keyframes godui-pop {
  40% {
    scale: calc(1 - (1 - var(--godui-pop-scale)) * var(--godui-motion));
  }
}
@keyframes godui-fade-in {
  from {
    opacity: 0;
  }
}
@keyframes godui-fade-out {
  to {
    opacity: 0;
  }
}
@keyframes godui-popover-in {
  from {
    opacity: 0;
    scale: calc(1 - (1 - var(--godui-enter-scale)) * var(--godui-motion));
    translate: calc(var(--godui-enter-x) * var(--godui-motion))
      calc(var(--godui-enter-y) * var(--godui-motion));
  }
}
@keyframes godui-popover-out {
  to {
    opacity: 0;
    scale: calc(1 - (1 - var(--godui-enter-scale)) * var(--godui-motion));
    translate: calc(var(--godui-enter-x) * var(--godui-motion))
      calc(var(--godui-enter-y) * var(--godui-motion));
  }
}
/* Keeps Radix Presence from hiding a closing Accordion / Collapsible panel
 * until its clip edge (animated in JS by useReveal, on translate) has swept
 * up. 0.99, not 1: Chrome won't composite an animation with no visible
 * change; the panel is hidden when it ends. */
@keyframes godui-reveal-hold {
  to {
    opacity: 0.99;
  }
}

/* ── calendar ────────────────────────────────────────────────────────────────
 * Component keyframes (also in the calendar registry entry). The --godui-
 * calendar-* tuning variables are set on the Calendar root; every distance
 * and delay is multiplied by --godui-motion. No blur: Chrome won't composite
 * a filter that moves pixels, so a blurred month change runs on the main
 * thread. Month change: the old weeks leave fast and short, the new ones
 * arrive a beat later from further away. The exit fades on
 * --godui-calendar-ease-out (an ease-out quad), not expo: expo empties the
 * old month before the new one shows (both together dip to 16% opacity, a
 * blink); quad keeps them above 65%. react-day-picker drops the old month on the old
 * caption's animationend, so the caption's exit (and the dropdown caption's
 * hide) last delay + base, the moment the new weeks land. The caption is
 * text on text, so it doesn't cross-fade like the grid: the old one is gone
 * by 30% of that, the new one starts --godui-calendar-caption-lag after the
 * weeks and ends with them. */
@theme inline {
  --animate-godui-calendar-in-from-end: godui-calendar-in-from-end
    var(--godui-duration-base) var(--ease-spring-snappy)
    calc(var(--godui-calendar-delay) * var(--godui-motion)) backwards;
  --animate-godui-calendar-in-from-start: godui-calendar-in-from-start
    var(--godui-duration-base) var(--ease-spring-snappy)
    calc(var(--godui-calendar-delay) * var(--godui-motion)) backwards;
  --animate-godui-calendar-out-to-start: godui-calendar-out-to-start
    var(--godui-duration-fast) var(--godui-calendar-ease-out) both;
  --animate-godui-calendar-out-to-end: godui-calendar-out-to-end
    var(--godui-duration-fast) var(--godui-calendar-ease-out) both;
  --animate-godui-calendar-caption-in-from-end: godui-calendar-caption-in-from-end
    calc(
      var(--godui-duration-base) -
      var(--godui-calendar-caption-lag) *
      var(--godui-motion)
    )
    var(--ease-spring-snappy)
    calc(
      (var(--godui-calendar-delay) + var(--godui-calendar-caption-lag)) *
      var(--godui-motion)
    )
    backwards;
  --animate-godui-calendar-caption-in-from-start: godui-calendar-caption-in-from-start
    calc(
      var(--godui-duration-base) -
      var(--godui-calendar-caption-lag) *
      var(--godui-motion)
    )
    var(--ease-spring-snappy)
    calc(
      (var(--godui-calendar-delay) + var(--godui-calendar-caption-lag)) *
      var(--godui-motion)
    )
    backwards;
  --animate-godui-calendar-caption-out-to-start: godui-calendar-caption-out-to-start
    calc(
      var(--godui-duration-base) +
      var(--godui-calendar-delay) *
      var(--godui-motion)
    )
    linear both;
  --animate-godui-calendar-caption-out-to-end: godui-calendar-caption-out-to-end
    calc(
      var(--godui-duration-base) +
      var(--godui-calendar-delay) *
      var(--godui-motion)
    )
    linear both;
  --animate-godui-calendar-caption-hide: godui-calendar-caption-hide
    calc(
      var(--godui-duration-base) +
      var(--godui-calendar-delay) *
      var(--godui-motion)
    )
    linear both;
  --animate-godui-calendar-fill-in: godui-calendar-fill-in
    var(--godui-duration-base) var(--ease-spring-bouncy) backwards;
  --animate-godui-calendar-fade-out: godui-calendar-fade-out
    var(--godui-duration-fast) var(--ease-out-expo) both;
  --animate-godui-calendar-fill-shrink: godui-calendar-fill-shrink
    var(--godui-duration-fast) var(--ease-out-expo) both;
  --animate-godui-calendar-track-in: godui-calendar-track-in
    calc(
      var(--godui-calendar-track-span, 1) *
      var(--godui-calendar-sweep) *
      var(--godui-motion) +
      var(--godui-duration-fast) *
      (1 - var(--godui-motion))
    )
    linear
    calc(
      var(--godui-calendar-track-at, 0) *
      var(--godui-calendar-sweep) *
      var(--godui-motion)
    )
    backwards;
  --animate-godui-calendar-ghost-glide: godui-calendar-ghost-glide
    calc(var(--godui-calendar-preview-sweep) * var(--godui-motion))
    cubic-bezier(0.33, 1, 0.68, 1) backwards;
}
@keyframes godui-calendar-in-from-end {
  from {
    opacity: 0;
    translate: calc(
        var(--godui-calendar-drift) *
        var(--godui-calendar-dir) *
        var(--godui-motion)
      )
      0;
  }
}
@keyframes godui-calendar-in-from-start {
  from {
    opacity: 0;
    translate: calc(
        var(--godui-calendar-drift) *
        var(--godui-calendar-dir) *
        -1 *
        var(--godui-motion)
      )
      0;
  }
}
@keyframes godui-calendar-out-to-start {
  to {
    opacity: 0;
    translate: calc(
        var(--godui-calendar-drift-out) *
        var(--godui-calendar-dir) *
        -1 *
        var(--godui-motion)
      )
      0;
  }
}
@keyframes godui-calendar-out-to-end {
  to {
    opacity: 0;
    translate: calc(
        var(--godui-calendar-drift-out) *
        var(--godui-calendar-dir) *
        var(--godui-motion)
      )
      0;
  }
}
@keyframes godui-calendar-caption-in-from-end {
  from {
    opacity: 0;
    translate: calc(
        var(--godui-calendar-caption-drift) *
        var(--godui-calendar-dir) *
        var(--godui-motion)
      )
      0;
  }
}
@keyframes godui-calendar-caption-in-from-start {
  from {
    opacity: 0;
    translate: calc(
        var(--godui-calendar-caption-drift) *
        var(--godui-calendar-dir) *
        -1 *
        var(--godui-motion)
      )
      0;
  }
}
/* The caption exits' curve is --godui-calendar-ease-out written out: Chrome
 * doesn't resolve var() in a keyframe's animation-timing-function (it falls
 * back to linear). */
@keyframes godui-calendar-caption-out-to-start {
  0% {
    opacity: 1;
    translate: 0 0;
    animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
  }
  30%,
  100% {
    opacity: 0;
    translate: calc(
        var(--godui-calendar-caption-drift) *
        var(--godui-calendar-dir) *
        -0.5 *
        var(--godui-motion)
      )
      0;
  }
}
@keyframes godui-calendar-caption-out-to-end {
  0% {
    opacity: 1;
    translate: 0 0;
    animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
  }
  30%,
  100% {
    opacity: 0;
    translate: calc(
        var(--godui-calendar-caption-drift) *
        var(--godui-calendar-dir) *
        0.5 *
        var(--godui-motion)
      )
      0;
  }
}
/* The old dropdown caption, hidden for the whole change: 0 → 0.01, not 0
 * throughout — Chrome won't composite an animation with no visible change. */
@keyframes godui-calendar-caption-hide {
  from {
    opacity: 0;
  }
  to {
    opacity: 0.01;
  }
}
/* A picked day's fill (and its copy of the number) grows from 60% while it
 * fades in over the first third; the spring overshoots to ≈108%. */
@keyframes godui-calendar-fill-in {
  0% {
    opacity: 0;
    scale: calc(1 - 0.4 * var(--godui-motion));
  }
  35% {
    opacity: 1;
  }
}
/* A layer leaving (a deselected day's fill, a range track half): from 1,
 * explicitly — its resting style is already opacity 0, so it stays hidden
 * once the fade ends and inside rdp's clone of the old month. */
@keyframes godui-calendar-fade-out {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}
/* …while a deselected day's fill (the layer's ::before) shrinks. */
@keyframes godui-calendar-fill-shrink {
  to {
    scale: calc(1 - 0.15 * var(--godui-motion));
  }
}
/* A range track half grows across from the side its sweep comes from, in its
 * slice of the sweep (linear: the halves run back to back, one continuous
 * front); under reduced motion every half fades in at once, in fast. */
@keyframes godui-calendar-track-in {
  from {
    opacity: var(--godui-motion);
    scale: calc(1 - var(--godui-motion)) 1;
  }
}
/* A range preview's ghost end moving along a row slides in from the previous
 * end (--godui-calendar-ghost-from days away; a column is the pill's own
 * width) on the ease-out cubic the track's front follows, over the same
 * --godui-calendar-preview-sweep: the pill rides the front. Under reduced
 * motion it takes no time: it snaps. */
@keyframes godui-calendar-ghost-glide {
  from {
    translate: calc(
        var(--godui-calendar-ghost-from, 0) *
        100% *
        var(--godui-calendar-dir) *
        var(--godui-motion)
      )
      0;
  }
}

/* ── checkbox ────────────────────────────────────────────────────────────────
 * Component keyframes (also in the checkbox registry entry). Scale, translate
 * and opacity. The disc rests at scale 1.42 (a circle the box's diagonal
 * wide), so it grows from and drains to 1.42 × (1 - motion): a dot, or under
 * reduced motion no scale and a fade instead. The check's window slides in
 * from -100% while the mark slides back from +100% on the same clock, so the
 * stroke holds still; it starts 70ms in and ends with the disc and the
 * box's pop (staggered ends would each cost a layout frame). The hold keeps Presence from removing the indicator
 * until the ink has drained (godui-reveal-hold, from godui-motion). */
@theme inline {
  --animate-godui-checkbox-fill-in: godui-checkbox-fill-in
    var(--godui-duration-base) var(--ease-spring-smooth) backwards;
  --animate-godui-checkbox-fill-out: godui-checkbox-fill-out
    var(--godui-duration-fast) var(--ease-spring-smooth) both;
  --animate-godui-checkbox-draw: godui-checkbox-draw
    calc(var(--godui-duration-base) - 70ms * var(--godui-motion))
    var(--ease-spring-smooth) calc(70ms * var(--godui-motion)) backwards;
  --animate-godui-checkbox-ink: godui-checkbox-ink
    calc(var(--godui-duration-base) - 70ms * var(--godui-motion))
    var(--ease-spring-smooth) calc(70ms * var(--godui-motion)) backwards;
  --animate-godui-checkbox-check-out: godui-checkbox-check-out
    var(--godui-duration-fast) var(--ease-out-expo) both;
  --animate-godui-checkbox-hold: godui-reveal-hold var(--godui-duration-fast)
    linear;
}
@keyframes godui-checkbox-fill-in {
  from {
    opacity: var(--godui-motion);
    scale: calc(1.42 * (1 - var(--godui-motion)));
  }
}
@keyframes godui-checkbox-fill-out {
  to {
    opacity: var(--godui-motion);
    scale: calc(1.42 * (1 - var(--godui-motion)));
  }
}
@keyframes godui-checkbox-draw {
  from {
    translate: calc(-100% * var(--godui-motion)) 0;
  }
}
@keyframes godui-checkbox-ink {
  from {
    opacity: var(--godui-motion);
    translate: calc(100% * var(--godui-motion)) 0;
  }
}
@keyframes godui-checkbox-check-out {
  to {
    opacity: 0;
    scale: calc(1 - 0.3 * var(--godui-motion));
  }
}

/* ── input-otp ───────────────────────────────────────────────────────────────
 * Component keyframe (also in the input-otp registry entry). Opacity only. */
@theme inline {
  --animate-godui-caret-blink: godui-caret-blink 1.25s ease-out infinite;
}
@keyframes godui-caret-blink {
  0%,
  70%,
  100% {
    opacity: 1;
  }
  20%,
  50% {
    opacity: 0;
  }
}

/* ── progress ────────────────────────────────────────────────────────────────
 * Component keyframe (also in the progress registry entry). Transform only:
 * the indeterminate bar sweeps across the track on a symmetric ease-in-out.
 * An animation beats the indicator's inline transform. */
@theme inline {
  --animate-godui-progress-indeterminate: godui-progress-indeterminate 1.4s
    cubic-bezier(0.65, 0, 0.35, 1) infinite;
}
@keyframes godui-progress-indeterminate {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(250%);
  }
}

/* ── skeleton ────────────────────────────────────────────────────────────────
 * Component keyframe (also in the skeleton registry entry). Transform only:
 * the ::after band starts fully outside the block on the left
 * (-translate-x-full) and the animation moves it fully outside on the right. */
@theme inline {
  --animate-godui-shimmer: godui-shimmer 1.6s cubic-bezier(0.65, 0, 0.35, 1)
    infinite;
}
@keyframes godui-shimmer {
  to {
    translate: 100% 0;
  }
}

packages/lab/styles.css

/* GodUI Lab — per-component animation tokens + keyframes.
 * Import AFTER @godui/components/styles.css. Tokens stay in `@theme inline`
 * because they reference per-element vars (e.g. --speed). */

@source "./src";

@theme inline {
  --animate-shimmer-slide: shimmer-slide var(--speed) ease-in-out infinite
    alternate;
  --animate-spin-around: spin-around calc(var(--speed) * 2) infinite linear;

  --animate-aurora-text: aurora-text var(--aurora-text-speed, 10s) infinite
    linear;

  --animate-magic-rainbow: magic-rainbow var(--rainbow-speed, 2s) infinite
    linear;
  --animate-magic-input-indeterminate: magic-input-indeterminate 1.2s
    ease-in-out infinite alternate;
  --animate-magic-input-sweep: magic-input-sweep 220ms ease-out;
  --animate-magic-input-spin: magic-input-spin 0.8s linear infinite;
  --animate-progress-fold-indeterminate: progress-fold-indeterminate 1.2s
    ease-in-out infinite;

  /* Blueprint grid: a light band sweeps across the lattice */
  --animate-blueprint-sweep: blueprint-sweep var(--blueprint-speed, 8s) linear
    infinite;

  /* Light rays: god-rays rotate and breathe */
  --animate-light-rays-sweep: light-rays-sweep var(--rays-speed, 14s)
    ease-in-out infinite alternate;

  /* Marquee: seamless track scroll (duration driven by the `--duration` var) */
  --animate-marquee: marquee var(--duration, 40s) linear infinite;
  --animate-marquee-vertical: marquee-vertical var(--duration, 40s) linear
    infinite;

  /* App showcase: the phone screen auto-scrolls its screenshot on a loop */
  --animate-app-showcase-scroll: app-showcase-scroll var(--as-loop, 24s) linear
    infinite;

  /* Mask flipbook — one token per sprite (step counts differ) */
  --animate-mask-nature-in: mask-button-in 0.7s steps(22) forwards;
  --animate-mask-nature-out: mask-button-out 0.7s steps(22) forwards;
  --animate-mask-urban-in: mask-button-in 0.7s steps(29) forwards;
  --animate-mask-urban-out: mask-button-out 0.7s steps(29) forwards;
  --animate-mask-forest-in: mask-button-in 0.7s steps(70) forwards;
  --animate-mask-forest-out: mask-button-out 0.7s steps(70) forwards;

  /* Voice orb: corona rotation, idle breathing, and the listening ring pulse */
  --animate-voice-orb-spin: voice-orb-spin 8s linear infinite;
  --animate-voice-orb-breathe: voice-orb-breathe 4s ease-in-out infinite;
  --animate-voice-orb-ring: voice-orb-ring 1.6s ease-out infinite;

  /* Split-flap: the old top leaf falls away, then the new bottom leaf rises */
  --animate-split-flap-fall: split-flap-fall 0.12s
    cubic-bezier(0.3, 0.7, 0.4, 1) forwards;
  --animate-split-flap-rise: split-flap-rise 0.12s
    cubic-bezier(0.22, 1, 0.36, 1) both;
}

@keyframes voice-orb-spin {
  to {
    transform: rotate(360deg);
  }
}

@keyframes voice-orb-breathe {
  0%,
  100% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.04);
  }
}

@keyframes voice-orb-ring {
  0% {
    transform: scale(0.85);
    opacity: 0.6;
  }
  100% {
    transform: scale(1.4);
    opacity: 0;
  }
}

/* Split-flap: top leaf hinges down and away (front carries the old glyph top) */
@keyframes split-flap-fall {
  from {
    transform: rotateX(0deg);
  }
  to {
    transform: rotateX(-90deg);
  }
}

/* Split-flap: bottom leaf hinges up into place (front carries the new glyph bottom) */
@keyframes split-flap-rise {
  from {
    transform: rotateX(90deg);
  }
  to {
    transform: rotateX(0deg);
  }
}

@keyframes shimmer-slide {
  to {
    transform: translate(calc(100cqw - 100%), 0);
  }
}

@keyframes spin-around {
  0% {
    transform: translateZ(0) rotate(0);
  }
  15%,
  35% {
    transform: translateZ(0) rotate(90deg);
  }
  65%,
  85% {
    transform: translateZ(0) rotate(270deg);
  }
  100% {
    transform: translateZ(0) rotate(360deg);
  }
}

/* Aurora text: gradient drifts back and forth across the clipped text */
@keyframes aurora-text {
  0% {
    background-position: 0% 50%;
  }
  50% {
    background-position: 100% 50%;
  }
  100% {
    background-position: 0% 50%;
  }
}

@keyframes magic-rainbow {
  0% {
    background-position: 0% 50%;
  }
  100% {
    background-position: 200% 50%;
  }
}

/* Magic input: indeterminate fill segment bouncing end to end (right→left) */
@keyframes magic-input-indeterminate {
  from {
    background-position: 110% center;
  }
  to {
    background-position: -10% center;
  }
}

/* Magic input: success / error fill sweeping in from the left */
@keyframes magic-input-sweep {
  from {
    background-size: 0% 100%;
  }
  to {
    background-size: 100% 100%;
  }
}

/* Magic input: spinner rotation */
@keyframes magic-input-spin {
  to {
    transform: rotate(360deg);
  }
}

/* Mask button: flipbook reveal driven by a sprite-sheet mask-position */
@keyframes mask-button-in {
  from {
    -webkit-mask-position: 0 0;
    mask-position: 0 0;
  }
  to {
    -webkit-mask-position: 100% 0;
    mask-position: 100% 0;
  }
}

@keyframes mask-button-out {
  from {
    -webkit-mask-position: 100% 0;
    mask-position: 100% 0;
  }
  to {
    -webkit-mask-position: 0 0;
    mask-position: 0 0;
  }
}

/* Marquee: track scrolls one full copy (plus the gap) then loops seamlessly */
@keyframes marquee {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(calc(-100% - var(--gap)));
  }
}

@keyframes marquee-vertical {
  from {
    transform: translateY(0);
  }
  to {
    transform: translateY(calc(-100% - var(--gap)));
  }
}

/* App showcase: track holds two stacked copies; scroll one full copy up */
@keyframes app-showcase-scroll {
  from {
    transform: translateY(0);
  }
  to {
    transform: translateY(-50%);
  }
}

/* Progress fold button: indeterminate segment sweeping left→right */
@keyframes progress-fold-indeterminate {
  0% {
    transform: translateX(-100%);
  }
  100% {
    transform: translateX(250%);
  }
}

/* Blueprint grid: a diagonal light band travels across the lattice */
@keyframes blueprint-sweep {
  0% {
    transform: translate(-50%, -50%) rotate(var(--blueprint-angle, 25deg));
    opacity: 0;
  }
  15%,
  85% {
    opacity: 1;
  }
  100% {
    transform: translate(150%, 150%) rotate(var(--blueprint-angle, 25deg));
    opacity: 0;
  }
}

/* Light rays: the ray fan slowly rotates and breathes in intensity */
@keyframes light-rays-sweep {
  0% {
    transform: rotate(calc(var(--rays-angle, 0deg) - 6deg)) scale(1);
    opacity: calc(var(--rays-intensity, 0.6) * 0.7);
  }
  100% {
    transform: rotate(calc(var(--rays-angle, 0deg) + 6deg)) scale(1.08);
    opacity: var(--rays-intensity, 0.6);
  }
}

demo.tsx

import "@/packages/components/styles.css";
import "@/packages/lab/styles.css";
export { default as default } from "@/import-examples/multi-button";

CSS

/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer properties;
@layer theme, base, components, utilities;
@layer theme {
  :root, :host {
    --font-sans: var(--font-sans);
    --font-serif: var(--font-serif);
    --font-mono: var(--font-mono);
    --spacing: 0.25rem;
    --text-xs: 0.75rem;
    --text-xs--line-height: calc(1 / 0.75);
    --text-sm: 0.875rem;
    --text-sm--line-height: calc(1.25 / 0.875);
    --font-weight-medium: 500;
    --radius-sm: calc(var(--radius) - 4px);
    --radius-md: calc(var(--radius) - 2px);
    --radius-lg: var(--radius);
    --radius-xl: calc(var(--radius) + 4px);
    --shadow-2xs: var(--shadow-2xs);
    --shadow-xs: var(--shadow-xs);
    --shadow-sm: var(--shadow-sm);
    --shadow-md: var(--shadow-md);
    --shadow-lg: var(--shadow-lg);
    --shadow-xl: var(--shadow-xl);
    --shadow-2xl: var(--shadow-2xl);
    --drop-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.15);
    --ease-out: cubic-bezier(0, 0, 0.2, 1);
    --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
    --default-transition-duration: 150ms;
    --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
    --default-font-family: var(--font-sans);
    --default-mono-font-family: var(--font-mono);
    --shadow: var(--shadow);
    --color-background: var(--background);
    --color-foreground: var(--foreground);
    --color-primary: var(--primary);
    --color-primary-foreground: var(--primary-foreground);
    --color-secondary: var(--secondary);
    --color-secondary-foreground: var(--secondary-foreground);
    --color-muted: var(--muted);
    --color-muted-foreground: var(--muted-foreground);
    --color-accent: var(--accent);
    --color-accent-foreground: var(--accent-foreground);
    --color-border: var(--border);
    --color-input: var(--input);
    --color-ring: var(--ring);
    --color-destructive: var(--destructive);
    --color-popover: var(--popover);
    --color-popover-foreground: var(--popover-foreground);
    --color-card: var(--card);
    --color-card-foreground: var(--card-foreground);
    --color-chart-1: var(--chart-1);
    --color-chart-2: var(--chart-2);
    --color-chart-3: var(--chart-3);
    --color-chart-4: var(--chart-4);
    --color-chart-5: var(--chart-5);
    --color-sidebar: var(--sidebar);
    --color-sidebar-foreground: var(--sidebar-foreground);
    --color-sidebar-primary: var(--sidebar-primary);
    --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
    --color-sidebar-accent: var(--sidebar-accent);
    --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
    --color-sidebar-border: var(--sidebar-border);
    --color-sidebar-ring: var(--sidebar-ring);
    --button-py-sm: 0.5rem;
    --button-px-sm: 1rem;
    --button-text-sm: 0.875rem;
    --button-leading-sm: 1.25rem;
    --button-radius-sm: var(--radius-md);
    --button-py-md: 0.75rem;
    --button-px-md: 1.5rem;
    --button-text-md: 0.875rem;
    --button-leading-md: 1.25rem;
    --button-radius-md: var(--radius-lg);
    --button-py-lg: 1rem;
    --button-px-lg: 2rem;
    --button-text-lg: 1rem;
    --button-leading-lg: 1.5rem;
    --button-radius-lg: var(--radius-xl);
    --z-index-base: var(--z-base);
    --z-index-raised: var(--z-raised);
    --z-index-overlay: var(--z-overlay);
    --z-index-sticky: var(--z-sticky);
    --z-index-popover: var(--z-popover);
    --z-index-modal: var(--z-modal);
    --z-index-toast: var(--z-toast);
    --ease-spring-snappy: linear(
    0,
    0.0536,
    0.1667,
    0.3015,
    0.436,
    0.5582,
    0.663,
    0.7491,
    0.8194,
    0.8719,
    0.9113,
    0.9403,
    0.9612,
    0.9758,
    0.9857,
    0.9923,
    0.9967,
    0.9992,
    1.0006,
    1.0013,
    1.0016,
    1.0016,
    1.0014,
    1.0012,
    1
  );
    --ease-spring-smooth: linear(
    0,
    0.0575,
    0.1789,
    0.3182,
    0.4518,
    0.5693,
    0.6676,
    0.747,
    0.8095,
    0.8579,
    0.8949,
    0.9227,
    0.9431,
    0.9586,
    0.9701,
    0.9784,
    0.9845,
    0.9889,
    0.9921,
    0.9943,
    0.996,
    0.9972,
    0.998,
    0.9986,
    1
  );
    --ease-spring-bouncy: linear(
    0,
    0.1642,
    0.4963,
    0.8272,
    1.0624,
    1.1772,
    1.1892,
    1.1415,
    1.0741,
    1.0143,
    0.9772,
    0.963,
    0.9656,
    0.9772,
    0.9902,
    1.0003,
    1.0058,
    1.0073,
    1.006,
    1.0035,
    1.0011,
    0.9995,
    0.9987,
    0.9986,
    1
  );
    --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
    --animate-godui-fade-scale-in: godui-fade-scale-in var(--godui-duration-base)
    var(--ease-spring-snappy) backwards;
    --animate-godui-fade-scale-out: godui-fade-scale-out
    var(--godui-duration-fast) var(--ease-out-expo) both;
    --animate-godui-slide-in-from-top: godui-slide-in-from-top
    var(--godui-duration-base) var(--ease-spring-snappy) backwards;
    --animate-godui-slide-in-from-right: godui-slide-in-from-right
    var(--godui-duration-slow) var(--ease-spring-smooth) backwards;
    --animate-godui-slide-in-from-bottom: godui-slide-in-from-bottom
    var(--godui-duration-base) var(--ease-spring-snappy) backwards;
    --animate-godui-slide-in-from-left: godui-slide-in-from-left
    var(--godui-duration-slow) var(--ease-spring-smooth) backwards;
    --animate-godui-slide-out-to-top: godui-slide-out-to-top
    var(--godui-duration-fast) var(--ease-out-expo) both;
    --animate-godui-slide-out-to-right: godui-slide-out-to-right
    var(--godui-duration-base) var(--ease-out-expo) both;
    --animate-godui-slide-out-to-bottom: godui-slide-out-to-bottom
    var(--godui-duration-fast) var(--ease-out-expo) both;
    --animate-godui-slide-out-to-left: godui-slide-out-to-left
    var(--godui-duration-base) var(--ease-out-expo) both;
    --animate-godui-pop: godui-pop var(--godui-duration-base)
    var(--ease-spring-bouncy);
    --animate-godui-fade-in: godui-fade-in var(--godui-duration-base)
    var(--ease-spring-snappy) backwards;
    --animate-godui-fade-out: godui-fade-out var(--godui-duration-fast)
    var(--ease-out-expo) both;
    --animate-godui-popover-in: godui-popover-in var(--godui-duration-base)
    var(--ease-spring-snappy) backwards;
    --animate-godui-popover-out: godui-popover-out var(--godui-duration-fast)
    var(--ease-out-expo) both;
    --animate-godui-reveal-hold: godui-reveal-hold var(--godui-duration-base)
    linear;
    --animate-godui-calendar-in-from-end: godui-calendar-in-from-end
    var(--godui-duration-base) var(--ease-spring-snappy)
    calc(var(--godui-calendar-delay) * var(--godui-motion)) backwards;
    --animate-godui-calendar-in-from-start: godui-calendar-in-from-start
    var(--godui-duration-base) var(--ease-spring-snappy)
    calc(var(--godui-calendar-delay) * var(--godui-motion)) backwards;
    --animate-godui-calendar-out-to-start: godui-calendar-out-to-start
    var(--godui-duration-fast) var(--godui-calendar-ease-out) both;
    --animate-godui-calendar-out-to-end: godui-calendar-out-to-end
    var(--godui-duration-fast) var(--godui-calendar-ease-out) both;
    --animate-godui-calendar-caption-in-from-end: godui-calendar-caption-in-from-end
    calc(
      var(--godui-duration-base) -
      var(--godui-calendar-caption-lag) *
      var(--godui-motion)
    )
    var(--ease-spring-snappy)
    calc(
      (var(--godui-calendar-delay) + var(--godui-calendar-caption-lag)) *
      var(--godui-motion)
    )
    backwards;
    --animate-godui-calendar-caption-in-from-start: godui-calendar-caption-in-from-start
    calc(
      var(--godui-duration-base) -
      var(--godui-calendar-caption-lag) *
      var(--godui-motion)
    )
    var(--ease-spring-snappy)
    calc(
      (var(--godui-calendar-delay) + var(--godui-calendar-caption-lag)) *
      var(--godui-motion)
    )
    backwards;
    --animate-godui-calendar-caption-out-to-start: godui-calendar-caption-out-to-start
    calc(
      var(--godui-duration-base) +
      var(--godui-calendar-delay) *
      var(--godui-motion)
    )
    linear both;
    --animate-godui-calendar-caption-out-to-end: godui-calendar-caption-out-to-end
    calc(
      var(--godui-duration-base) +
      var(--godui-calendar-delay) *
      var(--godui-motion)
    )
    linear both;
    --animate-godui-calendar-caption-hide: godui-calendar-caption-hide
    calc(
      var(--godui-duration-base) +
      var(--godui-calendar-delay) *
      var(--godui-motion)
    )
    linear both;
    --animate-godui-calendar-fill-in: godui-calendar-fill-in
    var(--godui-duration-base) var(--ease-spring-bouncy) backwards;
    --animate-godui-calendar-fade-out: godui-calendar-fade-out
    var(--godui-duration-fast) var(--ease-out-expo) both;
    --animate-godui-calendar-fill-shrink: godui-calendar-fill-shrink
    var(--godui-duration-fast) var(--ease-out-expo) both;
    --animate-godui-calendar-track-in: godui-calendar-track-in
    calc(
      var(--godui-calendar-track-span, 1) *
      var(--godui-calendar-sweep) *
      var(--godui-motion) +
      var(--godui-duration-fast) *
      (1 - var(--godui-motion))
    )
    linear
    calc(
      var(--godui-calendar-track-at, 0) *
      var(--godui-calendar-sweep) *
      var(--godui-motion)
    )
    backwards;
    --animate-godui-calendar-ghost-glide: godui-calendar-ghost-glide
    calc(var(--godui-calendar-preview-sweep) * var(--godui-motion))
    cubic-bezier(0.33, 1, 0.68, 1) backwards;
    --animate-godui-checkbox-fill-in: godui-checkbox-fill-in
    var(--godui-duration-base) var(--ease-spring-smooth) backwards;
    --animate-godui-checkbox-fill-out: godui-checkbox-fill-out
    var(--godui-duration-fast) var(--ease-spring-smooth) both;
    --animate-godui-checkbox-draw: godui-checkbox-draw
    calc(var(--godui-duration-base) - 70ms * var(--godui-motion))
    var(--ease-spring-smooth) calc(70ms * var(--godui-motion)) backwards;
    --animate-godui-checkbox-ink: godui-checkbox-ink
    calc(var(--godui-duration-base) - 70ms * var(--godui-motion))
    var(--ease-spring-smooth) calc(70ms * var(--godui-motion)) backwards;
    --animate-godui-checkbox-check-out: godui-checkbox-check-out
    var(--godui-duration-fast) var(--ease-out-expo) both;
    --animate-godui-checkbox-hold: godui-reveal-hold var(--godui-duration-fast)
    linear;
    --animate-godui-caret-blink: godui-caret-blink 1.25s ease-out infinite;
    --animate-godui-progress-indeterminate: godui-progress-indeterminate 1.4s
    cubic-bezier(0.65, 0, 0.35, 1) infinite;
    --animate-godui-shimmer: godui-shimmer 1.6s cubic-bezier(0.65, 0, 0.35, 1)
    infinite;
    --animate-shimmer-slide: shimmer-slide var(--speed) ease-in-out infinite
    alternate;
    --animate-spin-around: spin-around calc(var(--speed) * 2) infinite linear;
    --animate-aurora-text: aurora-text var(--aurora-text-speed, 10s) infinite
    linear;
    --animate-magic-rainbow: magic-rainbow var(--rainbow-speed, 2s) infinite
    linear;
    --animate-magic-input-indeterminate: magic-input-indeterminate 1.2s
    ease-in-out infinite alternate;
    --animate-magic-input-sweep: magic-input-sweep 220ms ease-out;
    --animate-magic-input-spin: magic-input-spin 0.8s linear infinite;
    --animate-progress-fold-indeterminate: progress-fold-indeterminate 1.2s
    ease-in-out infinite;
    --animate-blueprint-sweep: blueprint-sweep var(--blueprint-speed, 8s) linear
    infinite;
    --animate-light-rays-sweep: light-rays-sweep var(--rays-speed, 14s)
    ease-in-out infinite alternate;
    --animate-marquee: marquee var(--duration, 40s) linear infinite;
    --animate-marquee-vertical: marquee-vertical var(--duration, 40s) linear
    infinite;
    --animate-app-showcase-scroll: app-showcase-scroll var(--as-loop, 24s) linear
    infinite;
    --animate-mask-nature-in: mask-button-in 0.7s steps(22) forwards;
    --animate-mask-nature-out: mask-button-out 0.7s steps(22) forwards;
    --animate-mask-urban-in: mask-button-in 0.7s steps(29) forwards;
    --animate-mask-urban-out: mask-button-out 0.7s steps(29) forwards;
    --animate-mask-forest-in: mask-button-in 0.7s steps(70) forwards;
    --animate-mask-forest-out: mask-button-out 0.7s steps(70) forwards;
    --animate-voice-orb-spin: voice-orb-spin 8s linear infinite;
    --animate-voice-orb-breathe: voice-orb-breathe 4s ease-in-out infinite;
    --animate-voice-orb-ring: voice-orb-ring 1.6s ease-out infinite;
    --animate-split-flap-fall: split-flap-fall 0.12s
    cubic-bezier(0.3, 0.7, 0.4, 1) forwards;
    --animate-split-flap-rise: split-flap-rise 0.12s
    cubic-bezier(0.22, 1, 0.36, 1) both;
  }
}
@layer base {
  *, ::after, ::before, ::backdrop, ::file-selector-button {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    border: 0 solid;
  }
  html, :host {
    line-height: 1.5;
    -webkit-text-size-adjust: 100%;
    tab-size: 4;
    font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
    font-feature-settings: var(--default-font-feature-settings, normal);
    font-variation-settings: var(--default-font-variation-settings, normal);
    -webkit-tap-highlight-color: transparent;
  }
  hr {
    height: 0;
    color: inherit;
    border-top-width: 1px;
  }
  abbr:where([title]) {
    -webkit-text-decoration: underline dotted;
    text-decoration: underline dotted;
  }
  h1, h2, h3, h4, h5, h6 {
    font-size: inherit;
    font-weight: inherit;
  }
  a {
    color: inherit;
    -webkit-text-decoration: inherit;
    text-decoration: inherit;
  }
  b, strong {
    font-weight: bolder;
  }
  code, kbd, samp, pre {
    font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
    font-feature-settings: var(--default-mono-font-feature-settings, normal);
    font-variation-settings: var(--default-mono-font-variation-settings, normal);
    font-size: 1em;
  }
  small {
    font-size: 80%;
  }
  sub, sup {
    font-size: 75%;
    line-height: 0;
    position: relative;
    vertical-align: baseline;
  }
  sub {
    bottom: -0.25em;
  }
  sup {
    top: -0.5em;
  }
  table {
    text-indent: 0;
    border-color: inherit;
    border-collapse: collapse;
  }
  :-moz-focusring:where(:not(iframe)) {
    outline: auto;
  }
  progress {
    vertical-align: baseline;
  }
  summary {
    display: list-item;
  }
  ol, ul, menu {
    list-style: none;
  }
  img, svg, video, canvas, audio, iframe, embed, object {
    display: block;
    vertical-align: middle;
  }
  img, video {
    max-width: 100%;
    height: auto;
  }
  button, input, select, optgroup, textarea, ::file-selector-button {
    font: inherit;
    font-feature-settings: inherit;
    font-variation-settings: inherit;
    letter-spacing: inherit;
    color: inherit;
    border-radius: 0;
    background-color: transparent;
    opacity: 1;
  }
  :where(select:is([multiple], [size])) optgroup {
    font-weight: bolder;
  }
  :where(select:is([multiple], [size])) optgroup option {
    padding-inline-start: 20px;
  }
  ::file-selector-button {
    margin-inline-end: 4px;
  }
  ::placeholder {
    opacity: 1;
  }
  @supports (not (-webkit-appearance: -apple-pay-button))  or (contain-intrinsic-size: 1px) {
    ::placeholder {
      color: currentcolor;
      @supports (color: color-mix(in lab, red, red)) {
        color: color-mix(in oklab, currentcolor 50%, transparent);
      }
    }
  }
  textarea {
    resize: vertical;
  }
  ::-webkit-search-decoration {
    -webkit-appearance: none;
  }
  ::-webkit-date-and-time-value {
    min-height: 1lh;
    text-align: inherit;
  }
  ::-webkit-datetime-edit {
    display: inline-flex;
  }
  ::-webkit-datetime-edit-fields-wrapper {
    padding: 0;
  }
  ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
    padding-block: 0;
  }
  ::-webkit-calendar-picker-indicator {
    line-height: 1;
  }
  :-moz-ui-invalid {
    box-shadow: none;
  }
  button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {
    appearance: button;
  }
  ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
    height: auto;
  }
  [hidden]:where(:not([hidden="until-found"])) {
    display: none !important;
  }
}
@layer utilities {
  .pointer-events-none {
    pointer-events: none;
  }
  .invisible {
    visibility: hidden;
  }
  .visible {
    visibility: visible;
  }
  .absolute {
    position: absolute;
  }
  .relative {
    position: relative;
  }
  .inset-0 {
    inset: 0px;
  }
  .top-0 {
    top: 0px;
  }
  .left-0 {
    left: 0px;
  }
  .isolate {
    isolation: isolate;
  }
  .z-base {
    z-index: var(--z-base);
  }
  .z-raised {
    z-index: var(--z-raised);
  }
  .my-2 {
    margin-block: calc(var(--spacing) * 2);
  }
  .-ml-1 {
    margin-left: calc(var(--spacing) * -1);
  }
  .block {
    display: block;
  }
  .contents {
    display: contents;
  }
  .flex {
    display: flex;
  }
  .grid {
    display: grid;
  }
  .hidden {
    display: none;
  }
  .inline {
    display: inline;
  }
  .inline-flex {
    display: inline-flex;
  }
  .size-3\.5 {
    width: calc(var(--spacing) * 3.5);
    height: calc(var(--spacing) * 3.5);
  }
  .size-4 {
    width: calc(var(--spacing) * 4);
    height: calc(var(--spacing) * 4);
  }
  .size-\[18px\] {
    width: 18px;
    height: 18px;
  }
  .h-0 {
    height: 0px;
  }
  .h-10 {
    height: calc(var(--spacing) * 10);
  }
  .h-full {
    height: 100%;
  }
  .w-0 {
    width: 0px;
  }
  .w-px {
    width: 1px;
  }
  .shrink-0 {
    flex-shrink: 0;
  }
  .transform {
    transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
  }
  .cursor-pointer {
    cursor: pointer;
  }
  .items-center {
    align-items: center;
  }
  .items-stretch {
    align-items: stretch;
  }
  .justify-center {
    justify-content: center;
  }
  .justify-start {
    justify-content: flex-start;
  }
  .overflow-hidden {
    overflow: hidden;
  }
  .overflow-visible {
    overflow: visible;
  }
  .rounded-full {
    border-radius: calc(infinity * 1px);
  }
  .bg-\[var\(--multi-button-highlight\)\] {
    background-color: var(--multi-button-highlight);
  }
  .bg-background {
    background-color: var(--background);
  }
  .bg-border\/70 {
    background-color: var(--border);
    @supports (color: color-mix(in lab, red, red)) {
      background-color: color-mix(in oklab, var(--border) 70%, transparent);
    }
  }
  .bg-muted\/50 {
    background-color: var(--muted);
    @supports (color: color-mix(in lab, red, red)) {
      background-color: color-mix(in oklab, var(--muted) 50%, transparent);
    }
  }
  .bg-primary {
    background-color: var(--primary);
  }
  .bg-primary-foreground\/20 {
    background-color: var(--primary-foreground);
    @supports (color: color-mix(in lab, red, red)) {
      background-color: color-mix(in oklab, var(--primary-foreground) 20%, transparent);
    }
  }
  .bg-secondary {
    background-color: var(--secondary);
  }
  .bg-secondary-foreground\/15 {
    background-color: var(--secondary-foreground);
    @supports (color: color-mix(in lab, red, red)) {
      background-color: color-mix(in oklab, var(--secondary-foreground) 15%, transparent);
    }
  }
  .pr-2 {
    padding-right: calc(var(--spacing) * 2);
  }
  .text-sm {
    font-size: var(--text-sm);
    line-height: var(--tw-leading, var(--text-sm--line-height));
  }
  .text-xs {
    font-size: var(--text-xs);
    line-height: var(--tw-leading, var(--text-xs--line-height));
  }
  .leading-none {
    --tw-leading: 1;
    line-height: 1;
  }
  .font-medium {
    --tw-font-weight: var(--font-weight-medium);
    font-weight: var(--font-weight-medium);
  }
  .whitespace-nowrap {
    white-space: nowrap;
  }
  .text-foreground {
    color: var(--foreground);
  }
  .text-primary-foreground {
    color: var(--primary-foreground);
  }
  .text-secondary-foreground {
    color: var(--secondary-foreground);
  }
  .opacity-0 {
    opacity: 0%;
  }
  .opacity-100 {
    opacity: 100%;
  }
  .opacity-\[0\.14\] {
    opacity: 0.14;
  }
  .shadow-sm {
    --tw-shadow: var(--shadow-sm);
    box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
  }
  .shadow-xs {
    --tw-shadow: var(--shadow-xs);
    box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
  }
  .ring {
    --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
    box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
  }
  .ring-1 {
    --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
    box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
  }
  .ring-border\/60 {
    --tw-ring-color: var(--border);
    @supports (color: color-mix(in lab, red, red)) {
      --tw-ring-color: color-mix(in oklab, var(--border) 60%, transparent);
    }
  }
  .ring-border\/80 {
    --tw-ring-color: var(--border);
    @supports (color: color-mix(in lab, red, red)) {
      --tw-ring-color: color-mix(in oklab, var(--border) 80%, transparent);
    }
  }
  .ring-primary\/20 {
    --tw-ring-color: var(--primary);
    @supports (color: color-mix(in lab, red, red)) {
      --tw-ring-color: color-mix(in oklab, var(--primary) 20%, transparent);
    }
  }
  .outline {
    outline-style: var(--tw-outline-style);
    outline-width: 1px;
  }
  .blur {
    --tw-blur: blur(8px);
    filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
  }
  .drop-shadow-sm {
    --tw-drop-shadow-size: drop-shadow(0 1px 2px var(--tw-drop-shadow-color, rgb(0 0 0 / 0.15)));
    --tw-drop-shadow: drop-shadow(var(--drop-shadow-sm));
    filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
  }
  .filter {
    filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
  }
  .transition {
    transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
    transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
    transition-duration: var(--tw-duration, var(--default-transition-duration));
  }
  .transition-opacity {
    transition-property: opacity;
    transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
    transition-duration: var(--tw-duration, var(--default-transition-duration));
  }
  .duration-150 {
    --tw-duration: 150ms;
    transition-duration: 150ms;
  }
  .ease-in-out {
    --tw-ease: var(--ease-in-out);
    transition-timing-function: var(--ease-in-out);
  }
  .ease-out {
    --tw-ease: var(--ease-out);
    transition-timing-function: var(--ease-out);
  }
  .\[transition\:opacity_150ms_ease\] {
    transition: opacity 150ms ease;
  }
  .\[transition\:transform_200ms_cubic-bezier\(0\.3\,0\.7\,0\.4\,1\)\] {
    transition: transform 200ms cubic-bezier(0.3,0.7,0.4,1);
  }
  .\[transition\:transform_200ms_ease-out\] {
    transition: transform 200ms ease-out;
  }
  .\[transition\:transform_300ms_cubic-bezier\(0\.3\,0\.7\,0\.4\,1\.5\)\] {
    transition: transform 300ms cubic-bezier(0.3,0.7,0.4,1.5);
  }
  .\[transition\:width_200ms_cubic-bezier\(0\.3\,0\.7\,0\.4\,1\)\,background-color_150ms_ease\,color_150ms_ease\,scale_120ms_ease\] {
    transition: width 200ms cubic-bezier(0.3,0.7,0.4,1),background-color 150ms ease,color 150ms ease,scale 120ms ease;
  }
  .\[transition\:width_200ms_cubic-bezier\(0\.3\,0\.7\,0\.4\,1\)\,opacity_150ms_ease\] {
    transition: width 200ms cubic-bezier(0.3,0.7,0.4,1),opacity 150ms ease;
  }
  .\[transition\:width_200ms_cubic-bezier\(0\.3\,0\.7\,0\.4\,1\)\] {
    transition: width 200ms cubic-bezier(0.3,0.7,0.4,1);
  }
  .\[transition\:width_200ms_ease-out\,background-color_150ms_ease\,color_150ms_ease\,scale_120ms_ease\] {
    transition: width 200ms ease-out,background-color 150ms ease,color 150ms ease,scale 120ms ease;
  }
  .\[transition\:width_200ms_ease-out\,opacity_200ms_ease-out\] {
    transition: width 200ms ease-out,opacity 200ms ease-out;
  }
  .\[transition\:width_200ms_ease-out\] {
    transition: width 200ms ease-out;
  }
  .\[transition\:width_300ms_cubic-bezier\(0\.3\,0\.7\,0\.4\,1\.5\)\,background-color_150ms_ease\,color_150ms_ease\,scale_120ms_ease\] {
    transition: width 300ms cubic-bezier(0.3,0.7,0.4,1.5),background-color 150ms ease,color 150ms ease,scale 120ms ease;
  }
  .\[transition\:width_300ms_cubic-bezier\(0\.3\,0\.7\,0\.4\,1\.5\)\,opacity_150ms_ease\] {
    transition: width 300ms cubic-bezier(0.3,0.7,0.4,1.5),opacity 150ms ease;
  }
  .\[transition\:width_300ms_cubic-bezier\(0\.3\,0\.7\,0\.4\,1\.5\)\] {
    transition: width 300ms cubic-bezier(0.3,0.7,0.4,1.5);
  }
  .ring-inset {
    --tw-ring-inset: inset;
  }
  @media (hover: hover) {
    .hover\:bg-accent:hover {
      background-color: var(--accent);
    }
    .hover\:bg-primary-foreground\/10:hover {
      background-color: var(--primary-foreground);
    }
    @supports (color: color-mix(in lab, red, red)) {
      .hover\:bg-primary-foreground\/10:hover {
        background-color: color-mix(in oklab, var(--primary-foreground) 10%, transparent);
      }
    }
    .hover\:bg-secondary-foreground\/10:hover {
      background-color: var(--secondary-foreground);
    }
    @supports (color: color-mix(in lab, red, red)) {
      .hover\:bg-secondary-foreground\/10:hover {
        background-color: color-mix(in oklab, var(--secondary-foreground) 10%, transparent);
      }
    }
    .hover\:text-accent-foreground:hover {
      color: var(--accent-foreground);
    }
  }
  .focus\:outline-none:focus {
    --tw-outline-style: none;
    outline-style: none;
  }
  .focus-visible\:z-raised:focus-visible {
    z-index: var(--z-raised);
  }
  .focus-visible\:ring-2:focus-visible {
    --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
    box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
  }
  .focus-visible\:ring-ring:focus-visible {
    --tw-ring-color: var(--ring);
  }
  .focus-visible\:ring-inset:focus-visible {
    --tw-ring-inset: inset;
  }
  .active\:scale-\[0\.96\]:active {
    scale: 0.96;
  }
  .active\:bg-accent\/80:active {
    background-color: var(--accent);
    @supports (color: color-mix(in lab, red, red)) {
      background-color: color-mix(in oklab, var(--accent) 80%, transparent);
    }
  }
  .active\:bg-primary-foreground\/20:active {
    background-color: var(--primary-foreground);
    @supports (color: color-mix(in lab, red, red)) {
      background-color: color-mix(in oklab, var(--primary-foreground) 20%, transparent);
    }
  }
  .active\:bg-secondary-foreground\/20:active {
    background-color: var(--secondary-foreground);
    @supports (color: color-mix(in lab, red, red)) {
      background-color: color-mix(in oklab, var(--secondary-foreground) 20%, transparent);
    }
  }
  .disabled\:cursor-not-allowed:disabled {
    cursor: not-allowed;
  }
  .disabled\:opacity-40:disabled {
    opacity: 40%;
  }
  @media (prefers-reduced-motion: reduce) {
    .motion-reduce\:\[transition\:none\] {
      transition: none;
    }
  }
}
:root {
  --background: #fff;
  --foreground: #111;
  --primary: #171717;
  --primary-foreground: #fff;
  --secondary: #eee;
  --secondary-foreground: #171717;
  --muted: #eee;
  --muted-foreground: #666;
  --accent: #eee;
  --accent-foreground: #171717;
  --border: #ddd;
  --input: #ddd;
  --ring: #777;
  --destructive: #dc2626;
  --destructive-foreground: #fff;
  --popover: #fff;
  --popover-foreground: #111;
  --success: #15803d;
  --success-foreground: #fff;
  --radius: .625rem;
  --hairline: 1px;
}
.dark {
  --background: #090909;
  --foreground: #fafafa;
  --primary: #fafafa;
  --primary-foreground: #171717;
  --secondary: #262626;
  --secondary-foreground: #fafafa;
  --muted: #262626;
  --muted-foreground: #a3a3a3;
  --accent: #262626;
  --accent-foreground: #fafafa;
  --border: #333;
  --input: #333;
  --popover: #171717;
  --popover-foreground: #fafafa;
}
:root, .light {
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  --popover: oklch(1 0 0);
  --popover-foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);
  --accent: oklch(0.97 0 0);
  --accent-foreground: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);
  --chart-1: oklch(0.646 0.222 41.116);
  --chart-2: oklch(0.6 0.118 184.704);
  --chart-3: oklch(0.398 0.07 227.392);
  --chart-4: oklch(0.828 0.189 84.429);
  --chart-5: oklch(0.769 0.188 70.08);
  --sidebar: oklch(0.985 0 0);
  --sidebar-foreground: oklch(0.145 0 0);
  --sidebar-primary: oklch(0.205 0 0);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.97 0 0);
  --sidebar-accent-foreground: oklch(0.205 0 0);
  --sidebar-border: oklch(0.922 0 0);
  --sidebar-ring: oklch(0.708 0 0);
  --font-sans: var(
    --font-geist-sans,
    "Geist",
    ui-sans-serif,
    system-ui,
    -apple-system,
    BlinkMacSystemFont,
    "Segoe UI",
    Roboto,
    "Helvetica Neue",
    Arial,
    "Noto Sans",
    sans-serif
  );
  --font-serif: var(
    --font-geist-sans,
    "Geist",
    ui-serif,
    Georgia,
    Cambria,
    "Times New Roman",
    Times,
    serif
  );
  --font-mono: var(
    --font-geist-mono,
    "Geist Mono",
    ui-monospace,
    SFMono-Regular,
    Menlo,
    Monaco,
    Consolas,
    "Liberation Mono",
    "Courier New",
    monospace
  );
  --radius: 0.625rem;
  --shadow-2xs: 0 1px 2px rgb(0 0 0 / 0.02);
  --shadow-xs: 0 1px 3px rgb(0 0 0 / 0.04);
  --shadow-sm: 0 2px 6px rgb(0 0 0 / 0.05);
  --shadow: 0 4px 12px rgb(0 0 0 / 0.06);
  --shadow-md: 0 8px 20px rgb(0 0 0 / 0.08);
  --shadow-lg: 0 16px 32px rgb(0 0 0 / 0.1);
  --shadow-xl: 0 24px 48px rgb(0 0 0 / 0.12);
  --shadow-2xl: 0 40px 80px rgb(0 0 0 / 0.16);
  --syntax-plain: oklch(0.2 0.036 266);
  --syntax-punctuation: oklch(0.54 0.022 266);
  --syntax-keyword: oklch(0.52 0.19 302);
  --syntax-property: oklch(0.56 0.11 90);
  --syntax-string: oklch(0.52 0.106 168);
  --syntax-comment: oklch(0.63 0.016 266);
  --syntax-number: oklch(0.54 0.115 235);
  --inline-code-bg: var(--muted);
  --inline-code-fg: var(--foreground);
  --inline-code-border: var(--border);
  --code-block: oklch(0.978 0 0);
  --code-block-foreground: oklch(0.205 0 0);
  --code-block-border: oklch(0.922 0 0);
  --search-bg: var(--card);
  --search-fg: var(--muted-foreground);
  --search-border: var(--border);
  --search-kbd-bg: var(--muted);
  --search-kbd-fg: var(--muted-foreground);
  --search-placeholder: var(--muted-foreground);
  --search-highlight-bg: var(--primary);
  --search-highlight-fg: var(--primary-foreground);
  --theme-panel-bg: var(--card);
  --theme-panel-border: var(--border);
  --theme-panel-fg: var(--muted-foreground);
  --theme-panel-active-bg: var(--muted);
  --theme-panel-active-fg: var(--foreground);
  --z-base: 0;
  --z-raised: 10;
  --z-overlay: 20;
  --z-sticky: 30;
  --z-popover: 40;
  --z-modal: 50;
  --z-toast: 60;
  --color-bg: var(--background);
  --color-fg: var(--foreground);
  --color-border: var(--border);
  --color-primary: var(--primary);
  --color-primary-fg: var(--primary-foreground);
  --color-secondary-bg: var(--secondary);
  --color-secondary-fg: var(--secondary-foreground);
  --color-destructive: var(--destructive);
  --color-destructive-fg: oklch(1 0 0);
  --glow-highlight: 61, 102, 223;
  --color-primary-rgb: 61, 102, 223;
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --god-gradient: linear-gradient(
    135deg,
    oklch(0.55 0.19 266) 0%,
    oklch(1 0 0) 50%,
    oklch(0.85 0.12 90) 100%
  );
  --god-glow: 0 0 24px oklch(0.55 0.19 266 / 0.14), 0 0 80px oklch(0.85 0.12 90 / 0.06);
  --rainbow-1: oklch(0.55 0.19 266);
  --rainbow-2: oklch(0.68 0.16 305);
  --rainbow-3: oklch(0.82 0.1 185);
  --rainbow-4: oklch(0.78 0.12 14);
  --rainbow-5: oklch(0.8 0.13 90);
  --rainbow-speed: 2s;
}
.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.205 0 0);
  --card-foreground: oklch(0.985 0 0);
  --popover: oklch(0.205 0 0);
  --popover-foreground: oklch(0.985 0 0);
  --primary: oklch(0.922 0 0);
  --primary-foreground: oklch(0.205 0 0);
  --secondary: oklch(0.269 0 0);
  --secondary-foreground: oklch(0.985 0 0);
  --accent: oklch(0.269 0 0);
  --accent-foreground: oklch(0.985 0 0);
  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);
  --destructive: oklch(0.704 0.191 22.216);
  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 15%);
  --ring: oklch(0.556 0 0);
  --chart-1: oklch(0.488 0.243 264.376);
  --chart-2: oklch(0.696 0.17 162.48);
  --chart-3: oklch(0.769 0.188 70.08);
  --chart-4: oklch(0.627 0.265 303.9);
  --chart-5: oklch(0.645 0.246 16.439);
  --sidebar: oklch(0.205 0 0);
  --sidebar-foreground: oklch(0.985 0 0);
  --sidebar-primary: oklch(0.488 0.243 264.376);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.269 0 0);
  --sidebar-accent-foreground: oklch(0.985 0 0);
  --sidebar-border: oklch(1 0 0 / 10%);
  --sidebar-ring: oklch(0.556 0 0);
  --syntax-plain: oklch(0.935 0.01 266);
  --syntax-punctuation: oklch(0.65 0.02 266);
  --syntax-keyword: oklch(0.78 0.135 302);
  --syntax-property: oklch(0.83 0.12 90);
  --syntax-string: oklch(0.8 0.13 168);
  --syntax-comment: oklch(0.58 0.02 266);
  --syntax-number: oklch(0.8 0.12 224);
  --code-block: oklch(0.185 0 0);
  --code-block-foreground: oklch(0.985 0 0);
  --code-block-border: oklch(0.27 0 0);
  --inline-code-bg: var(--muted);
  --inline-code-fg: var(--foreground);
  --inline-code-border: var(--border);
  --search-bg: var(--card);
  --search-fg: var(--muted-foreground);
  --search-border: var(--border);
  --search-kbd-bg: var(--muted);
  --search-kbd-fg: var(--muted-foreground);
  --search-placeholder: var(--muted-foreground);
  --search-highlight-bg: var(--primary);
  --search-highlight-fg: var(--primary-foreground);
  --theme-panel-bg: var(--card);
  --theme-panel-border: var(--border);
  --theme-panel-fg: var(--muted-foreground);
  --theme-panel-active-bg: var(--muted);
  --theme-panel-active-fg: var(--foreground);
  --shadow-2xs: 0 1px 2px rgb(0 0 0 / 0.12);
  --shadow-xs: 0 1px 3px rgb(0 0 0 / 0.15);
  --shadow-sm: 0 2px 6px rgb(0 0 0 / 0.2);
  --shadow: 0 4px 12px rgb(0 0 0 / 0.25);
  --shadow-md: 0 8px 20px rgb(0 0 0 / 0.3);
  --shadow-lg: 0 16px 32px rgb(0 0 0 / 0.35);
  --shadow-xl: 0 24px 48px rgb(0 0 0 / 0.4);
  --shadow-2xl: 0 40px 80px rgb(0 0 0 / 0.5);
  --color-bg: var(--background);
  --color-fg: var(--foreground);
  --color-border: var(--border);
  --color-primary: var(--primary);
  --color-primary-fg: var(--primary-foreground);
  --color-secondary-bg: var(--secondary);
  --color-secondary-fg: var(--secondary-foreground);
  --color-destructive: var(--destructive);
  --color-destructive-fg: oklch(1 0 0);
  --glow-highlight: 66, 108, 229;
  --color-primary-rgb: 66, 108, 229;
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --god-gradient: linear-gradient(
    135deg,
    oklch(0.57 0.19 266) 0%,
    oklch(1 0 0) 50%,
    oklch(0.86 0.12 90) 100%
  );
  --god-glow: 0 0 24px oklch(0.57 0.19 266 / 0.28), 0 0 80px oklch(0.86 0.12 90 / 0.12);
  --rainbow-1: oklch(0.72 0.145 266);
  --rainbow-2: oklch(0.74 0.16 305);
  --rainbow-3: oklch(0.84 0.1 185);
  --rainbow-4: oklch(0.8 0.116 14);
  --rainbow-5: oklch(0.84 0.13 90);
  --rainbow-speed: 2s;
}
:root {
  --godui-duration-fast: 150ms;
  --godui-duration-base: 260ms;
  --godui-duration-slow: 380ms;
  --godui-enter-scale: 0.96;
  --godui-enter-distance: 0.5rem;
  --godui-pop-scale: 0.96;
  --godui-enter-x: 0px;
  --godui-enter-y: 0px;
  --godui-motion: 1;
}
@keyframes godui-fade-scale-in {
  from {
    opacity: 0;
    scale: calc(1 - (1 - var(--godui-enter-scale)) * var(--godui-motion));
  }
}
@keyframes godui-fade-scale-out {
  to {
    opacity: 0;
    scale: calc(1 - (1 - var(--godui-enter-scale)) * var(--godui-motion));
  }
}
@keyframes godui-slide-in-from-top {
  from {
    opacity: 0;
    translate: 0 calc(var(--godui-enter-distance) * -1 * var(--godui-motion));
  }
}
@keyframes godui-slide-in-from-right {
  from {
    opacity: 0;
    translate: calc(var(--godui-enter-distance) * var(--godui-motion)) 0;
  }
}
@keyframes godui-slide-in-from-bottom {
  from {
    opacity: 0;
    translate: 0 calc(var(--godui-enter-distance) * var(--godui-motion));
  }
}
@keyframes godui-slide-in-from-left {
  from {
    opacity: 0;
    translate: calc(var(--godui-enter-distance) * -1 * var(--godui-motion)) 0;
  }
}
@keyframes godui-slide-out-to-top {
  to {
    opacity: 0;
    translate: 0 calc(var(--godui-enter-distance) * -1 * var(--godui-motion));
  }
}
@keyframes godui-slide-out-to-right {
  to {
    opacity: 0;
    translate: calc(var(--godui-enter-distance) * var(--godui-motion)) 0;
  }
}
@keyframes godui-slide-out-to-bottom {
  to {
    opacity: 0;
    translate: 0 calc(var(--godui-enter-distance) * var(--godui-motion));
  }
}
@keyframes godui-slide-out-to-left {
  to {
    opacity: 0;
    translate: calc(var(--godui-enter-distance) * -1 * var(--godui-motion)) 0;
  }
}
@keyframes godui-pop {
  40% {
    scale: calc(1 - (1 - var(--godui-pop-scale)) * var(--godui-motion));
  }
}
@keyframes godui-fade-in {
  from {
    opacity: 0;
  }
}
@keyframes godui-fade-out {
  to {
    opacity: 0;
  }
}
@keyframes godui-popover-in {
  from {
    opacity: 0;
    scale: calc(1 - (1 - var(--godui-enter-scale)) * var(--godui-motion));
    translate: calc(var(--godui-enter-x) * var(--godui-motion)) calc(var(--godui-enter-y) * var(--godui-motion));
  }
}
@keyframes godui-popover-out {
  to {
    opacity: 0;
    scale: calc(1 - (1 - var(--godui-enter-scale)) * var(--godui-motion));
    translate: calc(var(--godui-enter-x) * var(--godui-motion)) calc(var(--godui-enter-y) * var(--godui-motion));
  }
}
@keyframes godui-reveal-hold {
  to {
    opacity: 0.99;
  }
}
@keyframes godui-calendar-in-from-end {
  from {
    opacity: 0;
    translate: calc( var(--godui-calendar-drift) * var(--godui-calendar-dir) * var(--godui-motion) ) 0;
  }
}
@keyframes godui-calendar-in-from-start {
  from {
    opacity: 0;
    translate: calc( var(--godui-calendar-drift) * var(--godui-calendar-dir) * -1 * var(--godui-motion) ) 0;
  }
}
@keyframes godui-calendar-out-to-start {
  to {
    opacity: 0;
    translate: calc( var(--godui-calendar-drift-out) * var(--godui-calendar-dir) * -1 * var(--godui-motion) ) 0;
  }
}
@keyframes godui-calendar-out-to-end {
  to {
    opacity: 0;
    translate: calc( var(--godui-calendar-drift-out) * var(--godui-calendar-dir) * var(--godui-motion) ) 0;
  }
}
@keyframes godui-calendar-caption-in-from-end {
  from {
    opacity: 0;
    translate: calc( var(--godui-calendar-caption-drift) * var(--godui-calendar-dir) * var(--godui-motion) ) 0;
  }
}
@keyframes godui-calendar-caption-in-from-start {
  from {
    opacity: 0;
    translate: calc( var(--godui-calendar-caption-drift) * var(--godui-calendar-dir) * -1 * var(--godui-motion) ) 0;
  }
}
@keyframes godui-calendar-caption-out-to-start {
  0% {
    opacity: 1;
    translate: 0 0;
    animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
  }
  30%, 100% {
    opacity: 0;
    translate: calc( var(--godui-calendar-caption-drift) * var(--godui-calendar-dir) * -0.5 * var(--godui-motion) ) 0;
  }
}
@keyframes godui-calendar-caption-out-to-end {
  0% {
    opacity: 1;
    translate: 0 0;
    animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
  }
  30%, 100% {
    opacity: 0;
    translate: calc( var(--godui-calendar-caption-drift) * var(--godui-calendar-dir) * 0.5 * var(--godui-motion) ) 0;
  }
}
@keyframes godui-calendar-caption-hide {
  from {
    opacity: 0;
  }
  to {
    opacity: 0.01;
  }
}
@keyframes godui-calendar-fill-in {
  0% {
    opacity: 0;
    scale: calc(1 - 0.4 * var(--godui-motion));
  }
  35% {
    opacity: 1;
  }
}
@keyframes godui-calendar-fade-out {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}
@keyframes godui-calendar-fill-shrink {
  to {
    scale: calc(1 - 0.15 * var(--godui-motion));
  }
}
@keyframes godui-calendar-track-in {
  from {
    opacity: var(--godui-motion);
    scale: calc(1 - var(--godui-motion)) 1;
  }
}
@keyframes godui-calendar-ghost-glide {
  from {
    translate: calc( var(--godui-calendar-ghost-from, 0) * 100% * var(--godui-calendar-dir) * var(--godui-motion) ) 0;
  }
}
@keyframes godui-checkbox-fill-in {
  from {
    opacity: var(--godui-motion);
    scale: calc(1.42 * (1 - var(--godui-motion)));
  }
}
@keyframes godui-checkbox-fill-out {
  to {
    opacity: var(--godui-motion);
    scale: calc(1.42 * (1 - var(--godui-motion)));
  }
}
@keyframes godui-checkbox-draw {
  from {
    translate: calc(-100% * var(--godui-motion)) 0;
  }
}
@keyframes godui-checkbox-ink {
  from {
    opacity: var(--godui-motion);
    translate: calc(100% * var(--godui-motion)) 0;
  }
}
@keyframes godui-checkbox-check-out {
  to {
    opacity: 0;
    scale: calc(1 - 0.3 * var(--godui-motion));
  }
}
@keyframes godui-caret-blink {
  0%, 70%, 100% {
    opacity: 1;
  }
  20%, 50% {
    opacity: 0;
  }
}
@keyframes godui-progress-indeterminate {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(250%);
  }
}
@keyframes godui-shimmer {
  to {
    translate: 100% 0;
  }
}
@keyframes voice-orb-spin {
  to {
    transform: rotate(360deg);
  }
}
@keyframes voice-orb-breathe {
  0%, 100% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.04);
  }
}
@keyframes voice-orb-ring {
  0% {
    transform: scale(0.85);
    opacity: 0.6;
  }
  100% {
    transform: scale(1.4);
    opacity: 0;
  }
}
@keyframes split-flap-fall {
  from {
    transform: rotateX(0deg);
  }
  to {
    transform: rotateX(-90deg);
  }
}
@keyframes split-flap-rise {
  from {
    transform: rotateX(90deg);
  }
  to {
    transform: rotateX(0deg);
  }
}
@keyframes shimmer-slide {
  to {
    transform: translate(calc(100cqw - 100%), 0);
  }
}
@keyframes spin-around {
  0% {
    transform: translateZ(0) rotate(0);
  }
  15%, 35% {
    transform: translateZ(0) rotate(90deg);
  }
  65%, 85% {
    transform: translateZ(0) rotate(270deg);
  }
  100% {
    transform: translateZ(0) rotate(360deg);
  }
}
@keyframes aurora-text {
  0% {
    background-position: 0% 50%;
  }
  50% {
    background-position: 100% 50%;
  }
  100% {
    background-position: 0% 50%;
  }
}
@keyframes magic-rainbow {
  0% {
    background-position: 0% 50%;
  }
  100% {
    background-position: 200% 50%;
  }
}
@keyframes magic-input-indeterminate {
  from {
    background-position: 110% center;
  }
  to {
    background-position: -10% center;
  }
}
@keyframes magic-input-sweep {
  from {
    background-size: 0% 100%;
  }
  to {
    background-size: 100% 100%;
  }
}
@keyframes magic-input-spin {
  to {
    transform: rotate(360deg);
  }
}
@keyframes mask-button-in {
  from {
    -webkit-mask-position: 0 0;
    mask-position: 0 0;
  }
  to {
    -webkit-mask-position: 100% 0;
    mask-position: 100% 0;
  }
}
@keyframes mask-button-out {
  from {
    -webkit-mask-position: 100% 0;
    mask-position: 100% 0;
  }
  to {
    -webkit-mask-position: 0 0;
    mask-position: 0 0;
  }
}
@keyframes marquee {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(calc(-100% - var(--gap)));
  }
}
@keyframes marquee-vertical {
  from {
    transform: translateY(0);
  }
  to {
    transform: translateY(calc(-100% - var(--gap)));
  }
}
@keyframes app-showcase-scroll {
  from {
    transform: translateY(0);
  }
  to {
    transform: translateY(-50%);
  }
}
@keyframes progress-fold-indeterminate {
  0% {
    transform: translateX(-100%);
  }
  100% {
    transform: translateX(250%);
  }
}
@keyframes blueprint-sweep {
  0% {
    transform: translate(-50%, -50%) rotate(var(--blueprint-angle, 25deg));
    opacity: 0;
  }
  15%, 85% {
    opacity: 1;
  }
  100% {
    transform: translate(150%, 150%) rotate(var(--blueprint-angle, 25deg));
    opacity: 0;
  }
}
@keyframes light-rays-sweep {
  0% {
    transform: rotate(calc(var(--rays-angle, 0deg) - 6deg)) scale(1);
    opacity: calc(var(--rays-intensity, 0.6) * 0.7);
  }
  100% {
    transform: rotate(calc(var(--rays-angle, 0deg) + 6deg)) scale(1.08);
    opacity: var(--rays-intensity, 0.6);
  }
}
@property --tw-rotate-x {
  syntax: "*";
  inherits: false;
}
@property --tw-rotate-y {
  syntax: "*";
  inherits: false;
}
@property --tw-rotate-z {
  syntax: "*";
  inherits: false;
}
@property --tw-skew-x {
  syntax: "*";
  inherits: false;
}
@property --tw-skew-y {
  syntax: "*";
  inherits: false;
}
@property --tw-leading {
  syntax: "*";
  inherits: false;
}
@property --tw-font-weight {
  syntax: "*";
  inherits: false;
}
@property --tw-shadow {
  syntax: "*";
  inherits: false;
  initial-value: 0 0 #0000;
}
@property --tw-shadow-color {
  syntax: "*";
  inherits: false;
}
@property --tw-shadow-alpha {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 100%;
}
@property --tw-inset-shadow {
  syntax: "*";
  inherits: false;
  initial-value: 0 0 #0000;
}
@property --tw-inset-shadow-color {
  syntax: "*";
  inherits: false;
}
@property --tw-inset-shadow-alpha {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 100%;
}
@property --tw-ring-color {
  syntax: "*";
  inherits: false;
}
@property --tw-ring-shadow {
  syntax: "*";
  inherits: false;
  initial-value: 0 0 #0000;
}
@property --tw-inset-ring-color {
  syntax: "*";
  inherits: false;
}
@property --tw-inset-ring-shadow {
  syntax: "*";
  inherits: false;
  initial-value: 0 0 #0000;
}
@property --tw-ring-inset {
  syntax: "*";
  inherits: false;
}
@property --tw-ring-offset-width {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}
@property --tw-ring-offset-color {
  syntax: "*";
  inherits: false;
  initial-value: #fff;
}
@property --tw-ring-offset-shadow {
  syntax: "*";
  inherits: false;
  initial-value: 0 0 #0000;
}
@property --tw-outline-style {
  syntax: "*";
  inherits: false;
  initial-value: solid;
}
@property --tw-blur {
  syntax: "*";
  inherits: false;
}
@property --tw-brightness {
  syntax: "*";
  inherits: false;
}
@property --tw-contrast {
  syntax: "*";
  inherits: false;
}
@property --tw-grayscale {
  syntax: "*";
  inherits: false;
}
@property --tw-hue-rotate {
  syntax: "*";
  inherits: false;
}
@property --tw-invert {
  syntax: "*";
  inherits: false;
}
@property --tw-opacity {
  syntax: "*";
  inherits: false;
}
@property --tw-saturate {
  syntax: "*";
  inherits: false;
}
@property --tw-sepia {
  syntax: "*";
  inherits: false;
}
@property --tw-drop-shadow {
  syntax: "*";
  inherits: false;
}
@property --tw-drop-shadow-color {
  syntax: "*";
  inherits: false;
}
@property --tw-drop-shadow-alpha {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 100%;
}
@property --tw-drop-shadow-size {
  syntax: "*";
  inherits: false;
}
@property --tw-duration {
  syntax: "*";
  inherits: false;
}
@property --tw-ease {
  syntax: "*";
  inherits: false;
}
@layer properties {
  @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
    *, ::before, ::after, ::backdrop {
      --tw-rotate-x: initial;
      --tw-rotate-y: initial;
      --tw-rotate-z: initial;
      --tw-skew-x: initial;
      --tw-skew-y: initial;
      --tw-leading: initial;
      --tw-font-weight: initial;
      --tw-shadow: 0 0 #0000;
      --tw-shadow-color: initial;
      --tw-shadow-alpha: 100%;
      --tw-inset-shadow: 0 0 #0000;
      --tw-inset-shadow-color: initial;
      --tw-inset-shadow-alpha: 100%;
      --tw-ring-color: initial;
      --tw-ring-shadow: 0 0 #0000;
      --tw-inset-ring-color: initial;
      --tw-inset-ring-shadow: 0 0 #0000;
      --tw-ring-inset: initial;
      --tw-ring-offset-width: 0px;
      --tw-ring-offset-color: #fff;
      --tw-ring-offset-shadow: 0 0 #0000;
      --tw-outline-style: solid;
      --tw-blur: initial;
      --tw-brightness: initial;
      --tw-contrast: initial;
      --tw-grayscale: initial;
      --tw-hue-rotate: initial;
      --tw-invert: initial;
      --tw-opacity: initial;
      --tw-saturate: initial;
      --tw-sepia: initial;
      --tw-drop-shadow: initial;
      --tw-drop-shadow-color: initial;
      --tw-drop-shadow-alpha: 100%;
      --tw-drop-shadow-size: initial;
      --tw-duration: initial;
      --tw-ease: initial;
    }
  }
}