{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "split-button",
  "type": "registry:ui",
  "title": "Split Button",
  "description": "Fluent 2-styled split button with shadcn-style APIs. Three composable parts (SplitButton, SplitButtonAction, SplitButtonTrigger) join a primary command to an icon-only chevron trigger, sharing variant/size via context and styled through buttonVariants. The chevron composes with the kit DropdownMenu via Base UI's render prop.",
  "author": "graundtech <https://github.com/graundtech/fluent2-react-kit>",
  "dependencies": [
    "class-variance-authority"
  ],
  "registryDependencies": [
    "https://fluent2-react-kit.graund.io/r/utils.json",
    "https://fluent2-react-kit.graund.io/r/button.json"
  ],
  "files": [
    {
      "path": "components/ui/split-button.tsx",
      "type": "registry:ui",
      "target": "components/ui/split-button.tsx",
      "content": "\"use client\";\n\nimport type { VariantProps } from \"class-variance-authority\";\nimport { createContext, useContext } from \"react\";\nimport type { ComponentProps } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { buttonVariants } from \"./button\";\n\n/**\n * SplitButton — Fluent 2-styled, kit-original split button family.\n *\n * A split button is a primary command visually joined to a smaller secondary\n * trigger that opens a menu — Word's \"Paste ▾\", the Ribbon's \"Underline ▾\", etc.\n * Clicking the main part runs the default command; clicking the chevron opens a\n * menu of alternatives. This kit item is the anchor for an upcoming Office-style\n * Ribbon, but stands alone.\n *\n * ## Fluent 2 mapping / no shadcn counterpart\n * Maps to Fluent 2's **SplitButton**. There is **no** shadcn/ui or Base UI\n * split-button primitive, so this is a **kit-original composition** designed\n * conservatively within kit idioms — it is nothing but two `buttonVariants`\n * buttons joined at the seam, plus a tiny context so the two parts share the\n * root's `variant`/`size`. It carries no menu behavior of its own: the menu is\n * supplied by composing the chevron with the kit `DropdownMenu` (recipe below),\n * which is why `dropdown-menu` is **not** a dependency of this file.\n *\n * Parts (shadcn-style compound API):\n *\n * | Part                 | Element                | Role                                   |\n * | -------------------- | ---------------------- | -------------------------------------- |\n * | `SplitButton`        | `div` `role=\"group\"`   | `inline-flex isolate` join + context   |\n * | `SplitButtonAction`  | `button`               | the primary command (`rounded-r-none`) |\n * | `SplitButtonTrigger` | `button` (icon-only)   | the chevron secondary (`rounded-l-none`)|\n *\n * `SplitButton` publishes its `variant`/`size` through a file-local context;\n * both parts read it, and either part can still override locally by passing its\n * own `variant`/`size` prop. The axes are exactly `Button`'s (`variant`: default\n * | secondary | outline | ghost | destructive | link; `size`: sm | default |\n * lg). The Trigger maps the shared text size to the matching square icon size\n * (sm→icon-sm, default→icon, lg→icon-lg) so its height always matches the Action.\n *\n * ## The joining divider (token decision)\n * Fluent draws a 1px seam between the two parts. Its color has to read on both\n * filled and outlined surfaces, so it is chosen per variant and pulled into a\n * single hairline with `-ml-px` (collapses the Action's right edge and the\n * Trigger's left edge into one line on bordered variants):\n *\n * - **default** (brand fill): `border-l-brand-60 dark:border-l-brand-90` — a\n *   darker brand line in light, a lighter brand line in dark; both read clearly\n *   against the `bg-primary` fill (brand-80 light / brand-70 dark). This is\n *   Fluent's \"darker brand divider\" on the primary split button.\n * - **destructive** (red fill): `border-l-destructive-foreground/30` — a subtle\n *   translucent-white seam on the red fill, in both themes.\n * - **secondary / ghost**: `border-l-border` — the neutral `--border` seam.\n * - **outline**: `border-l-input` — matches the part's own `border-input` frame\n *   so the seam is continuous with the outline.\n *\n * All four are token-driven (§3.7 — never a hardcoded color) and validated in\n * light and dark.\n *\n * ## Keyboard model — two tab stops (WAI / Fluent)\n * The Action and the Trigger are **two separate tab stops**, exactly as WAI-ARIA\n * and Fluent 2 specify for split buttons: Tab lands on the Action (Enter/Space\n * runs the command), Tab again lands on the Trigger (Enter/Space/↓ opens the\n * menu). They are two real `<button>`s, so this falls out of native semantics —\n * no roving tabindex, no `role` juggling. `role=\"group\"` on the root just\n * associates the pair for assistive tech.\n *\n * ## Accessibility\n * The Action is an ordinary button (name from its children). The Trigger is\n * icon-only, so it **must** have an accessible name: it defaults to\n * `aria-label=\"More options\"`, which consumers should override with something\n * specific to the command (e.g. `aria-label=\"Paste options\"`). The\n * `aria-haspopup=\"menu\"` / `aria-expanded` state is **not** hardcoded here — it\n * arrives from the `DropdownMenuTrigger` composition (verified in the tests), so\n * a bare `SplitButtonTrigger` (no menu) carries neither, and a composed one\n * carries both.\n *\n * ## Composition recipe (with the kit DropdownMenu)\n * Base UI parts compose via a `render` prop (not shadcn's `asChild`), so the\n * chevron is handed to the menu trigger as `render={<SplitButtonTrigger />}`:\n *\n * ```tsx\n * <DropdownMenu>\n *   <SplitButton variant=\"default\">\n *     <SplitButtonAction onClick={paste}>Paste</SplitButtonAction>\n *     <DropdownMenuTrigger\n *       render={<SplitButtonTrigger aria-label=\"Paste options\" />}\n *     />\n *   </SplitButton>\n *   <DropdownMenuContent>\n *     <DropdownMenuItem>Keep Source Formatting</DropdownMenuItem>\n *     <DropdownMenuItem>Merge Formatting</DropdownMenuItem>\n *     <DropdownMenuItem>Keep Text Only</DropdownMenuItem>\n *   </DropdownMenuContent>\n * </DropdownMenu>\n * ```\n *\n * The Action fires its `onClick` independently and never opens the menu; only\n * the chevron does.\n *\n * ## `\"use client\"` — required\n * This file owns a React context (`createContext`/`useContext`) to share the\n * root's `variant`/`size` with its parts, which is a client-only feature, so the\n * whole file is a client boundary. The chevron glyph is an **inline `<svg>`**\n * (the breadcrumb/pagination precedent), deliberately *not* `@fluentui/react-icons`,\n * so nothing here pulls the icon package's `__styles()`-at-module-scope problem\n * into play (conventions §9).\n */\n\ntype SplitButtonVariant = NonNullable<\n  VariantProps<typeof buttonVariants>[\"variant\"]\n>;\ntype SplitButtonSize = \"sm\" | \"default\" | \"lg\";\n\n/** Per-variant divider color for the 1px joining seam (see doc comment). */\nconst dividerByVariant: Record<SplitButtonVariant, string> = {\n  default: \"border-l-brand-60 dark:border-l-brand-90\",\n  destructive: \"border-l-destructive-foreground/30\",\n  secondary: \"border-l-border\",\n  outline: \"border-l-input\",\n  ghost: \"border-l-border\",\n  link: \"border-l-border\",\n};\n\n/** The shared text size maps to the matching square icon size on the Trigger. */\nconst triggerSizeBySize: Record<\n  SplitButtonSize,\n  NonNullable<VariantProps<typeof buttonVariants>[\"size\"]>\n> = {\n  sm: \"icon-sm\",\n  default: \"icon\",\n  lg: \"icon-lg\",\n};\n\ntype SplitButtonContextValue = {\n  variant: SplitButtonVariant;\n  size: SplitButtonSize;\n};\n\nconst SplitButtonContext = createContext<SplitButtonContextValue>({\n  variant: \"default\",\n  size: \"default\",\n});\n\ntype SplitButtonAxes = {\n  variant?: SplitButtonVariant;\n  size?: SplitButtonSize;\n};\n\n/**\n * SplitButton (root) — the `inline-flex isolate` group that joins the two parts\n * and shares `variant`/`size` with them via context. `isolate` gives the pair\n * its own stacking context so a focused part's ring/seam paints above its\n * neighbor.\n */\nfunction SplitButton({\n  className,\n  variant = \"default\",\n  size = \"default\",\n  ...props\n}: ComponentProps<\"div\"> & SplitButtonAxes) {\n  return (\n    <SplitButtonContext.Provider value={{ variant, size }}>\n      <div\n        role=\"group\"\n        data-slot=\"split-button\"\n        data-variant={variant}\n        data-size={size}\n        className={cn(\"isolate inline-flex\", className)}\n        {...props}\n      />\n    </SplitButtonContext.Provider>\n  );\n}\n\n/**\n * SplitButtonAction — the primary command. A normal button, styled through\n * `buttonVariants` with `rounded-r-none` so its right edge is flat against the\n * Trigger. Inherits `variant`/`size` from the root, overridable locally.\n */\nfunction SplitButtonAction({\n  className,\n  variant,\n  size,\n  ...props\n}: ComponentProps<\"button\"> & SplitButtonAxes) {\n  const ctx = useContext(SplitButtonContext);\n  const resolvedVariant = variant ?? ctx.variant;\n  const resolvedSize = size ?? ctx.size;\n\n  return (\n    <button\n      {...props}\n      data-slot=\"split-button-action\"\n      data-variant={resolvedVariant}\n      data-size={resolvedSize}\n      className={cn(\n        buttonVariants({ variant: resolvedVariant, size: resolvedSize }),\n        // join on the right + lift the focus ring above the Trigger\n        \"relative rounded-r-none focus-visible:z-10\",\n        className\n      )}\n    />\n  );\n}\n\n/**\n * SplitButtonTrigger — the icon-only chevron secondary. Styled through\n * `buttonVariants` at the square icon size for the shared `size`, with\n * `rounded-l-none` and the 1px joining divider (`-ml-px` + a per-variant\n * `border-l` color). Composes as a menu trigger via Base UI's `render` prop:\n * `<DropdownMenuTrigger render={<SplitButtonTrigger />} />` — which is why props\n * are spread *first* here, so the menu's injected behavior (onClick,\n * `aria-haspopup`, `aria-expanded`, id, ref, keyboard handlers) flows through\n * while this part keeps its own identity/presentation attributes.\n *\n * Defaults to a down chevron and `aria-label=\"More options\"`; both are\n * overridable (pass children to replace the glyph, `aria-label` to name it).\n */\nfunction SplitButtonTrigger({\n  className,\n  variant,\n  size,\n  children,\n  \"aria-label\": ariaLabel = \"More options\",\n  ...props\n}: ComponentProps<\"button\"> & SplitButtonAxes) {\n  const ctx = useContext(SplitButtonContext);\n  const resolvedVariant = variant ?? ctx.variant;\n  const resolvedSize = size ?? ctx.size;\n\n  return (\n    <button\n      {...props}\n      data-slot=\"split-button-trigger\"\n      data-variant={resolvedVariant}\n      data-size={resolvedSize}\n      aria-label={ariaLabel}\n      className={cn(\n        buttonVariants({\n          variant: resolvedVariant,\n          size: triggerSizeBySize[resolvedSize],\n        }),\n        // join on the left, collapse the seam to 1px, lift the focus ring\n        \"relative -ml-px rounded-l-none border-l focus-visible:z-10\",\n        // per-variant divider color (reads on filled and outlined surfaces)\n        dividerByVariant[resolvedVariant],\n        className\n      )}\n    >\n      {children ?? <SplitButtonChevron />}\n    </button>\n  );\n}\n\n/**\n * The default chevron glyph — an inline `<svg>` (breadcrumb/pagination\n * precedent), sized 16px, inheriting `currentColor`. `aria-hidden` so the\n * button's `aria-label` is its only accessible name.\n */\nfunction SplitButtonChevron() {\n  return (\n    <svg viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\" className=\"size-4\">\n      <path\n        d=\"M6 8L10 12L14 8\"\n        stroke=\"currentColor\"\n        strokeWidth=\"1.5\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\nexport { SplitButton, SplitButtonAction, SplitButtonTrigger };\n"
    }
  ]
}
