{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"button-group","type":"registry:ui","title":"Button group","description":"A toolbar of related tools in one raised shell; toggled tools sink into it.","dependencies":["clsx","tailwind-merge"],"registryDependencies":["https://caliperui.site/r/styles.json"],"files":[{"path":"ui/button-group.tsx","type":"registry:ui","target":"components/ui/button-group.tsx","content":"\"use client\";\n\nimport { useImperativeHandle, useRef, useState, type ComponentProps } from \"react\";\nimport { GlideHighlight, glideBox, type GlideBox } from \"./glide-highlight\";\nimport { cn } from \"./lib/cn\";\nimport \"./button-group.css\";\n\nexport function ButtonGroup({\n  ref,\n  className,\n  children,\n  ...props\n}: ComponentProps<\"div\">) {\n  const group = useRef<HTMLDivElement>(null);\n  useImperativeHandle(ref, () => group.current!, []);\n  const [box, setBox] = useState<GlideBox | null>(null);\n\n  function place(target: EventTarget | null) {\n    const button =\n      target instanceof Element\n        ? target.closest<HTMLElement>(\"[data-slot=button]\")\n        : null;\n    if (!button || !group.current?.contains(button)) return;\n    setBox(glideBox(button));\n  }\n\n  function rest(next: EventTarget | null) {\n    const button =\n      next instanceof Element\n        ? next.closest<HTMLElement>(\"[data-slot=button]\")\n        : null;\n    if (button && group.current?.contains(button)) return;\n    setBox((current) => (current ? { ...current, active: false } : null));\n  }\n\n  return (\n    <div\n      {...props}\n      ref={group}\n      role=\"group\"\n      data-slot=\"button-group\"\n      onPointerMove={(event) => place(event.target)}\n      onPointerLeave={() => rest(null)}\n      onFocus={(event) => place(event.target)}\n      onBlur={(event) => rest(event.relatedTarget)}\n      className={cn(\n        \"relative inline-flex max-w-full items-stretch gap-0.5 rounded-touch bg-raised p-[3px] shadow-raised [&>[data-slot=button]]:h-[calc(var(--control-height)-6px)] [&>[data-slot=button]]:rounded-[calc(var(--radius-touch)-3px)] [&>[data-slot=button][data-size=icon]]:w-[calc(var(--control-height)-6px)]\",\n        className,\n      )}\n    >\n      <GlideHighlight box={box} />\n      {children}\n    </div>\n  );\n}\n"},{"path":"ui/glide-highlight.tsx","type":"registry:ui","target":"components/ui/glide-highlight.tsx","content":"\"use client\";\n\nimport { useLayoutEffect, useState } from \"react\";\nimport \"./glide-highlight.css\";\n\nexport type GlideBox = {\n  active: boolean;\n  left: number;\n  top: number;\n  width: number;\n  height: number;\n};\n\nexport function glideBox(target: HTMLElement): GlideBox {\n  return {\n    active: true,\n    left: target.offsetLeft,\n    top: target.offsetTop,\n    width: target.offsetWidth,\n    height: target.offsetHeight,\n  };\n}\n\nexport function useHighlightedGlide(\n  root: HTMLElement | null,\n  selector = \"[data-highlighted]\",\n  /** Attributes whose change can move the target, e.g. data-state for an open trigger. */\n  attributes: readonly string[] = [\"data-highlighted\"],\n) {\n  const [box, setBox] = useState<GlideBox | null>(null);\n\n  useLayoutEffect(() => {\n    const element = root;\n    if (!element) return;\n    const update = () => {\n      const target = element.querySelector<HTMLElement>(selector);\n      setBox((current) =>\n        target ? glideBox(target) : current ? { ...current, active: false } : null,\n      );\n    };\n    update();\n    const observer = new MutationObserver(update);\n    observer.observe(element, {\n      subtree: true,\n      attributes: true,\n      attributeFilter: [...attributes],\n    });\n    return () => observer.disconnect();\n    // The attribute list is fixed per call site.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [root, selector]);\n\n  return box;\n}\n\nexport function GlideHighlight({ box }: { box: GlideBox | null }) {\n  if (!box) return null;\n  return (\n    <span\n      aria-hidden=\"true\"\n      data-slot=\"glide-highlight\"\n      data-active={box.active}\n      style={{\n        width: box.width,\n        height: box.height,\n        transform: `translate(${box.left}px, ${box.top}px)`,\n      }}\n    />\n  );\n}\n"},{"path":"ui/lib/cn.ts","type":"registry:ui","target":"components/ui/lib/cn.ts","content":"import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\nconst twMerge = extendTailwindMerge({\n  extend: { theme: { radius: [\"control\", \"surface\", \"overlay\"], shadow: [\"overlay\"] } },\n});\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"},{"path":"ui/button-group.css","type":"registry:file","target":"components/ui/button-group.css","content":"/* button-group.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"button-group\"] > [data-slot=\"glide-highlight\"] {\n    top: 0;\n    border-radius: 8px;\n    transition:\n      transform var(--duration-normal) var(--ease-out),\n      width var(--duration-normal) var(--ease-out),\n      height var(--duration-normal) var(--ease-out),\n      opacity var(--duration-fast) var(--ease-out);\n  }\n}\n"},{"path":"ui/glide-highlight.css","type":"registry:file","target":"components/ui/glide-highlight.css","content":"/* glide-highlight.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"glide-highlight\"] {\n    position: absolute;\n    top: 0;\n    left: 0;\n    z-index: 0;\n    pointer-events: none;\n    border-radius: var(--radius-surface);\n    background: var(--muted);\n    opacity: 0;\n    transition:\n      transform var(--duration-travel) var(--ease-showcase),\n      width var(--duration-travel) var(--ease-showcase),\n      height var(--duration-travel) var(--ease-showcase),\n      opacity var(--duration-deliberate) var(--ease-out);\n  }\n  [data-slot=\"glide-highlight\"][data-active=\"true\"] {\n    opacity: 1;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"glide-highlight\"] {\n      transition: none;\n    }\n    [data-slot=\"glide-highlight\"] {\n      transition:\n        transform 400ms var(--ease-showcase),\n        width 400ms var(--ease-showcase),\n        height 400ms var(--ease-showcase),\n        opacity 280ms var(--ease-out);\n    }\n  }\n}\n"}],"categories":["actions"],"meta":{"tier":"free","license":"MIT","family":"actions","docs":"/docs/components/button-group"}}