{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "branch",
  "title": "Message Branch",
  "author": "Jamie Thompson <jamie@jamiethompson.design> (https://jamiethompson.design)",
  "description": "Branch switcher for an AI chat message that has several alternative responses (regenerations, forks). MessageBranch holds the current index, MessageBranchContent shows one child branch at a time, and MessageBranchSelector composes MessageBranchPrevious, MessageBranchPage (\"2 of 3\") and MessageBranchNext in a button-group with wrap-around navigation. Use it inside a message to let users flip between regenerated answers; pairs with @uifiles/response for markdown branches.",
  "dependencies": [
    "cn",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "button-group"
  ],
  "files": [
    {
      "path": "registry/ai/branch.tsx",
      "content": "// Derived from Vercel AI Elements message.tsx (Apache-2.0, Copyright 2023 Vercel, Inc.).\n// Modified for uifiles: ported from Radix UI to Base UI; dependencies point at @uifiles.\n\"use client\"\n\nimport { cn } from \"cn\"\nimport { ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\"\nimport type { ComponentProps, HTMLAttributes, ReactNode } from \"react\"\nimport {\n  Children,\n  createContext,\n  Fragment,\n  isValidElement,\n  useCallback,\n  useContext,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\"\nimport { Button } from \"@/components/ui/button\"\nimport { ButtonGroup, ButtonGroupText } from \"@/components/ui/button-group\"\n\ninterface MessageBranchContextType {\n  /** The requested index before clamping; content clamps it against its own child count. */\n  branchIndex: number\n  currentBranch: number\n  totalBranches: number\n  /** The count MessageBranch read off its own children; undefined when it could not see the content. */\n  derivedTotal: number | undefined\n  goToPrevious: () => void\n  goToNext: () => void\n  registerTotal: (total: number) => void\n}\n\nconst MessageBranchContext = createContext<MessageBranchContextType | null>(\n  null\n)\n\nconst useMessageBranch = () => {\n  const context = useContext(MessageBranchContext)\n\n  if (!context) {\n    throw new Error(\n      \"MessageBranch components must be used within MessageBranch\"\n    )\n  }\n\n  return context\n}\n\nconst clampBranch = (index: number, total: number) =>\n  total === 0 ? 0 : Math.min(Math.max(index, 0), total - 1)\n\n// Children.toArray drops null/boolean children (conditional branches),\n// resolves Server Component children (keyless lazy nodes) and keys every\n// element, so a branch keeps its identity across navigation.\nconst branchesOf = (children: ReactNode) => Children.toArray(children)\n\nconst LAZY = Symbol.for(\"react.lazy\")\nconst MEMO = Symbol.for(\"react.memo\")\nconst FORWARD_REF = Symbol.for(\"react.forward_ref\")\n\n/**\n * The component behind an element type. A Server Component hands its client\n * children over as lazy types (the Flight client's reference to the module\n * export), which React resolves only when it renders them; memo and\n * forwardRef wrap a component in an object.\n */\nfunction componentOf(type: unknown): unknown {\n  if (typeof type !== \"object\" || type === null || !(\"$$typeof\" in type)) {\n    return type\n  }\n  if (type.$$typeof === MEMO && \"type\" in type) return componentOf(type.type)\n  if (type.$$typeof === FORWARD_REF && \"render\" in type) {\n    return componentOf(type.render)\n  }\n  if (\n    type.$$typeof === LAZY &&\n    \"_init\" in type &&\n    \"_payload\" in type &&\n    typeof type._init === \"function\"\n  ) {\n    // A module still loading throws its promise: MessageBranch suspends on\n    // it, as React would on the element itself a moment later, and counts\n    // once it is in.\n    return componentOf(type._init(type._payload))\n  }\n  return type\n}\n\n/**\n * The branch count of the first MessageBranchContent among `children`, looked\n * for through fragments and host elements. Reading it while rendering keeps\n * the selector and page count in server HTML; a content rendered by a custom\n * component cannot be seen here and registers its count after mount instead.\n */\nfunction countBranches(children: ReactNode): number | undefined {\n  for (const child of Children.toArray(children)) {\n    if (!isValidElement<{ children?: ReactNode }>(child)) continue\n    if (componentOf(child.type) === MessageBranchContent) {\n      return branchesOf(child.props.children).length\n    }\n    if (child.type === Fragment || typeof child.type === \"string\") {\n      const nested = countBranches(child.props.children)\n      if (nested !== undefined) return nested\n    }\n  }\n  return undefined\n}\n\nexport type MessageBranchProps = HTMLAttributes<HTMLDivElement> & {\n  branch?: number | undefined\n  defaultBranch?: number | undefined\n  onBranchChange?: ((branchIndex: number) => void) | undefined\n}\n\nexport const MessageBranch = ({\n  branch,\n  defaultBranch = 0,\n  onBranchChange,\n  className,\n  children,\n  ...props\n}: MessageBranchProps) => {\n  const onBranchChangeRef = useRef(onBranchChange)\n  useEffect(() => {\n    onBranchChangeRef.current = onBranchChange\n  })\n  const [uncontrolledBranch, setUncontrolledBranch] = useState(defaultBranch)\n  const [registeredTotal, setRegisteredTotal] = useState(0)\n  const derivedTotal = countBranches(children)\n  const totalBranches = derivedTotal ?? registeredTotal\n  const isControlled = branch !== undefined\n  const branchIndex = isControlled ? branch : uncontrolledBranch\n  const currentBranch = clampBranch(branchIndex, totalBranches)\n\n  // A controlled branch that clamping moved (out of range, or the list\n  // shrank below it) is reported once, so the parent's state follows what\n  // is shown; the ref keeps StrictMode's replayed effect from reporting the\n  // same clamp twice.\n  const reportedClampRef = useRef<string | null>(null)\n  useEffect(() => {\n    if (!isControlled || totalBranches === 0 || branch === currentBranch) {\n      reportedClampRef.current = null\n      return\n    }\n    const clamp = `${branch}>${currentBranch}`\n    if (reportedClampRef.current === clamp) return\n    reportedClampRef.current = clamp\n    onBranchChangeRef.current?.(currentBranch)\n  }, [isControlled, branch, currentBranch, totalBranches])\n\n  const setBranch = useCallback(\n    (next: number) => {\n      if (!isControlled) setUncontrolledBranch(next)\n      onBranchChangeRef.current?.(next)\n    },\n    [isControlled]\n  )\n\n  const goToPrevious = useCallback(() => {\n    if (totalBranches === 0) return\n    setBranch(currentBranch > 0 ? currentBranch - 1 : totalBranches - 1)\n  }, [currentBranch, totalBranches, setBranch])\n\n  const goToNext = useCallback(() => {\n    if (totalBranches === 0) return\n    setBranch(currentBranch < totalBranches - 1 ? currentBranch + 1 : 0)\n  }, [currentBranch, totalBranches, setBranch])\n\n  const contextValue = useMemo<MessageBranchContextType>(\n    () => ({\n      branchIndex,\n      currentBranch,\n      derivedTotal,\n      goToNext,\n      goToPrevious,\n      registerTotal: setRegisteredTotal,\n      totalBranches,\n    }),\n    [\n      branchIndex,\n      currentBranch,\n      derivedTotal,\n      goToNext,\n      goToPrevious,\n      totalBranches,\n    ]\n  )\n\n  return (\n    <MessageBranchContext.Provider value={contextValue}>\n      <div\n        className={cn(\"grid w-full gap-2 [&>div]:pb-0\", className)}\n        {...props}\n      >\n        {children}\n      </div>\n    </MessageBranchContext.Provider>\n  )\n}\n\nexport type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>\n\nexport const MessageBranchContent = ({\n  children,\n  className,\n  ...props\n}: MessageBranchContentProps) => {\n  const { branchIndex, derivedTotal, registerTotal } = useMessageBranch()\n  const branches = branchesOf(children)\n  const currentBranch = clampBranch(branchIndex, branches.length)\n\n  // Only content MessageBranch could not count itself registers, before\n  // paint so the selector does not flash in.\n  useLayoutEffect(() => {\n    if (derivedTotal !== undefined) return\n    registerTotal(branches.length)\n    return () => registerTotal(0)\n  }, [derivedTotal, branches.length, registerTotal])\n\n  return branches.map((branch, index) => (\n    <div\n      className={cn(\n        \"grid gap-2 overflow-hidden [&>div]:pb-0\",\n        index === currentBranch ? \"block\" : \"hidden\",\n        className\n      )}\n      key={isValidElement(branch) ? branch.key : index}\n      {...props}\n    >\n      {branch}\n    </div>\n  ))\n}\n\nexport type MessageBranchSelectorProps = ComponentProps<typeof ButtonGroup>\n\nexport const MessageBranchSelector = ({\n  className,\n  ...props\n}: MessageBranchSelectorProps) => {\n  const { totalBranches } = useMessageBranch()\n\n  // Don't render if there's only one branch\n  if (totalBranches <= 1) {\n    return null\n  }\n\n  return (\n    <ButtonGroup\n      className={cn(\n        \"[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md\",\n        className\n      )}\n      orientation=\"horizontal\"\n      {...props}\n    />\n  )\n}\n\nexport type MessageBranchPreviousProps = ComponentProps<typeof Button>\n\nexport const MessageBranchPrevious = ({\n  children,\n  ...props\n}: MessageBranchPreviousProps) => {\n  const { goToPrevious, totalBranches } = useMessageBranch()\n\n  return (\n    <Button\n      aria-label=\"Previous branch\"\n      disabled={totalBranches <= 1}\n      onClick={goToPrevious}\n      size=\"icon-sm\"\n      type=\"button\"\n      variant=\"ghost\"\n      {...props}\n    >\n      {children ?? <ChevronLeftIcon size={14} />}\n    </Button>\n  )\n}\n\nexport type MessageBranchNextProps = ComponentProps<typeof Button>\n\nexport const MessageBranchNext = ({\n  children,\n  ...props\n}: MessageBranchNextProps) => {\n  const { goToNext, totalBranches } = useMessageBranch()\n\n  return (\n    <Button\n      aria-label=\"Next branch\"\n      disabled={totalBranches <= 1}\n      onClick={goToNext}\n      size=\"icon-sm\"\n      type=\"button\"\n      variant=\"ghost\"\n      {...props}\n    >\n      {children ?? <ChevronRightIcon size={14} />}\n    </Button>\n  )\n}\n\nexport type MessageBranchPageProps = ComponentProps<typeof ButtonGroupText>\n\nexport const MessageBranchPage = ({\n  className,\n  ...props\n}: MessageBranchPageProps) => {\n  const { currentBranch, totalBranches } = useMessageBranch()\n\n  return (\n    <ButtonGroupText\n      className={cn(\n        \"border-none bg-transparent text-muted-foreground shadow-none\",\n        className\n      )}\n      {...props}\n    >\n      {totalBranches === 0 ? 0 : currentBranch + 1} of {totalBranches}\n    </ButtonGroupText>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ai/branch.tsx"
    }
  ],
  "docs": "Derived from Vercel AI Elements (Apache-2.0, Copyright 2023 Vercel, Inc.); ported to Base UI and maintained by uifiles. Built for the Base UI styles (base-nova): it composes the Base UI wrappers from components/ui and relies on their render prop and event signatures, so it does not work on Radix or React Aria styles. Extracted from AI Elements message.tsx (the MessageBranch* family only; shadcn's own message replaces the rest). Same export names and props as upstream, plus an optional controlled `branch` prop (pair it with `onBranchChange`; `defaultBranch` stays uncontrolled). MessageBranch counts the branches of its MessageBranchContent while rendering: it finds the content among its own children and inside fragments and plain elements, also when the content is wrapped in memo or forwardRef or is a client reference a Server Component hands over in the App Router (a reference whose module is still loading suspends MessageBranch until it is in), so the selector and MessageBranchPage (\"2 of 3\") are in server-rendered HTML, including a page that is a Server Component, and hydrate without a warning; content rendered by a custom component cannot be counted that way and registers after mount, so its selector appears on the client. The current branch is clamped into range, so an out-of-range `defaultBranch`/`branch` or a shrinking branch list never shows an empty page, and MessageBranchPage reads \"0 of 0\" while no content is mounted. When clamping moves a controlled `branch` (out of range, or the list shrank below it), `onBranchChange` is called once with the clamped index (once under StrictMode too) so the parent's state follows; an uncontrolled index is remembered, so a branch that is removed and restored shows again. MessageBranchContent ignores null/boolean children (conditional branches) and accepts children rendered by a Server Component (RSC lazy nodes resolve without key warnings). MessageBranchPage renders ButtonGroupText (a div; upstream typed it as a span) and accepts its `render` prop. The buttons are the Base UI Button primitive (nativeButton), so onClick/disabled/type behave as before; no asChild anywhere.",
  "categories": [
    "ai",
    "chat"
  ],
  "type": "registry:component"
}