{
  "$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",
    "lucide-react"
  ],
  "registryDependencies": [
    "https://mcp.xzero.co.uk/r/xzero-motion-tokens.json",
    "https://mcp.xzero.co.uk/r/tooltip.json"
  ],
  "files": [
    {
      "path": "registry/components/floating-button-group/floating-button-group.tsx",
      "type": "registry:component",
      "target": "@components/xzero/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 styles from \"./floating-button-group.module.css\";\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 = { opacity: 0, y: -3, filter: `blur(${motionTokens.blur.soft}px)`, transition: { duration: motionTokens.duration.fast, ease: standard } };\nconst fadeIn: TargetAndTransition = { opacity: 0 };\nconst fadeOut: TargetAndTransition = { opacity: 0, transition: { duration: motionTokens.duration.instant } };\n\nfunction Phase({ children, reduced }: { children: ReactNode; reduced: boolean }) {\n  const present = useIsPresent();\n  return <motion.span className={styles.phase} aria-hidden={present ? undefined : true} initial={reduced ? fadeIn : textIn} animate={rest} exit={reduced ? fadeOut : textOut} transition={{ duration: reduced ? motionTokens.duration.instant : motionTokens.duration.standard, ease: enter }}>{children}</motion.span>;\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({ label, icon, iconOnly, reserve, reduced }: { label: string; icon?: ReactNode; iconOnly: boolean; reserve: readonly string[]; reduced: boolean }) {\n  const ghosts = iconOnly ? [] : Array.from(new Set([label, ...reserve]));\n  return <span className={styles.slot}>\n    {ghosts.map(text => <span key={text} className={styles.ghost} aria-hidden=\"true\">\n      {icon ? <span className={styles.iconSpace} /> : null}\n      <span className={styles.text}>{text}</span>\n    </span>)}\n    <AnimatePresence initial={false}>\n      <Phase key={label} reduced={reduced}>\n        {icon ? <span className={styles.icon} aria-hidden=\"true\">{icon}</span> : null}\n        {iconOnly ? null : <span className={styles.text}>{label}</span>}\n      </Phase>\n    </AnimatePresence>\n  </span>;\n}\n\nfunction TipContent({ label, shortcut }: { label: string; shortcut?: string }) {\n  return <span className={styles.tip}>{label}{shortcut ? <kbd className={styles.kbd}>{shortcut}</kbd> : null}</span>;\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 <TooltipPrimitive.Root>\n    <TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>\n    <TooltipPrimitive.Portal>\n      <TooltipPrimitive.Content className={styles.sideTip} side={side} sideOffset={10} collisionPadding={12}>{content}</TooltipPrimitive.Content>\n    </TooltipPrimitive.Portal>\n  </TooltipPrimitive.Root>;\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({ items, label, variant = \"muted\", size = \"md\", orientation = \"horizontal\", iconOnly: groupIconOnly = false, tooltipSide, onAction, className, style }: 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), y = useMotionValue(0), width = useMotionValue(0), height = useMotionValue(0);\n  const opacity = useMotionValue(0), 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(), 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 { x: snap(box.left - outer.left) - root.clientLeft, y: snap(box.top - outer.top) - root.clientTop, width: snap(box.width), height: snap(box.height) };\n  }, []);\n\n  const place = useCallback((id: string | null, mode: \"move\" | \"follow\") => {\n    const node = id ? nodes.current.get(id) : undefined;\n    if (!node) {\n      animate(opacity, 0, { duration: reduced ? motionTokens.duration.instant : motionTokens.duration.standard, ease: standard });\n      return;\n    }\n    const rect = measure(node);\n    if (!rect) return;\n    const visible = opacity.get() > .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, { ...motionTokens.spring.morph, onComplete: () => { traveling.current = false; } });\n    } else {\n      traveling.current = false;\n      x.jump(rect.x); y.jump(rect.y); width.jump(rect.width); height.jump(rect.height);\n      if (!visible && !reduced) {\n        scale.jump(.94);\n        animate(scale, pressingRef.current ? .965 : 1, motionTokens.spring.snappy);\n      }\n    }\n    if (opacity.get() !== 1) animate(opacity, 1, { duration: reduced ? motionTokens.duration.instant : motionTokens.duration.fast, ease: standard });\n  }, [height, measure, opacity, reduced, scale, width, x, y]);\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 ? .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(() => { if (activeRef.current) place(activeRef.current, \"follow\"); });\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 = () => { setHoverId(null); setPressing(false); };\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)) { target = candidate; break; }\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 = [styles.group, styles[variant], styles[size], vertical ? styles.vertical : \"\", className].filter(Boolean).join(\" \");\n\n  const group = <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 className={styles.highlight} data-pressing={pressing && activeId ? \"\" : undefined} style={{ x, y, width, height, opacity, scale }} aria-hidden=\"true\" />\n    {items.map((item, position) => {\n      if (!isAction(item)) return <span key={item.id ?? `separator-${position}`} className={styles.separator} role=\"separator\" aria-orientation={vertical ? \"horizontal\" : \"vertical\"} />;\n      const { id, label: itemLabel, icon, onSelect, disabled = false, pressed, shortcut, reserveLabels = [] } = item;\n      const iconOnly = !!icon && (item.iconOnly ?? groupIconOnly);\n      const button = <button\n        ref={node => { if (node) nodes.current.set(id, node); else nodes.current.delete(id); }}\n        type=\"button\"\n        className={styles.item}\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 => { if (event.pointerType === \"mouse\" || event.pointerType === \"pen\") setHoverId(id); }}\n        onPointerDown={event => { if (disabled || event.button !== 0) return; setHoverId(id); setPressing(true); }}\n        onPointerUp={() => setPressing(false)}\n        onPointerCancel={() => setPressing(false)}\n        onPointerLeave={() => setPressing(false)}\n        onKeyDown={event => { if ((event.key === \"Enter\" || event.key === \" \") && !event.repeat && !disabled) setPressing(true); }}\n        onKeyUp={event => { if (event.key === \"Enter\" || event.key === \" \") setPressing(false); }}\n        onFocus={event => {\n          setTabStop(id);\n          let keyboard = false;\n          try { keyboard = event.currentTarget.matches(\":focus-visible\"); } catch { keyboard = false; }\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      // 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}>{button}</Tooltip>\n        : <SideTip key={id} side={side} content={tip}>{button}</SideTip>;\n    })}\n  </div>;\n\n  return side === \"left\" || side === \"right\" ? <TooltipPrimitive.Provider delayDuration={250} skipDelayDuration={300} disableHoverableContent>{group}</TooltipPrimitive.Provider> : group;\n}\n\nexport default FloatingButtonGroup;\n"
    },
    {
      "path": "registry/components/floating-button-group/floating-button-group.module.css",
      "type": "registry:component",
      "target": "@components/xzero/floating-button-group/floating-button-group.module.css",
      "content": "/* Separate soft buttons in a quiet tray, with real gaps between them. The tray always keeps its natural size and never wraps or clips its buttons. The buttons stay transparent: one shared highlight\n   moves underneath their content, so there is only ever one moving surface. Corners are concentric with the tray. */\n.group {\n  --fbg-h: 36px; --fbg-pad: 4px; --fbg-gap: 4px; --fbg-pad-x: 12px;\n  --fbg-outer: var(--radius-control);\n  --fbg-radius: calc(var(--fbg-outer) - var(--fbg-pad) - 1px);\n  --fbg-highlight: color-mix(in oklab, var(--foreground) 7%, transparent);\n  --fbg-highlight-press: color-mix(in oklab, var(--foreground) 12%, transparent);\n  --fbg-on: color-mix(in oklab, var(--control-on) 12%, transparent);\n  --fbg-rule: color-mix(in oklab, var(--foreground) 12%, transparent);\n  /* The muted tray is a translucent tint, not --surface-muted, so it still reads when it sits on a muted surface. */\n  --fbg-tray: color-mix(in oklab, var(--foreground) 4.5%, transparent);\n  --fbg-tray-edge: color-mix(in oklab, var(--foreground) 3%, transparent);\n  position: relative; isolation: isolate; display: inline-flex; flex: none; flex-wrap: nowrap; align-items: center; gap: var(--fbg-gap); padding: var(--fbg-pad); border: 1px solid var(--fbg-tray-edge); border-radius: var(--fbg-outer); background: var(--fbg-tray); color: var(--text-secondary); font-family: var(--font-body); letter-spacing: var(--tracking-body); -webkit-tap-highlight-color: transparent;\n}\n.sm { --fbg-h: 30px; --fbg-pad: 3px; --fbg-gap: 3px; --fbg-pad-x: 10px; --fbg-outer: calc(var(--radius-control) - 3px); }\n.md { --fbg-h: 36px; }\n/* Raised for overlays such as a selection toolbar: the floating layer shadow and a hairline edge. */\n.floating { border-color: var(--border); background: var(--surface-raised); box-shadow: var(--shadow-floating); }\n.vertical { flex-direction: column; align-items: stretch; }\n:global(:root[data-theme=\"dark\"]) .group {\n  --fbg-highlight: color-mix(in oklab, var(--foreground) 9%, transparent);\n  --fbg-highlight-press: color-mix(in oklab, var(--foreground) 15%, transparent);\n  --fbg-on: color-mix(in oklab, var(--control-on) 16%, transparent);\n  --fbg-tray: color-mix(in oklab, var(--foreground) 6%, transparent);\n  --fbg-tray-edge: color-mix(in oklab, var(--foreground) 5%, transparent);\n}\n\n/* The highlight paints above the buttons' own backgrounds (a pressed tint) and below their content. */\n.highlight { position: absolute; z-index: 1; top: 0; left: 0; border-radius: var(--fbg-radius); background: var(--fbg-highlight); pointer-events: none; transition: background-color var(--duration-fast) var(--ease-standard); }\n.highlight[data-pressing] { background: var(--fbg-highlight-press); transition-duration: var(--duration-instant); }\n\n/* Buttons are not positioned, so the highlight measures them against the tray and stacks above their background. */\n.item { display: inline-flex; min-width: var(--fbg-h); height: var(--fbg-h); flex: none; align-items: center; justify-content: center; margin: 0; padding: 0 var(--fbg-pad-x); border: 0; border-radius: var(--fbg-radius); background: transparent; color: inherit; font: inherit; font-size: var(--text-sm); font-weight: 500; line-height: 1; white-space: nowrap; cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent; touch-action: manipulation; transition: color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard); }\n.sm .item { font-size: var(--text-xs); }\n.item[data-icon]:not([data-icon-only]) { padding-left: calc(var(--fbg-pad-x) - 2px); }\n.item[data-icon-only] { width: var(--fbg-h); padding: 0; }\n.item[data-active], .item[aria-pressed=\"true\"] { color: var(--foreground); }\n.item[aria-pressed=\"true\"] { background: var(--fbg-on); }\n.item[aria-disabled=\"true\"] { color: var(--text-muted); cursor: not-allowed; opacity: .5; }\n.vertical .item:not([data-icon-only]) { justify-content: flex-start; }\n.vertical .item[data-icon-only] { align-self: center; }\n\n/* Ghosts of every label share one grid cell with the visible one, so the slot always has the widest label's width. */\n.slot { position: relative; z-index: 2; display: inline-grid; align-items: center; justify-items: center; }\n.slot > * { grid-area: 1 / 1; }\n.vertical .slot { justify-items: start; }\n.ghost { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; visibility: hidden; pointer-events: none; }\n.iconSpace { display: block; width: 16px; height: 16px; flex: none; }\n.phase { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }\n.icon { display: inline-flex; flex: none; }\n.icon svg { width: 16px; height: 16px; stroke-width: 1.75; }\n.text { display: block; }\n\n.separator { flex: none; align-self: center; width: 1px; height: calc(var(--fbg-h) * .5); margin-inline: 2px; border-radius: 1px; background: var(--fbg-rule); }\n.vertical .separator { width: calc(var(--fbg-h) * .5); height: 1px; margin: 2px 0; }\n\n.tip { display: inline-flex; align-items: baseline; gap: var(--space-3); white-space: nowrap; }\n.kbd { font: inherit; color: color-mix(in oklab, var(--background) 58%, var(--foreground)); font-variant-numeric: tabular-nums; }\n\n/* Side tooltips for a column match the library tooltip, rising a few pixels away from the rail instead of up. */\n.sideTip { --tip-x: -3px; z-index: 90; max-width: 15rem; padding: var(--space-3) var(--space-4); border: 1px solid color-mix(in oklab, var(--background) 14%, var(--foreground)); border-radius: var(--radius-control); background: var(--foreground); color: var(--background); box-shadow: var(--shadow-raised); font-family: var(--font-body); font-size: var(--text-sm); font-weight: 400; letter-spacing: var(--tracking-body); line-height: var(--leading-body); transform-origin: var(--radix-tooltip-content-transform-origin); transition: opacity var(--duration-fast) var(--ease-enter), transform var(--duration-fast) var(--ease-enter); }\n.sideTip[data-side=\"left\"] { --tip-x: 3px; }\n@starting-style {\n  .sideTip[data-state$=\"-open\"] { opacity: 0; transform: translateX(var(--tip-x)) scale(.97); }\n}\n/* Radix unmounts when the exit animation ends, so a no-op keyframe times the exit while the transition does the visual work. */\n.sideTip[data-state=\"closed\"] { opacity: 0; transform: scale(.98); transition: opacity 110ms var(--ease-standard), transform 110ms var(--ease-standard); animation: side-tip-exit 110ms linear both; }\n@keyframes side-tip-exit { to { --side-tip-exit: 1; } }\n\n@media (prefers-reduced-motion: reduce) {\n  .item, .highlight { transition: color var(--duration-instant) linear, background-color var(--duration-instant) linear, opacity var(--duration-instant) linear; }\n  .sideTip, .sideTip[data-state=\"closed\"] { transform: none; transition: opacity 90ms linear; }\n}\n"
    }
  ],
  "docs": "Docs and live preview: https://ui.xzero.co.uk/components/floating-button-group",
  "meta": {
    "tier": "free",
    "kind": "component",
    "tags": [
      "action",
      "group",
      "toolbar",
      "new"
    ],
    "docs": "https://ui.xzero.co.uk/components/floating-button-group",
    "markdown": "https://ui.xzero.co.uk/components/floating-button-group/markdown"
  }
}