{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"split-button","type":"registry:ui","title":"Split button","description":"A primary action joined to its alternatives; the chosen option becomes the label.","dependencies":["@radix-ui/react-dropdown-menu","@radix-ui/react-slot","clsx","lucide-react","tailwind-merge"],"registryDependencies":["https://caliperui.site/r/styles.json"],"files":[{"path":"ui/button.tsx","type":"registry:ui","target":"components/ui/button.tsx","content":"\"use client\";\n\nimport { Slot, Slottable } from \"@radix-ui/react-slot\";\nimport { LoaderCircle } from \"lucide-react\";\nimport {\n  useLayoutEffect,\n  useRef,\n  useState,\n  type ComponentProps,\n  type ReactNode,\n} from \"react\";\nimport { cn } from \"./lib/cn\";\nimport \"./shared/loading-indicator.css\";\nimport \"./button.css\";\n\n/* Colour, material and press live in button.css keyed on data-variant, so a\n   consumer's utility classes still override them. Raised to press: the\n   button sinks 1px into an inner shade instead of scaling. */\nconst variants = {\n  default: \"\",\n  secondary: \"\",\n  ghost: \"\",\n  destructive: \"\",\n};\n\nconst sizes = {\n  sm: \"h-8 gap-1.5 rounded-full px-3 text-[13px] max-md:min-h-11 [@media(pointer:coarse)]:min-h-11\",\n  md: \"h-[var(--control-height)] gap-2 rounded-full px-3.5 text-sm\",\n  lg: \"h-11 gap-2 rounded-full px-[18px] text-[15px]\",\n  icon: \"size-[var(--control-height)] rounded-full p-0\",\n};\n\nexport type ButtonProps = ComponentProps<\"button\"> & {\n  variant?: keyof typeof variants;\n  size?: keyof typeof sizes;\n  asChild?: boolean;\n  /** Shows a spinner over the label and swallows clicks while keeping focus and width. */\n  loading?: boolean;\n  /** Animate a meaningful label/icon change, such as saving → saved. */\n  feedbackKey?: string;\n};\n\nexport function Button({\n  className,\n  variant = \"default\",\n  size = \"md\",\n  asChild = false,\n  loading = false,\n  feedbackKey,\n  disabled,\n  \"aria-disabled\": ariaDisabled,\n  children,\n  onClick,\n  onClickCapture,\n  tabIndex,\n  type = \"button\",\n  ...props\n}: ButtonProps) {\n  const Component = asChild ? Slot : \"button\";\n  const nativeDisabled = Boolean(disabled);\n  const blocked =\n    nativeDisabled ||\n    loading ||\n    ariaDisabled === true ||\n    ariaDisabled === \"true\";\n\n  return (\n    <Component\n      data-slot=\"button\"\n      data-variant={variant}\n      data-size={size}\n      type={asChild ? undefined : type}\n      className={cn(\n        \"relative inline-flex shrink-0 items-center justify-center whitespace-nowrap border border-transparent font-medium tracking-[-0.006em] outline-none focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none aria-disabled:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n        variants[variant],\n        sizes[size],\n        className,\n      )}\n      disabled={asChild ? undefined : nativeDisabled}\n      aria-disabled={nativeDisabled || ariaDisabled ? true : undefined}\n      aria-busy={loading || undefined}\n      tabIndex={asChild && nativeDisabled ? -1 : tabIndex}\n      {...props}\n      onKeyDown={(event) => {\n        if (blocked && (event.key === \"Enter\" || event.key === \" \")) {\n          event.preventDefault();\n        }\n        props.onKeyDown?.(event);\n      }}\n      onClickCapture={(event) => {\n        if (blocked) {\n          event.preventDefault();\n          event.stopPropagation();\n          return;\n        }\n        onClickCapture?.(event);\n      }}\n      onClick={onClick}\n    >\n      {loading && asChild && (\n        <LoaderCircle data-slot=\"loading-indicator\" aria-hidden=\"true\" />\n      )}\n      {loading && !asChild && feedbackKey === undefined && (\n        <span data-slot=\"button-busy\" aria-hidden=\"true\">\n          <LoaderCircle data-slot=\"loading-indicator\" />\n        </span>\n      )}\n      <Slottable>\n        {loading && !asChild && feedbackKey === undefined ? (\n          <span data-slot=\"button-label\" data-hidden=\"true\">\n            {children}\n          </span>\n        ) : feedbackKey !== undefined && !asChild ? (\n          <ButtonFeedback stateKey={feedbackKey}>\n            {loading && (\n              <LoaderCircle data-slot=\"loading-indicator\" aria-hidden=\"true\" />\n            )}\n            {children}\n          </ButtonFeedback>\n        ) : (\n          children\n        )}\n      </Slottable>\n    </Component>\n  );\n}\n\nfunction ButtonFeedback({\n  stateKey,\n  children,\n}: {\n  stateKey: string;\n  children: ReactNode;\n}) {\n  const content = useRef<HTMLSpanElement>(null);\n  const [initialKey] = useState(stateKey);\n  const [width, setWidth] = useState<number>();\n  useLayoutEffect(() => {\n    const element = content.current;\n    if (!element) return;\n    const measure = () => setWidth(element.offsetWidth);\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(element);\n    return () => observer.disconnect();\n  }, [stateKey]);\n  return (\n    <span data-slot=\"button-feedback\" style={{ width }}>\n      <span\n        key={stateKey}\n        ref={content}\n        data-slot=\"button-feedback-label\"\n        data-enter={stateKey !== initialKey}\n      >\n        {children}\n      </span>\n    </span>\n  );\n}\n"},{"path":"ui/dropdown-menu.tsx","type":"registry:ui","target":"components/ui/dropdown-menu.tsx","content":"\"use client\";\n\nimport * as Primitive from \"@radix-ui/react-dropdown-menu\";\nimport { Check, Circle } from \"lucide-react\";\nimport { useEffect, useRef, useState, type ComponentProps, type ReactNode } from \"react\";\nimport { GlideHighlight, useHighlightedGlide } from \"./glide-highlight\";\nimport { cn } from \"./lib/cn\";\nimport \"./shared/dropdown-content.css\";\nimport \"./dropdown-menu.css\";\n\nexport const DropdownMenu = Primitive.Root;\nexport const DropdownMenuTrigger = Primitive.Trigger;\nexport const DropdownMenuGroup = Primitive.Group;\nexport const DropdownMenuRadioGroup = Primitive.RadioGroup;\n\nexport function DropdownMenuContent({\n  className,\n  sideOffset = 8,\n  collisionPadding = 16,\n  children,\n  ...props\n}: ComponentProps<typeof Primitive.Content>) {\n  const [content, setContent] = useState<HTMLDivElement | null>(null);\n  const box = useHighlightedGlide(content);\n  return (\n    <Primitive.Portal>\n      <Primitive.Content\n        ref={setContent}\n        data-slot=\"dropdown-content\"\n        sideOffset={sideOffset}\n        collisionPadding={collisionPadding}\n        className={cn(\n          \"relative z-50 min-w-56 max-w-[calc(100vw-2rem)] max-h-[var(--radix-dropdown-menu-content-available-height)] overflow-y-auto rounded-surface border border-border bg-elevated p-1 text-[13px] text-foreground shadow-overlay outline-none\",\n          className,\n        )}\n        {...props}\n      >\n        <GlideHighlight box={box} />\n        {children}\n      </Primitive.Content>\n    </Primitive.Portal>\n  );\n}\n\n/** Row anatomy shared by every menu: dropdown, menubar and context menus. */\nexport const menuItemClass =\n  \"relative z-10 flex min-h-[34px] max-md:min-h-[var(--control-height)] cursor-default select-none items-center gap-2.5 rounded-control px-2.5 text-[13px] font-normal outline-none data-[highlighted]:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-45 data-[variant=destructive]:text-destructive [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground data-[variant=destructive]:[&_svg]:text-destructive\";\n\nexport function DropdownMenuItem({\n  className,\n  ...props\n}: ComponentProps<typeof Primitive.Item>) {\n  return (\n    <Primitive.Item\n      data-slot=\"dropdown-item\"\n      className={cn(menuItemClass, className)}\n      {...props}\n    />\n  );\n}\nexport function DropdownMenuCheckboxItem({\n  className,\n  children,\n  ...props\n}: ComponentProps<typeof Primitive.CheckboxItem>) {\n  return (\n    <Primitive.CheckboxItem\n      data-slot=\"dropdown-item\"\n      className={cn(menuItemClass, \"pl-8\", className)}\n      {...props}\n    >\n      <span className=\"absolute left-2.5 flex size-4 items-center justify-center\">\n        <Primitive.ItemIndicator>\n          <Check aria-hidden=\"true\" />\n        </Primitive.ItemIndicator>\n      </span>\n      {children}\n    </Primitive.CheckboxItem>\n  );\n}\nexport function DropdownMenuRadioItem({\n  className,\n  children,\n  ...props\n}: ComponentProps<typeof Primitive.RadioItem>) {\n  return (\n    <Primitive.RadioItem\n      data-slot=\"dropdown-item\"\n      className={cn(menuItemClass, \"pl-8\", className)}\n      {...props}\n    >\n      <span className=\"absolute left-2.5 flex size-4 items-center justify-center\">\n        <Primitive.ItemIndicator>\n          <Circle className=\"!size-2 fill-current\" aria-hidden=\"true\" />\n        </Primitive.ItemIndicator>\n      </span>\n      {children}\n    </Primitive.RadioItem>\n  );\n}\nexport function DropdownMenuLabel({\n  className,\n  ...props\n}: ComponentProps<typeof Primitive.Label>) {\n  return (\n    <Primitive.Label\n      className={cn(\n        \"px-2.5 pb-1 pt-2 text-xs font-medium text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\nexport function DropdownMenuSeparator({\n  className,\n  ...props\n}: ComponentProps<typeof Primitive.Separator>) {\n  return (\n    <Primitive.Separator\n      className={cn(\"-mx-1 my-1 h-px bg-border\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function DropdownMenuShortcut({\n  className,\n  ...props\n}: ComponentProps<\"span\">) {\n  return (\n    <span\n      className={cn(\n        \"ml-auto pl-4 font-mono text-xs tracking-wide text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/**\n * A destructive item that asks in place: choosing it turns the row into the\n * question with Cancel and the confirming action, instead of stacking a\n * dialog on the menu. Both answers are menu items, so arrows and Enter work.\n */\nexport function DropdownMenuConfirmItem({\n  label,\n  icon,\n  question,\n  detail,\n  confirmLabel = \"Delete\",\n  onConfirm,\n}: {\n  label: string;\n  icon?: ReactNode;\n  /** e.g. \"Delete 3 files?\" */\n  question: string;\n  /** e.g. \"They move to Trash for 30 days.\" */\n  detail?: string;\n  confirmLabel?: string;\n  onConfirm: () => void;\n}) {\n  const [asking, setAsking] = useState(false);\n  const cancel = useRef<HTMLDivElement>(null);\n  useEffect(() => {\n    if (asking) cancel.current?.focus();\n  }, [asking]);\n  if (!asking) {\n    return (\n      <DropdownMenuItem\n        data-variant=\"destructive\"\n        onSelect={(event) => {\n          event.preventDefault();\n          setAsking(true);\n        }}\n      >\n        {icon}\n        {label}\n      </DropdownMenuItem>\n    );\n  }\n  return (\n    <div data-slot=\"menu-confirm\" role=\"group\" aria-label={question}>\n      <p className=\"px-2.5 pt-2 text-[13px] font-medium text-destructive\">{question}</p>\n      {detail && <p className=\"px-2.5 pb-1 text-xs text-muted-foreground\">{detail}</p>}\n      <DropdownMenuItem\n        ref={cancel}\n        onSelect={(event) => {\n          event.preventDefault();\n          setAsking(false);\n        }}\n      >\n        Cancel\n      </DropdownMenuItem>\n      <DropdownMenuItem data-variant=\"destructive\" onSelect={onConfirm}>\n        {icon}\n        {confirmLabel}\n      </DropdownMenuItem>\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/split-button.tsx","type":"registry:ui","target":"components/ui/split-button.tsx","content":"\"use client\";\n\nimport { Check, ChevronDown } from \"lucide-react\";\nimport { useState, type ReactNode } from \"react\";\nimport { Button, type ButtonProps } from \"./button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuRadioGroup,\n  DropdownMenuTrigger,\n} from \"./dropdown-menu\";\nimport * as Primitive from \"@radix-ui/react-dropdown-menu\";\nimport \"./split-button.css\";\n\nexport type SplitAction = {\n  label: string;\n  onSelect: () => void;\n  disabled?: boolean;\n};\n\nexport type SplitOption = {\n  value: string;\n  label: string;\n  /** One line on what this choice does, e.g. \"4 commits become one on main\". */\n  description?: string;\n  disabled?: boolean;\n};\n\ntype Shared = Omit<ButtonProps, \"children\" | \"onClick\" | \"size\"> & {\n  menuLabel?: string;\n  size?: \"sm\" | \"md\" | \"lg\";\n};\n\ntype ActionsMode = Shared & {\n  /** Alternatives that run immediately from the menu. */\n  actions: readonly SplitAction[];\n  children: ReactNode;\n  onClick?: ButtonProps[\"onClick\"];\n  options?: never;\n};\n\ntype ChoiceMode = Shared & {\n  /** Choices; the selected one becomes the button's label. */\n  options: readonly SplitOption[];\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  /** Runs the selected option. */\n  onRun: (value: string) => void;\n  /** Small heading above the options, e.g. \"Merge method\". */\n  menuHeading?: string;\n  icon?: ReactNode;\n  actions?: never;\n};\n\nconst optionRow =\n  \"items-start py-2 pl-8 max-md:min-h-[52px] [&_[data-slot=split-check]]:absolute [&_[data-slot=split-check]]:left-2.5 [&_[data-slot=split-check]]:top-2.5\";\n\n/**\n * A primary action joined to its close alternatives. In choice mode the\n * chosen option becomes the label, and every option states its consequence.\n */\nexport function SplitButton(props: ActionsMode | ChoiceMode) {\n  const { menuLabel = \"More actions\", variant = \"default\", size = \"md\", disabled } = props;\n  const [inner, setInner] = useState(\n    props.options ? (props.defaultValue ?? props.options.find((o) => !o.disabled)?.value ?? \"\") : \"\",\n  );\n\n  const triggerClass = size === \"lg\" ? \"size-11 rounded-l-none\" : size === \"sm\" ? \"size-8 rounded-l-none\" : \"rounded-l-none\";\n\n  if (props.options) {\n    const { options, value, onValueChange, onRun, menuHeading, icon } = props;\n    const current = value ?? inner;\n    const selected = options.find((o) => o.value === current) ?? options[0];\n    const choose = (next: string) => {\n      if (value === undefined) setInner(next);\n      onValueChange?.(next);\n    };\n    return (\n      <div data-slot=\"split-button\" className=\"inline-flex items-stretch gap-px\">\n        <Button\n          variant={variant}\n          size={size}\n          disabled={disabled}\n          className=\"rounded-r-none\"\n          onClick={() => onRun(selected.value)}\n        >\n          {icon}\n          {selected.label}\n        </Button>\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <Button variant={variant} size=\"icon\" disabled={disabled} aria-label={menuLabel} className={triggerClass}>\n              <ChevronDown data-slot=\"split-chevron\" aria-hidden=\"true\" />\n            </Button>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent align=\"end\" className=\"w-80\">\n            {menuHeading && <DropdownMenuLabel>{menuHeading}</DropdownMenuLabel>}\n            <DropdownMenuRadioGroup value={selected.value} onValueChange={choose}>\n              {options.map((option) => (\n                <Primitive.RadioItem\n                  key={option.value}\n                  value={option.value}\n                  disabled={option.disabled}\n                  data-slot=\"dropdown-item\"\n                  className={`relative z-10 flex cursor-default select-none gap-2.5 rounded-[8px] px-2.5 text-[13px] outline-none data-[disabled]:opacity-60 ${optionRow}`}\n                >\n                  <span data-slot=\"split-check\" className=\"flex size-4 items-center justify-center\">\n                    <Primitive.ItemIndicator>\n                      <Check className=\"size-4\" aria-hidden=\"true\" />\n                    </Primitive.ItemIndicator>\n                  </span>\n                  <span className=\"grid gap-0.5\">\n                    <span className=\"text-sm font-medium\">{option.label}</span>\n                    {option.description && (\n                      <span className=\"text-xs text-muted-foreground\">{option.description}</span>\n                    )}\n                  </span>\n                </Primitive.RadioItem>\n              ))}\n            </DropdownMenuRadioGroup>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n    );\n  }\n\n  const { actions, children, onClick, ...rest } = props;\n  delete (rest as Partial<ActionsMode>).menuLabel;\n  delete (rest as Partial<ActionsMode>).variant;\n  delete (rest as Partial<ActionsMode>).size;\n  return (\n    <div data-slot=\"split-button\" className=\"inline-flex items-stretch gap-px\">\n      <Button variant={variant} size={size} className=\"rounded-r-none\" onClick={onClick} {...rest}>\n        {children}\n      </Button>\n      <DropdownMenu>\n        <DropdownMenuTrigger asChild>\n          <Button variant={variant} size=\"icon\" disabled={disabled} aria-label={menuLabel} className={triggerClass}>\n            <ChevronDown data-slot=\"split-chevron\" aria-hidden=\"true\" />\n          </Button>\n        </DropdownMenuTrigger>\n        <DropdownMenuContent align=\"end\">\n          {actions.map((action) => (\n            <DropdownMenuItem\n              key={action.label}\n              disabled={action.disabled}\n              onSelect={action.onSelect}\n              className=\"max-md:min-h-[52px]\"\n            >\n              {action.label}\n            </DropdownMenuItem>\n          ))}\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </div>\n  );\n}\n"},{"path":"ui/shared/loading-indicator.css","type":"registry:file","target":"components/ui/shared/loading-indicator.css","content":"/* Shared by button.tsx, confirm-morph.tsx, expanding-button-group.tsx, field.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"loading-indicator\"] {\n    animation: caliper-spin var(--duration-loading) var(--ease-linear) infinite;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"loading-indicator\"] {\n      animation: none;\n    }\n  }\n}\n"},{"path":"ui/button.css","type":"registry:file","target":"components/ui/button.css","content":"/* button.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"button\"] {\n    transition-property: background-color, color, border-color;\n    transition-duration: var(--duration-fast);\n    transition-timing-function: var(--ease-out);\n  }\n\n  /* Button material. Raised to press: a lit top edge and a small lift; the\n     press travels 1px down and swaps the lift for an inner shade. Only\n     translate animates (release on the settle curve); shadows swap. */\n  [data-slot=\"button\"] {\n    --b-bg: var(--raised);\n    --b-bg-hover: var(--raised-hover);\n    --b-bg-press: var(--raised-press);\n    --b-fg: var(--foreground);\n    --b-shadow: var(--shadow-raised);\n    --b-shadow-press: var(--shadow-raised-press);\n    background-color: var(--b-bg);\n    color: var(--b-fg);\n    box-shadow: var(--b-shadow);\n    transition-property: background-color, color, border-color, translate, scale;\n    transition-duration:\n      var(--duration-fast), var(--duration-fast), var(--duration-fast),\n      var(--duration-normal), var(--duration-normal);\n    transition-timing-function:\n      var(--ease-out), var(--ease-out), var(--ease-out), var(--ease-settle),\n      var(--ease-settle);\n  }\n  [data-slot=\"button\"][data-variant=\"default\"] {\n    --b-bg: var(--primary);\n    --b-bg-hover: var(--primary-hover);\n    --b-bg-press: var(--primary-press);\n    --b-fg: var(--primary-foreground);\n    --b-shadow: var(--shadow-accent);\n    --b-shadow-press: var(--shadow-accent-press);\n  }\n  [data-slot=\"button\"][data-variant=\"destructive\"] {\n    --b-bg: var(--danger-fill);\n    --b-bg-hover: var(--danger-fill-hover);\n    --b-bg-press: var(--danger-fill-press);\n    --b-fg: var(--danger-fill-foreground);\n    --b-shadow: var(--shadow-danger);\n    --b-shadow-press: var(--shadow-danger-press);\n  }\n  [data-slot=\"button\"][data-variant=\"ghost\"] {\n    --b-bg: transparent;\n    --b-bg-hover: var(--muted);\n    --b-bg-press: var(--muted);\n    --b-fg: var(--muted-foreground);\n    --b-shadow: none;\n    --b-shadow-press: none;\n  }\n  @media (hover: hover) {\n    [data-slot=\"button\"]:hover {\n      background-color: var(--b-bg-hover);\n    }\n    [data-slot=\"button\"][data-variant=\"ghost\"]:hover {\n      color: var(--foreground);\n    }\n  }\n  [data-slot=\"button\"]:active:not(:disabled):not([aria-disabled=\"true\"]):not(\n      [aria-busy=\"true\"]\n    ) {\n    translate: 0 var(--motion-press-travel);\n    scale: var(--motion-press-scale);\n    background-color: var(--b-bg-press);\n    box-shadow: var(--b-shadow-press);\n    transition-duration:\n      var(--duration-fast), var(--duration-fast), var(--duration-fast), 90ms,\n      90ms;\n    transition-timing-function: var(--ease-out);\n  }\n  [data-slot=\"button\"]:is(:disabled, [aria-disabled=\"true\"]):not([aria-busy=\"true\"]) {\n    background-color: var(--sunken);\n    color: var(--muted-foreground);\n  }\n  [data-slot=\"button\"][data-variant=\"ghost\"]:is(:disabled, [aria-disabled=\"true\"]):not(\n      [aria-busy=\"true\"]\n    ) {\n    background-color: transparent;\n    box-shadow: none;\n  }\n  /* Loading keeps the exact width: the label stays in flow but invisible. */\n  [data-slot=\"button-label\"] {\n    display: inline-flex;\n    align-items: center;\n    gap: inherit;\n  }\n  /* Opacity, not visibility, so the label still names the button. */\n  [data-slot=\"button-label\"][data-hidden=\"true\"] {\n    opacity: 0;\n  }\n  [data-slot=\"button-busy\"] {\n    position: absolute;\n    inset: 0;\n    display: grid;\n    place-items: center;\n  }\n  [data-slot=\"button-feedback\"] {\n    display: inline-flex;\n    align-items: center;\n    gap: inherit;\n    overflow: clip;\n    transition: width var(--duration-normal) var(--ease-out);\n  }\n  [data-slot=\"button-feedback-label\"] {\n    display: inline-flex;\n    flex-shrink: 0;\n    align-items: center;\n    gap: inherit;\n    width: max-content;\n  }\n  [data-slot=\"button-feedback-label\"][data-enter=\"true\"] {\n    animation: caliper-feedback-enter var(--duration-normal) var(--ease-out);\n  }\n\n  [data-slot=\"button\"][aria-busy=\"true\"] {\n    pointer-events: none;\n  }\n  [data-slot=\"button\"][data-action-phase=\"error\"] {\n    color: var(--danger);\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"button\"],\n    [data-slot=\"button-feedback\"] {\n      transition: none;\n    }\n    [data-slot=\"button\"]:active:not(:disabled):not([aria-disabled=\"true\"]):not(\n        [aria-busy=\"true\"]\n      ) {\n      translate: none;\n    }\n    [data-slot=\"button-feedback-label\"],\n    [data-slot=\"button-feedback-label\"][data-enter=\"true\"] {\n      animation: none;\n    }\n  }\n\n  /* Toggle: pressed is recessed, as a toggled tool in a button group is. */\n  /* Four attributes, so it wins over the group and toolbar pressed rules in any stylesheet order. */\n  [data-slot=\"button\"][data-variant=\"ghost\"][data-toggle][aria-pressed=\"true\"] {\n    --b-bg: var(--raised-press);\n    --b-bg-hover: var(--raised-press);\n    --b-bg-press: var(--raised-press);\n    --b-fg: var(--foreground);\n    --b-shadow: var(--shadow-sunken);\n    --b-shadow-press: var(--shadow-sunken);\n  }\n}\n"},{"path":"ui/shared/dropdown-content.css","type":"registry:file","target":"components/ui/shared/dropdown-content.css","content":"/* Shared by context-menu.tsx, dropdown-menu.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"dropdown-item\"] {\n    transition:\n      color var(--duration-fast) var(--ease-out),\n      background-color var(--duration-fast) var(--ease-out),\n      scale var(--duration-fast) var(--ease-out);\n  }\n  [data-slot=\"dropdown-item\"]:focus-visible {\n    outline: 2px solid var(--ring);\n    outline-offset: -2px;\n  }\n  [data-slot=\"dropdown-content\"] {\n    transform-origin: var(--radix-dropdown-menu-content-transform-origin);\n  }\n  [data-slot=\"dropdown-content\"] [data-slot=\"glide-highlight\"] {\n    border-radius: var(--radius-control);\n    transition:\n      transform var(--duration-fast) var(--ease-out),\n      width var(--duration-fast) var(--ease-out),\n      height var(--duration-fast) var(--ease-out),\n      opacity var(--duration-fast) var(--ease-out);\n  }\n  [data-slot=\"dropdown-content\"][data-state=\"open\"] {\n    animation: caliper-dialog-enter var(--duration-normal) var(--ease-settle);\n  }\n  [data-slot=\"dropdown-content\"][data-state=\"closed\"] {\n    animation: caliper-fade-out var(--duration-fast) var(--ease-in) forwards;\n    pointer-events: none;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"dropdown-item\"] {\n      transition: none;\n    }\n    [data-slot=\"dropdown-content\"] {\n      animation: none;\n    }\n    /* Preserve the exit name for Radix Presence's completion matcher, including\n       a preference changed during exit. No visual motion or unmount timer. */\n    [data-slot=\"dropdown-content\"][data-state=\"closed\"] {\n      animation: caliper-fade-out var(--duration-instant) var(--ease-in) forwards !important;\n    }\n  }\n}\n\n[data-slot=\"dropdown-content\"] {\n  background-color: var(--elevated-glass);\n  border-color: color-mix(in srgb, var(--edge-light) 45%, var(--border));\n  backdrop-filter: blur(var(--blur-overlay));\n  box-shadow:\n    var(--shadow-overlay),\n    0 0 40px var(--edge-glow),\n    inset 0 1px 0 var(--edge-light);\n}\n@layer components {\n  [data-slot=\"dropdown-content\"][data-variant=\"glass\"] {\n    background: var(--elevated-glass);\n    -webkit-backdrop-filter: blur(24px) saturate(1.5);\n    backdrop-filter: blur(24px) saturate(1.5);\n    border-color: color-mix(in srgb, var(--edge-light) 45%, var(--border));\n    box-shadow: var(--shadow-overlay), inset 0 1px 0 var(--edge-light);\n    border-radius: 20px;\n  }\n  /* Moving along the bar swaps menus at once: a closing menu that lingered to fade would see focus leave and dismiss the bar. */\n  [data-slot=\"dropdown-content\"][data-menubar][data-state=\"closed\"] { animation: none; }\n}\n"},{"path":"ui/dropdown-menu.css","type":"registry:file","target":"components/ui/dropdown-menu.css","content":"/* dropdown-menu.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"menu-confirm\"] {\n    margin: 2px 0;\n    padding-bottom: 2px;\n    border-radius: var(--radius-control);\n    background: var(--destructive-subtle);\n    animation: caliper-fade-in 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"},{"path":"ui/split-button.css","type":"registry:file","target":"components/ui/split-button.css","content":"/* split-button.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"split-chevron\"] {\n    transition: rotate var(--duration-normal) var(--ease-settle);\n  }\n  [data-state=\"open\"] > [data-slot=\"split-chevron\"] {\n    rotate: 180deg;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"split-chevron\"] {\n      transition: none;\n    }\n  }\n}\n"}],"categories":["actions"],"meta":{"tier":"free","license":"MIT","family":"actions","docs":"/docs/components/split-button"}}