{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "button-group",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": ["@base-ui/react", "motion", "class-variance-authority"],
  "devDependencies": [],
  "files": [
    {
      "path": "button-group.tsx",
      "content": "\"use client\";\n\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { LayoutGroup, motion } from \"motion/react\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst componentThemeClassName =\n  \"[--ic-background:#ffffff] [--ic-foreground:#111111] [--ic-primary:#111111] [--ic-secondary:#646b75] [--ic-surface-border:#e9edf2] [--ic-border:#e3e7ec] [--ic-card:#ffffff] [--ic-card-foreground:#111111] [--ic-muted:#f5f7fa] [--ic-muted-foreground:#6d7480] [--ic-accent:#f3f5f8] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--ic-accent-foreground:#111111] [--ic-input:#e3e7ec] [--ic-ring:rgba(17,17,17,0.16)] [--ic-destructive:#dc2626] [--ic-paper:#fcfcfd] [--ic-popover-foreground:#111111] [--ic-brand:#0ea5e9] [--ic-brand-soft:#bae6fd] [--ic-shadow-soft:0_18px_38px_-24px_rgba(15,23,42,0.35)] [--ic-chart-1:oklch(0.52_0.19_254)] [--ic-chart-2:oklch(0.74_0.11_232)] [--ic-chart-3:oklch(0.42_0.16_262)] [--ic-chart-4:oklch(0.84_0.07_228)] [--ic-chart-5:oklch(0.62_0.14_240)] [--color-background:var(--ic-background)] [--color-foreground:var(--ic-foreground)] [--color-primary:var(--ic-primary)] [--color-secondary:var(--ic-secondary)] [--color-border:var(--ic-border)] [--color-card:var(--ic-card)] [--color-card-foreground:var(--ic-card-foreground)] [--color-muted:var(--ic-muted)] [--color-muted-foreground:var(--ic-muted-foreground)] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--color-input:var(--ic-input)] [--color-ring:var(--ic-ring)] [--color-destructive:var(--ic-destructive)] [--color-paper:var(--ic-paper)] [--color-popover-foreground:var(--ic-popover-foreground)] [--color-brand:var(--ic-brand)] [--color-brand-soft:var(--ic-brand-soft)] [--color-chart-1:var(--ic-chart-1)] [--color-chart-2:var(--ic-chart-2)] [--color-chart-3:var(--ic-chart-3)] [--color-chart-4:var(--ic-chart-4)] [--color-chart-5:var(--ic-chart-5)] dark:[--ic-background:#111111] dark:[--ic-foreground:#f6f3ec] dark:[--ic-primary:#f6f3ec] dark:[--ic-secondary:#cbc6bb] dark:[--ic-surface-border:#2a2a25] dark:[--ic-border:#2b2a25] dark:[--ic-card:#111111] dark:[--ic-card-foreground:#f6f3ec] dark:[--ic-muted:#171716] dark:[--ic-muted-foreground:#9a958a] dark:[--ic-accent:#1a1a18] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] dark:[--ic-accent-foreground:#f6f3ec] dark:[--ic-input:#2b2a25] dark:[--ic-ring:rgba(246,243,236,0.18)] dark:[--ic-destructive:#f87171] dark:[--ic-paper:#171716] dark:[--ic-popover-foreground:#f6f3ec] dark:[--ic-brand:#38bdf8] dark:[--ic-brand-soft:#0c4a6e] dark:[--ic-shadow-soft:0_20px_44px_-28px_rgba(0,0,0,0.6)] dark:[--ic-chart-1:oklch(0.68_0.17_250)] dark:[--ic-chart-2:oklch(0.82_0.09_225)] dark:[--ic-chart-3:oklch(0.58_0.15_260)] dark:[--ic-chart-4:oklch(0.75_0.12_235)] dark:[--ic-chart-5:oklch(0.88_0.06_220)]\";\n\ntype ButtonGroupSize = \"sm\" | \"md\" | \"lg\";\ntype ButtonVariant = \"default\" | \"destructive\" | \"ghost\" | \"outline\";\n\ntype ButtonGroupContextValue = {\n  connected: boolean;\n  size: ButtonGroupSize;\n};\n\nconst ButtonGroupContext = React.createContext<ButtonGroupContextValue | null>(\n  null\n);\n\nfunction useButtonGroupContext() {\n  return React.useContext(ButtonGroupContext);\n}\n\nfunction resolveButtonGroupSize(\n  size: ButtonGroupSize | undefined,\n  context: ButtonGroupContextValue | null\n) {\n  return size ?? context?.size ?? \"md\";\n}\n\nfunction shouldApplyTheme(context: ButtonGroupContextValue | null) {\n  return !context?.connected;\n}\n\ntype SeparatorProps = React.HTMLAttributes<HTMLDivElement> & {\n  decorative?: boolean;\n  orientation?: \"horizontal\" | \"vertical\";\n};\n\nfunction Separator({\n  className,\n  decorative = true,\n  orientation = \"horizontal\",\n  ...props\n}: SeparatorProps) {\n  return (\n    <div\n      aria-hidden={decorative || undefined}\n      className={cn(\n        \"shrink-0 bg-border\",\n        orientation === \"horizontal\" ? \"h-px w-full\" : \"h-full w-px\",\n        className\n      )}\n      data-slot=\"separator\"\n      role={decorative ? \"presentation\" : \"separator\"}\n      {...props}\n    />\n  );\n}\n\nconst buttonGroupVariants = cva(\n  \"flex w-fit items-stretch overflow-hidden rounded-lg border border-border bg-background has-[>[data-slot=button-group-items]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-lg [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1\",\n  {\n    variants: {\n      orientation: {\n        horizontal:\n          \"[&>[data-slot]]:rounded-none [&>[data-slot]~[data-slot]]:border-border [&>[data-slot]~[data-slot]]:border-l\",\n        vertical:\n          \"flex-col [&>[data-slot]]:rounded-none [&>[data-slot]~[data-slot]]:border-border [&>[data-slot]~[data-slot]]:border-t\",\n      },\n    },\n    defaultVariants: {\n      orientation: \"horizontal\",\n    },\n  }\n);\n\nconst actionButtonVariants = cva(\n  \"inline-flex items-center justify-center whitespace-nowrap py-0 font-medium outline-none transition-colors disabled:pointer-events-none disabled:opacity-50\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"bg-background text-muted-foreground hover:bg-muted hover:text-foreground\",\n        destructive:\n          \"bg-background text-destructive hover:bg-destructive/10 hover:text-destructive\",\n        ghost:\n          \"bg-transparent text-muted-foreground hover:bg-muted hover:text-foreground\",\n        outline:\n          \"bg-background text-muted-foreground hover:bg-muted hover:text-foreground\",\n      },\n      connected: {\n        true: \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset\",\n        false:\n          \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      connected: false,\n    },\n  }\n);\n\nconst MotionButton = motion.button;\n\ntype Ripple = { id: string; x: number; y: number; size: number };\n\ntype MotionSafeButtonProps = Omit<\n  React.ButtonHTMLAttributes<HTMLButtonElement>,\n  | \"onAnimationEnd\"\n  | \"onAnimationIteration\"\n  | \"onAnimationStart\"\n  | \"onDrag\"\n  | \"onDragEnd\"\n  | \"onDragStart\"\n>;\n\ntype ButtonGroupChild = React.ReactElement<MotionSafeButtonProps>;\n\nconst rootSizeStyles: Record<ButtonGroupSize, string> = {\n  sm: \"min-h-8 rounded-md px-2.5 text-xs\",\n  md: \"min-h-9 rounded-lg px-3 text-sm\",\n  lg: \"min-h-10 rounded-lg px-4 text-sm\",\n};\n\nconst connectedRootSizeStyles: Record<ButtonGroupSize, string> = {\n  sm: \"min-h-8 px-2.5 text-xs\",\n  md: \"min-h-9 px-3 text-sm\",\n  lg: \"min-h-10 px-4 text-sm\",\n};\n\nconst iconSizeStyles: Record<ButtonGroupSize, string> = {\n  sm: \"size-8 rounded-md\",\n  md: \"size-9 rounded-lg\",\n  lg: \"size-10 rounded-lg\",\n};\n\nconst connectedIconSizeStyles: Record<ButtonGroupSize, string> = {\n  sm: \"size-8\",\n  md: \"size-9\",\n  lg: \"size-10\",\n};\n\nconst textSizeStyles: Record<ButtonGroupSize, string> = {\n  sm: \"px-2 text-xs\",\n  md: \"px-2.5 text-sm\",\n  lg: \"px-3 text-sm\",\n};\n\nconst groupItemSizeStyles: Record<ButtonGroupSize, string> = {\n  sm: \"min-h-8 min-w-8 px-2.5 text-xs\",\n  md: \"min-h-9 min-w-9 px-3 text-sm\",\n  lg: \"min-h-10 min-w-10 px-4 text-sm\",\n};\n\nconst segmentedShellSizeStyles: Record<\n  ButtonGroupSize,\n  { horizontal: string; vertical: string }\n> = {\n  sm: {\n    horizontal: \"gap-0.5 p-0.5\",\n    vertical: \"gap-0.5 p-0.5\",\n  },\n  md: {\n    horizontal: \"gap-0.5 p-0.5\",\n    vertical: \"gap-0.5 p-0.5\",\n  },\n  lg: {\n    horizontal: \"gap-1 p-1\",\n    vertical: \"gap-1 p-1\",\n  },\n};\n\nconst segmentedItemSizeStyles: Record<ButtonGroupSize, string> = {\n  sm: \"min-h-7 flex-1 px-2.5 text-xs\",\n  md: \"min-h-8 flex-1 px-3 text-sm\",\n  lg: \"min-h-9 flex-1 px-4 text-sm\",\n};\n\nconst segmentedIndicatorTransition = {\n  type: \"spring\" as const,\n  stiffness: 170,\n  damping: 21,\n  mass: 0.9,\n  bounce: 0,\n};\n\nconst contentSizeStyles: Record<ButtonGroupSize, string> = {\n  sm: \"gap-1.5 [&_svg]:size-3.5 [&_svg]:shrink-0\",\n  md: \"gap-1.5 [&_svg]:size-4 [&_svg]:shrink-0\",\n  lg: \"gap-2 [&_svg]:size-4 [&_svg]:shrink-0\",\n};\n\ninterface ButtonProps extends MotionSafeButtonProps {\n  children: React.ReactNode;\n  disableRipple?: boolean;\n  showBorder?: boolean;\n  size?: ButtonGroupSize;\n  variant?: ButtonVariant;\n}\n\ninterface RippleButtonProps extends MotionSafeButtonProps {\n  children: React.ReactNode;\n  className?: string;\n  contentClassName?: string;\n  disableRipple?: boolean;\n  themed?: boolean;\n}\n\nfunction getButtonGroupItemKey(child: ButtonGroupChild, index: number) {\n  if (child.key != null) {\n    return child.key;\n  }\n\n  const { children, name, value } = child.props;\n  const ariaLabel = child.props[\"aria-label\"];\n\n  if (typeof value === \"string\" || typeof value === \"number\") {\n    return `value-${value}-${index}`;\n  }\n\n  if (typeof name === \"string\" && name.length > 0) {\n    return `name-${name}-${index}`;\n  }\n\n  if (typeof ariaLabel === \"string\" && ariaLabel.length > 0) {\n    return `label-${ariaLabel}-${index}`;\n  }\n\n  if (typeof children === \"string\" && children.trim().length > 0) {\n    return `text-${children.trim()}-${index}`;\n  }\n\n  return `button-group-item-${index}`;\n}\n\nconst RippleButton = React.forwardRef<HTMLButtonElement, RippleButtonProps>(\n  (\n    {\n      className,\n      children,\n      contentClassName,\n      disabled,\n      disableRipple,\n      onPointerDown,\n      themed = true,\n      type = \"button\",\n      ...props\n    },\n    ref\n  ) => {\n    const [ripples, setRipples] = React.useState<Ripple[]>([]);\n\n    const handlePointerDown = (e: React.PointerEvent<HTMLButtonElement>) => {\n      onPointerDown?.(e);\n\n      if (disabled || disableRipple || e.button !== 0) {\n        return;\n      }\n\n      const rect = e.currentTarget.getBoundingClientRect();\n      const x = e.clientX - rect.left;\n      const y = e.clientY - rect.top;\n      const size =\n        2 *\n        Math.max(\n          Math.hypot(x, y),\n          Math.hypot(rect.width - x, y),\n          Math.hypot(x, rect.height - y),\n          Math.hypot(rect.width - x, rect.height - y)\n        );\n      const id =\n        typeof crypto !== \"undefined\" && crypto.randomUUID\n          ? crypto.randomUUID()\n          : `${Date.now()}-${Math.random()}`;\n\n      setRipples((current) => [...current, { id, x, y, size }]);\n    };\n\n    return (\n      <MotionButton\n        className={cn(\n          themed && componentThemeClassName,\n          \"relative cursor-pointer touch-manipulation select-none overflow-hidden\",\n          className\n        )}\n        data-slot=\"button\"\n        disabled={disabled}\n        onPointerDown={handlePointerDown}\n        ref={ref}\n        type={type}\n        {...props}\n      >\n        <span\n          aria-hidden\n          className=\"pointer-events-none absolute inset-0 z-0 overflow-hidden rounded-[inherit]\"\n        >\n          {ripples.map((ripple) => (\n            <motion.span\n              animate={{ opacity: 0, scale: 1 }}\n              className=\"pointer-events-none absolute -translate-x-1/2 -translate-y-1/2 rounded-full bg-current\"\n              initial={{ opacity: 0.16, scale: 0 }}\n              key={ripple.id}\n              onAnimationComplete={() => {\n                setRipples((current) =>\n                  current.filter((item) => item.id !== ripple.id)\n                );\n              }}\n              style={{\n                height: ripple.size,\n                left: ripple.x,\n                top: ripple.y,\n                width: ripple.size,\n              }}\n              transition={{\n                duration: 0.55,\n                ease: [0.22, 1, 0.36, 1],\n              }}\n            />\n          ))}\n        </span>\n        <span\n          className={cn(\n            \"relative z-10 inline-flex items-center gap-2\",\n            contentClassName\n          )}\n        >\n          {children}\n        </span>\n      </MotionButton>\n    );\n  }\n);\nRippleButton.displayName = \"RippleButton\";\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  (\n    {\n      className,\n      children,\n      disableRipple = false,\n      showBorder = true,\n      size,\n      variant = \"default\",\n      disabled,\n      ...props\n    },\n    ref\n  ) => {\n    const groupContext = useButtonGroupContext();\n    const resolvedSize = resolveButtonGroupSize(size, groupContext);\n    const connected = groupContext?.connected ?? false;\n    const showStandaloneBorder =\n      !connected && variant !== \"ghost\" && showBorder;\n\n    return (\n      <RippleButton\n        className={cn(\n          actionButtonVariants({\n            variant,\n            connected,\n          }),\n          showStandaloneBorder && \"border border-border\",\n          connected\n            ? connectedRootSizeStyles[resolvedSize]\n            : rootSizeStyles[resolvedSize],\n          className\n        )}\n        contentClassName={contentSizeStyles[resolvedSize]}\n        disabled={disabled}\n        disableRipple={disableRipple}\n        ref={ref}\n        themed={shouldApplyTheme(groupContext)}\n        {...props}\n      >\n        {children}\n      </RippleButton>\n    );\n  }\n);\nButton.displayName = \"Button\";\n\ninterface IconButtonProps extends MotionSafeButtonProps {\n  children: React.ReactNode;\n  disableRipple?: boolean;\n  showBorder?: boolean;\n  size?: ButtonGroupSize;\n  variant?: ButtonVariant;\n}\n\nconst IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(\n  (\n    {\n      className,\n      children,\n      disableRipple = false,\n      showBorder = true,\n      size,\n      variant = \"default\",\n      disabled,\n      ...props\n    },\n    ref\n  ) => {\n    const groupContext = useButtonGroupContext();\n    const resolvedSize = resolveButtonGroupSize(size, groupContext);\n    const connected = groupContext?.connected ?? false;\n    const showStandaloneBorder =\n      !connected && variant !== \"ghost\" && showBorder;\n\n    return (\n      <RippleButton\n        className={cn(\n          actionButtonVariants({\n            variant,\n            connected,\n          }),\n          showStandaloneBorder && \"border border-border\",\n          connected\n            ? connectedIconSizeStyles[resolvedSize]\n            : iconSizeStyles[resolvedSize],\n          className\n        )}\n        contentClassName={contentSizeStyles[resolvedSize]}\n        disabled={disabled}\n        disableRipple={disableRipple}\n        ref={ref}\n        themed={shouldApplyTheme(groupContext)}\n        {...props}\n      >\n        {children}\n      </RippleButton>\n    );\n  }\n);\nIconButton.displayName = \"IconButton\";\n\ntype ButtonGroupProps = React.ComponentProps<\"div\"> &\n  VariantProps<typeof buttonGroupVariants> & {\n    size?: ButtonGroupSize;\n  };\n\nfunction ButtonGroup({\n  className,\n  orientation,\n  role = \"group\",\n  size = \"md\",\n  children,\n  ...props\n}: ButtonGroupProps) {\n  const contextValue = React.useMemo(\n    () => ({\n      connected: true,\n      size,\n    }),\n    [size]\n  );\n\n  return (\n    <ButtonGroupContext.Provider value={contextValue}>\n      <div\n        className={cn(\n          componentThemeClassName,\n          buttonGroupVariants({ orientation }),\n          \"*:focus-visible:relative *:focus-visible:z-10\",\n          className\n        )}\n        data-orientation={orientation ?? \"horizontal\"}\n        data-slot=\"button-group\"\n        role={role}\n        {...props}\n      >\n        {children}\n      </div>\n    </ButtonGroupContext.Provider>\n  );\n}\n\nfunction ButtonGroupText({\n  className,\n  render,\n  ...props\n}: useRender.ComponentProps<\"div\">) {\n  const groupContext = useButtonGroupContext();\n  const resolvedSize = resolveButtonGroupSize(undefined, groupContext);\n  const connected = groupContext?.connected ?? false;\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(\n      {\n        className: cn(\n          shouldApplyTheme(groupContext) && componentThemeClassName,\n          \"flex items-center gap-2 font-medium text-muted-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none\",\n          connected\n            ? \"rounded-none border-0 bg-muted\"\n            : \"rounded-lg border border-border bg-muted\",\n          textSizeStyles[resolvedSize],\n          className\n        ),\n      },\n      {\n        ...props,\n        \"data-slot\": \"button-group-text\",\n      } as typeof props\n    ),\n    render,\n    state: {\n      slot: \"button-group-text\",\n    },\n  });\n}\n\nfunction ButtonGroupSeparator({\n  className,\n  orientation = \"vertical\",\n  ...props\n}: React.ComponentProps<typeof Separator>) {\n  return (\n    <Separator\n      className={cn(\n        \"relative self-stretch bg-input data-horizontal:mx-px data-vertical:my-px data-vertical:h-auto data-horizontal:w-auto\",\n        className\n      )}\n      data-horizontal={orientation === \"horizontal\" ? \"\" : undefined}\n      data-orientation={orientation}\n      data-slot=\"button-group-separator\"\n      data-vertical={orientation === \"vertical\" ? \"\" : undefined}\n      orientation={orientation}\n      {...props}\n    />\n  );\n}\n\ninterface ButtonGroupItemsProps extends React.HTMLAttributes<HTMLDivElement> {\n  children: React.ReactNode;\n  disableRipple?: boolean;\n  showDividers?: boolean;\n  size?: ButtonGroupSize;\n}\n\ntype HoverFrame = {\n  height: number;\n  width: number;\n  x: number;\n  y: number;\n};\n\nfunction ButtonGroupItems({\n  children,\n  className,\n  disableRipple = false,\n  onPointerLeave,\n  role = \"group\",\n  showDividers = true,\n  size: sizeProp,\n  ...props\n}: ButtonGroupItemsProps) {\n  const groupContext = useButtonGroupContext();\n  const resolvedSize = resolveButtonGroupSize(sizeProp, groupContext);\n  const connected = groupContext?.connected ?? false;\n  const [activeHoverIndex, setActiveHoverIndex] = React.useState<number | null>(\n    null\n  );\n  const [hoverFrame, setHoverFrame] = React.useState<HoverFrame | null>(null);\n  const groupRef = React.useRef<HTMLDivElement | null>(null);\n  const itemRefs = React.useRef<Array<HTMLButtonElement | null>>([]);\n  const childArray = React.Children.toArray(children).filter(\n    (child): child is ButtonGroupChild =>\n      React.isValidElement<MotionSafeButtonProps>(child)\n  );\n  const usesSharedHover = !showDividers;\n\n  const updateHoverFrame = React.useCallback((index: number) => {\n    const group = groupRef.current;\n    const node = itemRefs.current[index];\n\n    if (!(group && node)) {\n      return;\n    }\n\n    const groupRect = group.getBoundingClientRect();\n    const nodeRect = node.getBoundingClientRect();\n    const next = {\n      height: nodeRect.height,\n      width: nodeRect.width,\n      x: nodeRect.left - groupRect.left,\n      y: nodeRect.top - groupRect.top,\n    };\n\n    setHoverFrame((current) => {\n      if (\n        current?.height === next.height &&\n        current?.width === next.width &&\n        current?.x === next.x &&\n        current?.y === next.y\n      ) {\n        return current;\n      }\n\n      return next;\n    });\n  }, []);\n\n  React.useEffect(() => {\n    if (process.env.NODE_ENV === \"production\") {\n      return;\n    }\n\n    const allChildren = React.Children.toArray(children);\n\n    if (allChildren.length === 0) {\n      console.warn(\"[ButtonGroupItems] Pass at least one button-like child.\");\n      return;\n    }\n\n    if (childArray.length !== allChildren.length) {\n      console.warn(\n        \"[ButtonGroupItems] Only valid button-like React elements are rendered. Non-element children are ignored.\"\n      );\n    }\n  }, [childArray.length, children]);\n\n  React.useEffect(() => {\n    if (!usesSharedHover || activeHoverIndex === null) {\n      return;\n    }\n\n    const group = groupRef.current;\n    const activeNode = itemRefs.current[activeHoverIndex];\n\n    if (!group) {\n      return;\n    }\n\n    const handleMeasure = () => {\n      updateHoverFrame(activeHoverIndex);\n    };\n\n    const resizeObserver = new ResizeObserver(handleMeasure);\n    resizeObserver.observe(group);\n\n    if (activeNode) {\n      resizeObserver.observe(activeNode);\n    }\n\n    window.addEventListener(\"resize\", handleMeasure);\n\n    return () => {\n      resizeObserver.disconnect();\n      window.removeEventListener(\"resize\", handleMeasure);\n    };\n  }, [activeHoverIndex, updateHoverFrame, usesSharedHover]);\n\n  if (childArray.length === 0) {\n    return null;\n  }\n\n  return (\n    <div\n      className={cn(\n        shouldApplyTheme(groupContext) && componentThemeClassName,\n        \"relative inline-flex items-stretch bg-background\",\n        connected\n          ? \"rounded-none border-0\"\n          : [\"rounded-lg\", showDividers && \"border border-border\"],\n        className\n      )}\n      data-slot=\"button-group-items\"\n      onPointerLeave={(event) => {\n        onPointerLeave?.(event);\n\n        if (usesSharedHover) {\n          setActiveHoverIndex(null);\n          setHoverFrame(null);\n        }\n      }}\n      ref={groupRef}\n      role={role}\n      {...props}\n    >\n      {usesSharedHover && hoverFrame ? (\n        <motion.span\n          animate={{\n            height: hoverFrame.height,\n            left: hoverFrame.x,\n            opacity: 1,\n            top: hoverFrame.y,\n            width: hoverFrame.width,\n          }}\n          aria-hidden\n          className=\"pointer-events-none absolute top-0 left-0 z-0 rounded-md bg-muted/45\"\n          initial={false}\n          transition={{\n            type: \"spring\",\n            stiffness: 420,\n            damping: 34,\n            mass: 0.8,\n          }}\n        />\n      ) : null}\n      {childArray.map((child, index) => {\n        const itemKey = getButtonGroupItemKey(child, index);\n        const isFirst = index === 0;\n        const isLast = index === childArray.length - 1;\n        const {\n          children: childChildren,\n          className: childClassName,\n          onBlur: childOnBlur,\n          onFocus: childOnFocus,\n          onPointerEnter: childOnPointerEnter,\n          onPointerLeave: childOnPointerLeave,\n          ...childProps\n        } = child.props;\n        const showSharedHover = usesSharedHover && activeHoverIndex === index;\n\n        return (\n          <RippleButton\n            className={cn(\n              \"relative inline-flex items-center justify-center whitespace-nowrap py-0 font-medium text-muted-foreground transition-colors hover:text-foreground\",\n              \"outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset\",\n              \"disabled:pointer-events-none disabled:opacity-50\",\n              groupItemSizeStyles[resolvedSize],\n              showDividers\n                ? [isFirst && \"rounded-l-[7px]\", isLast && \"rounded-r-[7px]\"]\n                : \"rounded-md\",\n              showSharedHover && \"text-foreground\",\n              !(usesSharedHover || showDividers) && \"hover:bg-muted\",\n              showDividers && \"hover:bg-muted\",\n              showDividers && !isLast && \"border-border border-r\",\n              childClassName\n            )}\n            contentClassName={contentSizeStyles[resolvedSize]}\n            disableRipple={disableRipple}\n            key={itemKey}\n            onBlur={(event) => {\n              childOnBlur?.(event);\n\n              if (\n                groupRef.current?.contains(event.relatedTarget as Node | null)\n              ) {\n                return;\n              }\n\n              setActiveHoverIndex((current) =>\n                current === index ? null : current\n              );\n              setHoverFrame(null);\n            }}\n            onFocus={(event) => {\n              childOnFocus?.(event);\n\n              if (usesSharedHover) {\n                setActiveHoverIndex(index);\n                updateHoverFrame(index);\n              }\n            }}\n            onPointerEnter={(event) => {\n              childOnPointerEnter?.(event);\n\n              if (usesSharedHover) {\n                setActiveHoverIndex(index);\n                updateHoverFrame(index);\n              }\n            }}\n            onPointerLeave={childOnPointerLeave}\n            ref={(node) => {\n              itemRefs.current[index] = node;\n            }}\n            themed={false}\n            {...childProps}\n          >\n            {childChildren}\n          </RippleButton>\n        );\n      })}\n    </div>\n  );\n}\n\ntype SegmentedControlOptionObject = {\n  value: string;\n  label?: React.ReactNode;\n  disabled?: boolean;\n  icon?: React.ReactNode;\n};\n\ntype SegmentedControlOption = string | SegmentedControlOptionObject;\n\ntype NormalizedSegmentedOption = {\n  value: string;\n  label: React.ReactNode;\n  disabled?: boolean;\n  icon?: React.ReactNode;\n};\n\ninterface SegmentedControlProps {\n  options: SegmentedControlOption[];\n  value?: string;\n  onChange?: (value: string) => void;\n  ariaLabel?: string;\n  ariaLabelledBy?: string;\n  className?: string;\n  disabled?: boolean;\n  layoutId?: string;\n  name?: string;\n  orientation?: \"horizontal\" | \"vertical\";\n  size?: ButtonGroupSize;\n}\n\nfunction normalizeSegmentedOptions(\n  options: SegmentedControlOption[]\n): NormalizedSegmentedOption[] {\n  return options.map((option) => {\n    if (typeof option === \"string\") {\n      return {\n        value: option,\n        label: option,\n      };\n    }\n\n    return {\n      value: option.value,\n      label: option.label ?? option.value,\n      disabled: option.disabled,\n      icon: option.icon,\n    };\n  });\n}\n\nfunction getEnabledOptionIndex(\n  options: NormalizedSegmentedOption[],\n  startIndex: number,\n  direction: 1 | -1\n) {\n  if (options.length === 0) {\n    return -1;\n  }\n\n  let index = startIndex;\n\n  for (const _option of options) {\n    index = (index + direction + options.length) % options.length;\n\n    if (!options[index]?.disabled) {\n      return index;\n    }\n  }\n\n  return -1;\n}\n\nfunction isRtlElement(element: HTMLElement | null) {\n  if (!element) {\n    return false;\n  }\n\n  const dir = window.getComputedStyle(element).direction;\n\n  return dir === \"rtl\";\n}\n\nfunction SegmentedControl({\n  options,\n  value,\n  onChange,\n  ariaLabel,\n  ariaLabelledBy,\n  className,\n  disabled = false,\n  layoutId,\n  name,\n  orientation = \"horizontal\",\n  size = \"md\",\n}: SegmentedControlProps) {\n  const instanceId = React.useId();\n  const resolvedLayoutId = layoutId ?? `segmented-indicator-${instanceId}`;\n  const normalizedOptions = React.useMemo(\n    () => normalizeSegmentedOptions(options),\n    [options]\n  );\n  const initialValue = normalizedOptions.find(\n    (option) => !option.disabled\n  )?.value;\n  const [selected, setSelected] = React.useState(value ?? initialValue ?? \"\");\n  const [isHovered, setIsHovered] = React.useState<string | null>(null);\n  const buttonRefs = React.useRef<Array<HTMLButtonElement | null>>([]);\n  const groupRef = React.useRef<HTMLDivElement | null>(null);\n  const resolvedSelected =\n    normalizedOptions.find(\n      (option) => option.value === selected && !option.disabled\n    )?.value ??\n    normalizedOptions.find((option) => !option.disabled)?.value ??\n    \"\";\n\n  React.useEffect(() => {\n    if (process.env.NODE_ENV === \"production\") {\n      return;\n    }\n\n    if (!(ariaLabel || ariaLabelledBy)) {\n      console.warn(\n        \"[SegmentedControl] Provide ariaLabel or ariaLabelledBy for accessibility.\"\n      );\n    }\n\n    const values = normalizedOptions.map((option) => option.value);\n    const uniqueValues = new Set(values);\n\n    if (uniqueValues.size !== values.length) {\n      console.warn(\n        \"[SegmentedControl] Option values must be unique. Duplicate values can break selection.\"\n      );\n    }\n  }, [ariaLabel, ariaLabelledBy, normalizedOptions]);\n\n  React.useEffect(() => {\n    buttonRefs.current.length = normalizedOptions.length;\n  }, [normalizedOptions.length]);\n\n  React.useEffect(() => {\n    if (value !== undefined) {\n      const next =\n        normalizedOptions.find(\n          (option) => option.value === value && !option.disabled\n        )?.value ??\n        normalizedOptions.find((option) => !option.disabled)?.value ??\n        \"\";\n\n      setSelected((current) => (current === next ? current : next));\n      return;\n    }\n\n    setSelected((current) => {\n      const stillValid = normalizedOptions.some(\n        (option) => option.value === current && !option.disabled\n      );\n\n      if (stillValid) {\n        return current;\n      }\n\n      return normalizedOptions.find((option) => !option.disabled)?.value ?? \"\";\n    });\n  }, [normalizedOptions, value]);\n\n  const handleSelect = (nextValue: string) => {\n    const option = normalizedOptions.find((item) => item.value === nextValue);\n\n    if (!option || option.disabled || disabled) {\n      return;\n    }\n\n    if (nextValue === resolvedSelected) {\n      return;\n    }\n\n    setSelected(nextValue);\n    onChange?.(nextValue);\n  };\n\n  const focusOption = (index: number) => {\n    buttonRefs.current[index]?.focus();\n  };\n\n  const moveSelection = (index: number) => {\n    const option = normalizedOptions[index];\n\n    if (!option || option.disabled) {\n      return;\n    }\n\n    handleSelect(option.value);\n    focusOption(index);\n  };\n\n  const moveByDirection = (index: number, direction: 1 | -1) => {\n    const nextIndex = getEnabledOptionIndex(\n      normalizedOptions,\n      index,\n      direction\n    );\n\n    if (nextIndex === -1) {\n      return;\n    }\n\n    moveSelection(nextIndex);\n  };\n\n  const handleKeyDown = (\n    event: React.KeyboardEvent<HTMLButtonElement>,\n    index: number\n  ) => {\n    const rtl = isRtlElement(groupRef.current);\n    const horizontalNext = rtl ? -1 : 1;\n    const horizontalPrevious = rtl ? 1 : -1;\n\n    switch (event.key) {\n      case \"ArrowRight\": {\n        event.preventDefault();\n        moveByDirection(\n          index,\n          orientation === \"horizontal\" ? horizontalNext : 1\n        );\n        break;\n      }\n      case \"ArrowLeft\": {\n        event.preventDefault();\n        moveByDirection(\n          index,\n          orientation === \"horizontal\" ? horizontalPrevious : -1\n        );\n        break;\n      }\n      case \"ArrowDown\": {\n        event.preventDefault();\n        moveByDirection(index, orientation === \"vertical\" ? 1 : horizontalNext);\n        break;\n      }\n      case \"ArrowUp\": {\n        event.preventDefault();\n        moveByDirection(\n          index,\n          orientation === \"vertical\" ? -1 : horizontalPrevious\n        );\n        break;\n      }\n      case \"Home\": {\n        event.preventDefault();\n        moveSelection(getEnabledOptionIndex(normalizedOptions, -1, 1));\n        break;\n      }\n      case \"End\": {\n        event.preventDefault();\n        moveSelection(getEnabledOptionIndex(normalizedOptions, 0, -1));\n        break;\n      }\n      default:\n        break;\n    }\n  };\n\n  if (normalizedOptions.length === 0) {\n    return null;\n  }\n\n  return (\n    <div\n      aria-disabled={disabled || undefined}\n      aria-label={ariaLabel}\n      aria-labelledby={ariaLabelledBy}\n      aria-orientation={orientation}\n      className={cn(\n        componentThemeClassName,\n        \"relative inline-flex items-stretch rounded-lg border border-border bg-background\",\n        orientation === \"vertical\" ? \"flex-col\" : \"flex-row\",\n        segmentedShellSizeStyles[size][orientation],\n        disabled && \"pointer-events-none opacity-60\",\n        className\n      )}\n      ref={groupRef}\n      role=\"radiogroup\"\n    >\n      {name && !disabled ? (\n        <input name={name} type=\"hidden\" value={resolvedSelected} />\n      ) : null}\n      <LayoutGroup id={resolvedLayoutId}>\n        {normalizedOptions.map((option, index) => {\n          const isSelected = resolvedSelected === option.value;\n          const showHover = isHovered === option.value && !isSelected;\n          const isOptionDisabled = disabled || option.disabled;\n\n          return (\n            <motion.button\n              aria-checked={isSelected}\n              aria-disabled={isOptionDisabled || undefined}\n              className={cn(\n                \"relative z-10 inline-flex cursor-pointer touch-manipulation select-none items-center justify-center whitespace-nowrap rounded-md py-0 font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset\",\n                segmentedItemSizeStyles[size],\n                isSelected ? \"text-foreground\" : \"text-muted-foreground\",\n                isOptionDisabled && \"cursor-not-allowed opacity-50\"\n              )}\n              disabled={isOptionDisabled}\n              key={option.value}\n              onClick={() => handleSelect(option.value)}\n              onKeyDown={(event) => handleKeyDown(event, index)}\n              onPointerEnter={() => {\n                if (!isOptionDisabled) {\n                  setIsHovered(option.value);\n                }\n              }}\n              onPointerLeave={() =>\n                setIsHovered((current) =>\n                  current === option.value ? null : current\n                )\n              }\n              ref={(node) => {\n                buttonRefs.current[index] = node;\n              }}\n              role=\"radio\"\n              tabIndex={isSelected && !isOptionDisabled ? 0 : -1}\n              type=\"button\"\n              whileTap={isOptionDisabled ? undefined : { scale: 0.96 }}\n            >\n              {isSelected ? (\n                <motion.span\n                  aria-hidden\n                  className=\"absolute inset-0 z-0 rounded-md bg-accent will-change-[transform,width,height]\"\n                  initial={false}\n                  layoutId={resolvedLayoutId}\n                  transition={segmentedIndicatorTransition}\n                />\n              ) : null}\n              <motion.span\n                animate={{\n                  opacity: showHover ? 1 : 0,\n                }}\n                aria-hidden\n                className=\"pointer-events-none absolute inset-0 z-0 rounded-md bg-muted/40\"\n                initial={false}\n                transition={{\n                  duration: 0.24,\n                  ease: [0.22, 1, 0.36, 1],\n                }}\n              />\n              <motion.span\n                animate={{\n                  opacity: isSelected ? 1 : 0.72,\n                }}\n                className=\"relative z-10 inline-flex items-center gap-1.5\"\n                transition={{\n                  duration: 0.28,\n                  ease: [0.22, 1, 0.36, 1],\n                }}\n              >\n                {option.icon}\n                {option.label}\n              </motion.span>\n            </motion.button>\n          );\n        })}\n      </LayoutGroup>\n    </div>\n  );\n}\n\nexport {\n  Button,\n  IconButton,\n  ButtonGroup,\n  ButtonGroupItems,\n  ButtonGroupSeparator,\n  ButtonGroupText,\n  SegmentedControl,\n  actionButtonVariants,\n  buttonGroupVariants,\n};\n\nexport type {\n  ButtonGroupSize,\n  ButtonVariant,\n  ButtonProps,\n  IconButtonProps,\n  ButtonGroupProps,\n  ButtonGroupItemsProps,\n  SegmentedControlProps,\n  SegmentedControlOption,\n  SegmentedControlOptionObject,\n};\n",
      "type": "registry:ui"
    }
  ],
  "title": "Button Group",
  "description": "Grouped action buttons, text segments, separators, segmented button rows, and a selectable segmented control with spring-driven hover and active motion."
}
