{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "floating-button-group",
  "type": "registry:ui",
  "title": "Floating button group",
  "description": "Separate soft buttons in a quiet tray, with one shared highlight that morphs from button to button as you move, and a pressed state that settles in place.",
  "categories": [
    "actions"
  ],
  "dependencies": [
    "@radix-ui/react-tooltip",
    "motion"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/motion-tokens.json",
    "https://irisui.dev/r/tooltip.json",
    "https://irisui.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "src/registry/components/floating-button-group/floating-button-group.tsx",
      "content": "\"use client\";\n\nimport { Fragment, useCallback, useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport type { CSSProperties, FocusEvent, KeyboardEvent, ReactElement, ReactNode } from \"react\";\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\nimport { AnimatePresence, animate, motion, useIsPresent, useMotionValue, useReducedMotion } from \"motion/react\";\nimport type { TargetAndTransition } from \"motion/react\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport { Tooltip } from \"../tooltip/tooltip\";\nimport { cn } from \"../lib/utils\";\n\nexport type FloatingButtonGroupVariant = \"muted\" | \"floating\";\nexport type FloatingButtonGroupSize = \"sm\" | \"md\";\nexport type FloatingButtonGroupOrientation = \"horizontal\" | \"vertical\";\n\nexport interface FloatingButtonGroupAction {\n  type?: \"action\";\n  /** Stable key, also passed to onAction. */\n  id: string;\n  /** Visible label, and the accessible name when the item shows only its icon. */\n  label: string;\n  icon?: ReactNode;\n  /** Shows only the icon. The label moves into a tooltip and the accessible name. Overrides the group's iconOnly. */\n  iconOnly?: boolean;\n  onSelect?: () => void;\n  disabled?: boolean;\n  /** Marks an action that switches a mode on, such as Present. Sets aria-pressed and keeps a quiet tint while on. */\n  pressed?: boolean;\n  /** A shortcut hint shown in the tooltip, such as \"⌘Z\". The component does not bind the key. */\n  shortcut?: string;\n  /** Other labels this item can switch to, such as [\"Copied\"] for Share. The button keeps the width of the widest, so a label change never moves anything. */\n  reserveLabels?: readonly string[];\n}\n\nexport interface FloatingButtonGroupSeparator {\n  type: \"separator\";\n  id?: string;\n}\n\nexport type FloatingButtonGroupItem = FloatingButtonGroupAction | FloatingButtonGroupSeparator;\n\nexport interface FloatingButtonGroupProps {\n  items: readonly FloatingButtonGroupItem[];\n  /** Accessible name of the toolbar, such as \"Board actions\". */\n  label: string;\n  /** muted sits quietly on a page; floating raises the tray with the floating shadow for overlays such as a selection toolbar. */\n  variant?: FloatingButtonGroupVariant;\n  size?: FloatingButtonGroupSize;\n  orientation?: FloatingButtonGroupOrientation;\n  /** Shows every item as an icon only, with its label in a tooltip. Items with their own iconOnly win. */\n  iconOnly?: boolean;\n  /** Side of the item tooltips. Defaults to top for a row and right for a column. */\n  tooltipSide?: \"top\" | \"bottom\" | \"left\" | \"right\";\n  /** Called with the item id after the item's own onSelect. */\n  onAction?: (id: string) => void;\n  className?: string;\n  style?: CSSProperties;\n}\n\nconst isAction = (item: FloatingButtonGroupItem): item is FloatingButtonGroupAction => item.type !== \"separator\";\nconst standard = [...motionTokens.ease.standard] as [number, number, number, number];\nconst enter = [...motionTokens.ease.enter] as [number, number, number, number];\n\nconst rest: TargetAndTransition = { opacity: 1, y: 0, filter: \"blur(0px)\" };\nconst textIn: TargetAndTransition = { opacity: 0, y: 4, filter: `blur(${motionTokens.blur.soft}px)` };\nconst textOut: TargetAndTransition = {\n  opacity: 0,\n  y: -3,\n  filter: `blur(${motionTokens.blur.soft}px)`,\n  transition: { duration: motionTokens.duration.fast, ease: standard },\n};\nconst fadeIn: TargetAndTransition = { opacity: 0 };\nconst fadeOut: TargetAndTransition = { opacity: 0, transition: { duration: motionTokens.duration.instant } };\n\n/* Separate soft buttons in a quiet tray, with real gaps between them. The tray always keeps its natural size and never wraps or\n   clips its buttons. The buttons stay transparent: one shared highlight moves underneath their content, so there is only ever one\n   moving surface. Corners are concentric with the tray. The muted tray is a translucent ink tint, not the hover colour, so it\n   still reads when it sits on a muted surface. */\nconst groupClass =\n  \"group/fbg relative isolate inline-flex flex-none flex-nowrap items-center gap-(--fbg-gap) rounded-(--fbg-outer) border border-(--fbg-tray-edge) bg-(--fbg-tray) p-(--fbg-pad) font-body tracking-body text-ink-2 [-webkit-tap-highlight-color:transparent] [--fbg-gap:4px] [--fbg-h:36px] [--fbg-highlight-press:color-mix(in_oklab,var(--ink)_12%,transparent)] [--fbg-highlight:color-mix(in_oklab,var(--ink)_7%,transparent)] [--fbg-on:color-mix(in_oklab,var(--control-on)_12%,transparent)] [--fbg-outer:var(--radius-button)] [--fbg-pad-x:12px] [--fbg-pad:4px] [--fbg-radius:calc(var(--fbg-outer)-var(--fbg-pad)-1px)] [--fbg-rule:color-mix(in_oklab,var(--ink)_12%,transparent)] [--fbg-tray-edge:color-mix(in_oklab,var(--ink)_3%,transparent)] [--fbg-tray:color-mix(in_oklab,var(--ink)_4.5%,transparent)] dark:[--fbg-highlight-press:color-mix(in_oklab,var(--ink)_15%,transparent)] dark:[--fbg-highlight:color-mix(in_oklab,var(--ink)_9%,transparent)] dark:[--fbg-on:color-mix(in_oklab,var(--control-on)_16%,transparent)] dark:[--fbg-tray-edge:color-mix(in_oklab,var(--ink)_5%,transparent)] dark:[--fbg-tray:color-mix(in_oklab,var(--ink)_6%,transparent)]\";\nconst groupSizes: Record<FloatingButtonGroupSize, string> = {\n  sm: \"[--fbg-gap:3px] [--fbg-h:30px] [--fbg-outer:var(--radius-button)] [--fbg-pad-x:10px] [--fbg-pad:3px]\",\n  md: \"[--fbg-h:36px]\",\n};\n/* Raised for overlays such as a selection toolbar: the floating layer shadow and a hairline edge. */\nconst groupVariants: Record<FloatingButtonGroupVariant, string> = {\n  muted: \"\",\n  floating: \"border-line bg-surface shadow-floating\",\n};\n/* The highlight paints above the buttons' own backgrounds (a pressed tint) and below their content. */\nconst highlightClass =\n  \"pointer-events-none absolute top-0 left-0 z-1 rounded-(--fbg-radius) bg-(--fbg-highlight) transition-[background-color] duration-fast ease-standard data-pressing:bg-(--fbg-highlight-press) data-pressing:duration-instant motion-reduce:[transition:color_var(--duration-instant)_linear,background-color_var(--duration-instant)_linear,opacity_var(--duration-instant)_linear]\";\n/* Buttons are not positioned, so the highlight measures them against the tray and stacks above their background. */\nconst itemClass =\n  \"m-0 inline-flex h-(--fbg-h) min-w-(--fbg-h) flex-none cursor-pointer touch-manipulation items-center justify-center rounded-(--fbg-radius) border-0 bg-transparent px-(--fbg-pad-x) leading-none font-medium whitespace-nowrap text-inherit select-none [-webkit-tap-highlight-color:transparent] [transition:color_var(--duration-fast)_var(--ease-standard),background-color_var(--duration-fast)_var(--ease-standard),opacity_var(--duration-fast)_var(--ease-standard)] aria-disabled:cursor-not-allowed aria-disabled:text-ink-3 aria-disabled:opacity-50 aria-pressed:bg-(--fbg-on) aria-pressed:not-aria-disabled:text-ink data-active:not-aria-disabled:text-ink data-icon:not-data-icon-only:pl-[calc(var(--fbg-pad-x)-2px)] data-icon-only:w-(--fbg-h) data-icon-only:p-0 group-aria-[orientation=vertical]/fbg:not-data-icon-only:justify-start group-aria-[orientation=vertical]/fbg:data-icon-only:self-center motion-reduce:[transition:color_var(--duration-instant)_linear,background-color_var(--duration-instant)_linear,opacity_var(--duration-instant)_linear]\";\n/* Side tooltips for a column match the library tooltip, rising a few pixels away from the rail instead of up. Radix unmounts\n   when the exit animation ends, so a no-op keyframe (floating-button-group-tip-exit) times the exit while the transition does\n   the visual work. */\nconst sideTipClass =\n  \"z-90 max-w-60 origin-(--radix-tooltip-content-transform-origin) rounded-control border border-[color-mix(in_oklab,var(--page)_14%,var(--ink))] bg-ink px-4 py-3 font-body text-sm leading-body font-normal tracking-body text-page shadow-raised [--tip-x:-3px] [transition:opacity_var(--duration-fast)_var(--ease-enter),transform_var(--duration-fast)_var(--ease-enter)] data-[side=left]:[--tip-x:3px] data-[state$=-open]:starting:opacity-0 data-[state$=-open]:starting:[transform:translateX(var(--tip-x))_scale(.97)] data-[state=closed]:animate-floating-button-group-tip-exit data-[state=closed]:opacity-0 data-[state=closed]:[transform:scale(.98)] data-[state=closed]:[transition:opacity_110ms_var(--ease-standard),transform_110ms_var(--ease-standard)] motion-reduce:[transform:none] motion-reduce:[transition:opacity_90ms_linear] motion-reduce:data-[state=closed]:[transform:none] motion-reduce:data-[state=closed]:[transition:opacity_90ms_linear]\";\n\nfunction Phase({ children, reduced }: { children: ReactNode; reduced: boolean }) {\n  const present = useIsPresent();\n  return (\n    <motion.span\n      className=\"inline-flex items-center gap-1.5 whitespace-nowrap\"\n      aria-hidden={present ? undefined : true}\n      initial={reduced ? fadeIn : textIn}\n      animate={rest}\n      exit={reduced ? fadeOut : textOut}\n      transition={{ duration: reduced ? motionTokens.duration.instant : motionTokens.duration.standard, ease: enter }}\n    >\n      {children}\n    </motion.span>\n  );\n}\n\n/**\n * Icon and label. Every label the item can show is laid out invisibly in the same grid cell, so the button always has the width\n * of the widest one: a label change crossfades in place and never resizes the button, its neighbours or the tray.\n */\nfunction ItemBody({\n  label,\n  icon,\n  iconOnly,\n  reserve,\n  reduced,\n}: {\n  label: string;\n  icon?: ReactNode;\n  iconOnly: boolean;\n  reserve: readonly string[];\n  reduced: boolean;\n}) {\n  const ghosts = iconOnly ? [] : Array.from(new Set([label, ...reserve]));\n  return (\n    <span className=\"relative z-2 inline-grid items-center justify-items-center *:[grid-area:1/1] group-aria-[orientation=vertical]/fbg:justify-items-start\">\n      {ghosts.map((text) => (\n        <span\n          key={text}\n          className=\"pointer-events-none invisible inline-flex items-center gap-1.5 whitespace-nowrap\"\n          aria-hidden=\"true\"\n        >\n          {icon ? <span className=\"block size-4 flex-none\" /> : null}\n          <span className=\"block\">{text}</span>\n        </span>\n      ))}\n      <AnimatePresence initial={false}>\n        <Phase key={label} reduced={reduced}>\n          {icon ? (\n            <span className=\"inline-flex flex-none [&_svg]:size-4 [&_svg]:[stroke-width:1.75]\" aria-hidden=\"true\">\n              {icon}\n            </span>\n          ) : null}\n          {iconOnly ? null : <span className=\"block\">{label}</span>}\n        </Phase>\n      </AnimatePresence>\n    </span>\n  );\n}\n\nfunction TipContent({ label, shortcut }: { label: string; shortcut?: string }) {\n  return (\n    <span className=\"inline-flex items-baseline gap-3 whitespace-nowrap\">\n      {label}\n      {shortcut ? (\n        <kbd className=\"font-[inherit] text-[color-mix(in_oklab,var(--page)_58%,var(--ink))] tabular-nums\">\n          {shortcut}\n        </kbd>\n      ) : null}\n    </span>\n  );\n}\n\n/** Side tooltips for a column. The library Tooltip opens above or below, which would cover the neighbouring button. */\nfunction SideTip({ side, content, children }: { side: \"left\" | \"right\"; content: ReactNode; children: ReactElement }) {\n  return (\n    <TooltipPrimitive.Root>\n      <TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>\n      <TooltipPrimitive.Portal>\n        <TooltipPrimitive.Content className={sideTipClass} side={side} sideOffset={10} collisionPadding={12}>\n          {content}\n        </TooltipPrimitive.Content>\n      </TooltipPrimitive.Portal>\n    </TooltipPrimitive.Root>\n  );\n}\n\n/**\n * Separate soft buttons in a quiet tray. One shared highlight morphs, in position and size, to whichever button the pointer\n * or keyboard focus is on, fades when the pointer leaves the tray, and settles darker in place while a button is pressed.\n * It is the toolbar's only hover and focus indicator. Arrow keys move focus, Home and End jump to the ends, Enter and Space act.\n */\nexport function FloatingButtonGroup({\n  items,\n  label,\n  variant = \"muted\",\n  size = \"md\",\n  orientation = \"horizontal\",\n  iconOnly: groupIconOnly = false,\n  tooltipSide,\n  onAction,\n  className,\n  style,\n}: FloatingButtonGroupProps) {\n  const reduced = useReducedMotion() ?? false;\n  const tray = useRef<HTMLDivElement>(null);\n  const nodes = useRef(new Map<string, HTMLButtonElement>());\n  const [hoverId, setHoverId] = useState<string | null>(null);\n  const [focusId, setFocusId] = useState<string | null>(null);\n  const [pressing, setPressing] = useState(false);\n  const [tabStop, setTabStop] = useState<string | null>(null);\n\n  const actions = items.filter(isAction);\n  const byId = new Map(actions.map((action) => [action.id, action]));\n  const enabled = (id: string | null) => !!id && !!byId.get(id) && !byId.get(id)?.disabled;\n  // Pointer wins over keyboard focus while it is in the tray; a disabled item under the pointer hands back to focus.\n  const activeId = enabled(hoverId) ? hoverId : focusId && byId.has(focusId) ? focusId : null;\n  const stop = enabled(tabStop) ? tabStop : (actions.find((action) => !action.disabled)?.id ?? null);\n  const vertical = orientation === \"vertical\";\n  const side = tooltipSide ?? (vertical ? \"right\" : \"top\");\n  const idsKey = actions.map((action) => action.id).join(\"\\u0000\");\n\n  // The highlight lives in motion values so moving it never re-renders the buttons.\n  const x = useMotionValue(0),\n    y = useMotionValue(0),\n    width = useMotionValue(0),\n    height = useMotionValue(0);\n  const opacity = useMotionValue(0),\n    scale = useMotionValue(1);\n  const traveling = useRef(false);\n  const activeRef = useRef(activeId);\n  const pressingRef = useRef(pressing);\n\n  /** The button's box in tray coordinates, exact to the device pixel (offsetLeft rounds to whole pixels) and free of ancestor transforms. */\n  const measure = useCallback((node: HTMLElement) => {\n    const root = tray.current;\n    if (!root) return null;\n    const outer = root.getBoundingClientRect(),\n      box = node.getBoundingClientRect();\n    const unscale = outer.width ? root.offsetWidth / outer.width : 1;\n    const dpr = window.devicePixelRatio || 1;\n    const snap = (value: number) => Math.round(value * unscale * dpr) / dpr;\n    return {\n      x: snap(box.left - outer.left) - root.clientLeft,\n      y: snap(box.top - outer.top) - root.clientTop,\n      width: snap(box.width),\n      height: snap(box.height),\n    };\n  }, []);\n\n  const place = useCallback(\n    (id: string | null, mode: \"move\" | \"follow\") => {\n      const node = id ? nodes.current.get(id) : undefined;\n      if (!node) {\n        animate(opacity, 0, {\n          duration: reduced ? motionTokens.duration.instant : motionTokens.duration.standard,\n          ease: standard,\n        });\n        return;\n      }\n      const rect = measure(node);\n      if (!rect) return;\n      const visible = opacity.get() > 0.15;\n      // Travel between buttons on the morph spring. Appearing from nothing, and every move with reduced motion, lands in place.\n      const travel = !reduced && visible && (mode === \"move\" || traveling.current);\n      if (travel) {\n        traveling.current = true;\n        animate(x, rect.x, motionTokens.spring.morph);\n        animate(y, rect.y, motionTokens.spring.morph);\n        animate(width, rect.width, motionTokens.spring.morph);\n        animate(height, rect.height, {\n          ...motionTokens.spring.morph,\n          onComplete: () => {\n            traveling.current = false;\n          },\n        });\n      } else {\n        traveling.current = false;\n        x.jump(rect.x);\n        y.jump(rect.y);\n        width.jump(rect.width);\n        height.jump(rect.height);\n        if (!visible && !reduced) {\n          scale.jump(0.94);\n          animate(scale, pressingRef.current ? 0.965 : 1, motionTokens.spring.snappy);\n        }\n      }\n      if (opacity.get() !== 1)\n        animate(opacity, 1, {\n          duration: reduced ? motionTokens.duration.instant : motionTokens.duration.fast,\n          ease: standard,\n        });\n    },\n    [height, measure, opacity, reduced, scale, width, x, y],\n  );\n\n  useLayoutEffect(() => {\n    activeRef.current = activeId;\n    place(activeId, \"move\");\n  }, [activeId, place]);\n\n  useEffect(() => {\n    pressingRef.current = pressing;\n    animate(scale, pressing && activeId ? 0.965 : 1, reduced ? { duration: 0 } : motionTokens.spring.snappy);\n  }, [activeId, pressing, reduced, scale]);\n\n  // A font that loads or a container that reflows moves the buttons; the highlight follows without travel.\n  useEffect(() => {\n    const root = tray.current;\n    if (!root || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(() => {\n      if (activeRef.current) place(activeRef.current, \"follow\");\n    });\n    observer.observe(root);\n    nodes.current.forEach((node) => observer.observe(node));\n    return () => observer.disconnect();\n  }, [idsKey, place]);\n\n  // A native listener, because React bubbles pointer events through portals: moving from a button onto its tooltip\n  // would not count as leaving the tray, and the highlight would stay behind.\n  useEffect(() => {\n    const root = tray.current;\n    if (!root) return;\n    const leave = () => {\n      setHoverId(null);\n      setPressing(false);\n    };\n    root.addEventListener(\"pointerleave\", leave);\n    return () => root.removeEventListener(\"pointerleave\", leave);\n  }, []);\n\n  const buttons = () => Array.from(tray.current?.querySelectorAll<HTMLButtonElement>(\"[data-fbg-item]\") ?? []);\n\n  const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    const all = buttons();\n    const index = all.findIndex((node) => node === document.activeElement);\n    if (index < 0) return;\n    const next = vertical ? \"ArrowDown\" : \"ArrowRight\";\n    const previous = vertical ? \"ArrowUp\" : \"ArrowLeft\";\n    const usable = (node: HTMLButtonElement) => node.getAttribute(\"aria-disabled\") !== \"true\";\n    let target: HTMLButtonElement | undefined;\n    if (event.key === \"Home\") target = all.find(usable);\n    else if (event.key === \"End\") target = [...all].reverse().find(usable);\n    else if (event.key === next || event.key === previous) {\n      const step = event.key === next ? 1 : -1;\n      for (let offset = 1; offset <= all.length; offset += 1) {\n        const candidate = all[(index + step * offset + all.length * offset) % all.length]!;\n        if (usable(candidate)) {\n          target = candidate;\n          break;\n        }\n      }\n    } else return;\n    event.preventDefault();\n    if (!target) return;\n    // The keyboard takes the highlight back until the pointer moves again.\n    setHoverId(null);\n    target.focus();\n  };\n\n  const onBlur = (event: FocusEvent<HTMLDivElement>) => {\n    if (tray.current?.contains(event.relatedTarget as Node | null)) return;\n    setFocusId(null);\n    setPressing(false);\n  };\n\n  const classes = cn(\n    groupClass,\n    groupSizes[size],\n    groupVariants[variant],\n    vertical && \"flex-col items-stretch\",\n    className,\n  );\n\n  const group = (\n    // oxlint-disable-next-line jsx-a11y/interactive-supports-focus -- toolbar pattern: focus moves between the buttons with a roving tabindex, so the container itself is not a tab stop\n    <div\n      ref={tray}\n      className={classes}\n      style={style}\n      role=\"toolbar\"\n      aria-label={label}\n      aria-orientation={orientation}\n      data-variant={variant}\n      onKeyDown={onKeyDown}\n      onBlur={onBlur}\n    >\n      <motion.span\n        className={highlightClass}\n        data-pressing={pressing && activeId ? \"\" : undefined}\n        style={{ x, y, width, height, opacity, scale }}\n        aria-hidden=\"true\"\n      />\n      {items.map((item, position) => {\n        if (!isAction(item))\n          return (\n            <hr\n              key={item.id ?? `separator-${position}`}\n              className=\"mx-0.5 h-[calc(var(--fbg-h)*.5)] w-px flex-none border-0 self-center rounded-[1px] bg-(--fbg-rule) group-aria-[orientation=vertical]/fbg:mx-0 group-aria-[orientation=vertical]/fbg:my-0.5 group-aria-[orientation=vertical]/fbg:h-px group-aria-[orientation=vertical]/fbg:w-[calc(var(--fbg-h)*.5)]\"\n              aria-orientation={vertical ? \"horizontal\" : \"vertical\"}\n            />\n          );\n        const { id, label: itemLabel, icon, onSelect, disabled = false, pressed, shortcut, reserveLabels = [] } = item;\n        const iconOnly = !!icon && (item.iconOnly ?? groupIconOnly);\n        const button = (\n          <button\n            ref={(node) => {\n              if (node) nodes.current.set(id, node);\n              else nodes.current.delete(id);\n            }}\n            type=\"button\"\n            className={cn(size === \"sm\" ? \"text-xs\" : \"text-sm\", itemClass)}\n            data-fbg-item=\"\"\n            data-active={activeId === id ? \"\" : undefined}\n            data-icon={icon ? \"\" : undefined}\n            data-icon-only={iconOnly ? \"\" : undefined}\n            aria-label={itemLabel}\n            aria-pressed={pressed === undefined ? undefined : pressed}\n            aria-disabled={disabled || undefined}\n            tabIndex={stop === id ? 0 : -1}\n            onPointerMove={(event) => {\n              if (event.pointerType === \"mouse\" || event.pointerType === \"pen\") setHoverId(id);\n            }}\n            onPointerDown={(event) => {\n              if (disabled || event.button !== 0) return;\n              setHoverId(id);\n              setPressing(true);\n            }}\n            onPointerUp={() => setPressing(false)}\n            onPointerCancel={() => setPressing(false)}\n            onPointerLeave={() => setPressing(false)}\n            onKeyDown={(event) => {\n              if ((event.key === \"Enter\" || event.key === \" \") && !event.repeat && !disabled) setPressing(true);\n            }}\n            onKeyUp={(event) => {\n              if (event.key === \"Enter\" || event.key === \" \") setPressing(false);\n            }}\n            onFocus={(event) => {\n              setTabStop(id);\n              let keyboard = false;\n              try {\n                keyboard = event.currentTarget.matches(\":focus-visible\");\n              } catch {\n                keyboard = false;\n              }\n              setFocusId(keyboard ? id : null);\n            }}\n            onClick={() => {\n              if (disabled) return;\n              onSelect?.();\n              onAction?.(id);\n            }}\n          >\n            <ItemBody label={itemLabel} icon={icon} iconOnly={iconOnly} reserve={reserveLabels} reduced={reduced} />\n          </button>\n        );\n        // Tooltips render no wrapper element, so every button stays a direct child of the tray and measures against it.\n        if (!iconOnly && !shortcut) return <Fragment key={id}>{button}</Fragment>;\n        const tip = <TipContent label={itemLabel} shortcut={shortcut} />;\n        return side === \"top\" || side === \"bottom\" ? (\n          <Tooltip key={id} side={side} content={tip}>\n            {button}\n          </Tooltip>\n        ) : (\n          <SideTip key={id} side={side} content={tip}>\n            {button}\n          </SideTip>\n        );\n      })}\n    </div>\n  );\n\n  return side === \"left\" || side === \"right\" ? (\n    <TooltipPrimitive.Provider delayDuration={250} skipDelayDuration={300} disableHoverableContent>\n      {group}\n    </TooltipPrimitive.Provider>\n  ) : (\n    group\n  );\n}\n\nexport default FloatingButtonGroup;\n",
      "type": "registry:component",
      "target": "@components/iris/floating-button-group/floating-button-group.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/floating-button-group",
  "meta": {
    "kind": "component",
    "group": "Actions",
    "export": "FloatingButtonGroup",
    "tags": [
      "action",
      "group",
      "toolbar",
      "new"
    ],
    "docs": "https://irisui.dev/components/floating-button-group"
  }
}
