{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"An interactive AI branch component for displaying conversation flows.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { ChevronLeftIcon, ChevronRightIcon, Copy, Pencil } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type { HTMLAttributes, ReactElement, ReactNode } from \"react\";\nimport { createContext, useContext, useEffect, useMemo, useState } from \"react\";\n\ninterface AIBranchContextType {\n  branches: ReactElement[];\n  currentBranch: number;\n  goToNext: () => void;\n  goToPrevious: () => void;\n  setBranches: (branches: ReactElement[]) => void;\n  totalBranches: number;\n}\n\n/**\n * The same two springs every other AI component uses.\n *\n * This file predates that vocabulary and had eight hand-tuned\n * `stiffness`/`damping` pairs — several of which also passed `duration`, which\n * Motion ignores once stiffness is present. Sharing the tokens is what stops\n * `ai-branch` feeling like it came from a different library than `ai-message`.\n */\nconst SPRING_DEFAULT = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\nconst SPRING_SNAPPY = {\n  bounce: 0,\n  duration: 0.2,\n  type: \"spring\" as const,\n};\n\nconst AIBranchContext = createContext<AIBranchContextType | null>(null);\n\nconst useAIBranch = () => {\n  const context = useContext(AIBranchContext);\n  if (!context) {\n    throw new Error(\"AIBranch components must be used within AIBranch\");\n  }\n  return context;\n};\n\nexport type AIBranchProps = HTMLAttributes<HTMLDivElement> & {\n  defaultBranch?: number;\n  onBranchChange?: (branchIndex: number) => void;\n};\n\nexport const AIBranch = ({\n  defaultBranch = 0,\n  onBranchChange,\n  className,\n  ...props\n}: AIBranchProps) => {\n  const [currentBranch, setCurrentBranch] = useState(defaultBranch);\n  const [branches, setBranches] = useState<ReactElement[]>([]);\n\n  const handleBranchChange = (newBranch: number) => {\n    setCurrentBranch(newBranch);\n    onBranchChange?.(newBranch);\n  };\n\n  const goToPrevious = () => {\n    const newBranch =\n      currentBranch > 0 ? currentBranch - 1 : branches.length - 1;\n    handleBranchChange(newBranch);\n  };\n\n  const goToNext = () => {\n    const newBranch =\n      currentBranch < branches.length - 1 ? currentBranch + 1 : 0;\n    handleBranchChange(newBranch);\n  };\n\n  const contextValue: AIBranchContextType = {\n    branches,\n    currentBranch,\n    goToNext,\n    goToPrevious,\n    setBranches,\n    totalBranches: branches.length,\n  };\n\n  return (\n    <AIBranchContext.Provider value={contextValue}>\n      <div\n        className={cn(\"grid w-full gap-2 [&>div]:pb-0\", className)}\n        {...props}\n      />\n    </AIBranchContext.Provider>\n  );\n};\n\nexport interface AIBranchMessagesProps {\n  children: ReactElement | ReactElement[];\n}\n\nexport const AIBranchMessages = ({ children }: AIBranchMessagesProps) => {\n  const { currentBranch, setBranches, branches } = useAIBranch();\n  const shouldReduceMotion = useReducedMotion();\n  const childrenArray = useMemo(\n    () => (Array.isArray(children) ? children : [children]),\n    [children]\n  );\n\n  // Use useEffect to update branches when they change\n  useEffect(() => {\n    if (branches.length !== childrenArray.length) {\n      setBranches(childrenArray);\n    }\n  }, [childrenArray, branches, setBranches]);\n\n  return childrenArray.map((branch, index) => (\n    <motion.div\n      animate={\n        shouldReduceMotion\n          ? { opacity: index === currentBranch ? 1 : 0 }\n          : {\n              display: index === currentBranch ? \"block\" : \"none\",\n              opacity: index === currentBranch ? 1 : 0,\n              y: index === currentBranch ? 0 : 10,\n            }\n      }\n      className={cn(\n        \"grid gap-2 [&>div]:pb-0\",\n        index === currentBranch ? \"block\" : \"hidden\"\n      )}\n      initial={shouldReduceMotion ? { opacity: 0 } : { opacity: 0, y: 10 }}\n      key={`branch-${index}-${currentBranch}`}\n      transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n    >\n      {branch}\n    </motion.div>\n  ));\n};\n\nexport type AIBranchSelectorProps = HTMLAttributes<HTMLDivElement> & {\n  from: \"user\" | \"assistant\";\n};\n\nexport const AIBranchSelector = ({\n  className,\n  from,\n  ...props\n}: AIBranchSelectorProps) => {\n  const { totalBranches } = useAIBranch();\n\n  // Don't render if there's only one branch\n  if (totalBranches <= 1) {\n    return null;\n  }\n\n  return (\n    <div\n      className={cn(\n        \"flex items-center gap-2 self-end px-10\",\n        from === \"assistant\" ? \"justify-start\" : \"justify-end\",\n        className\n      )}\n      {...props}\n    />\n  );\n};\n\nexport interface AIBranchPreviousProps {\n  children?: ReactNode;\n  className?: string;\n}\n\nexport const AIBranchPrevious = ({\n  className,\n  children,\n}: AIBranchPreviousProps) => {\n  const { goToPrevious, totalBranches } = useAIBranch();\n  const shouldReduceMotion = useReducedMotion();\n\n  return (\n    <motion.button\n      aria-label=\"Previous branch\"\n      className={cn(\n        \"size-7 shrink-0 cursor-pointer rounded-full text-muted-foreground transition-colors\",\n        \"hover:bg-muted hover:text-foreground\",\n        \"disabled:pointer-events-none disabled:opacity-50\",\n        \"flex items-center justify-center\",\n        className\n      )}\n      disabled={totalBranches <= 1}\n      onClick={goToPrevious}\n      transition={shouldReduceMotion ? { duration: 0 } : SPRING_SNAPPY}\n      type=\"button\"\n      whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}\n      whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}\n    >\n      {children ?? <ChevronLeftIcon size={14} />}\n    </motion.button>\n  );\n};\n\nexport interface AIBranchNextProps {\n  children?: ReactNode;\n  className?: string;\n}\n\nexport const AIBranchNext = ({ className, children }: AIBranchNextProps) => {\n  const { goToNext, totalBranches } = useAIBranch();\n  const shouldReduceMotion = useReducedMotion();\n\n  return (\n    <motion.button\n      aria-label=\"Next branch\"\n      className={cn(\n        \"size-7 shrink-0 cursor-pointer rounded-full text-muted-foreground transition-colors\",\n        \"hover:bg-muted hover:text-foreground\",\n        \"disabled:pointer-events-none disabled:opacity-50\",\n        \"flex items-center justify-center\",\n        className\n      )}\n      disabled={totalBranches <= 1}\n      onClick={goToNext}\n      transition={shouldReduceMotion ? { duration: 0 } : SPRING_SNAPPY}\n      type=\"button\"\n      whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}\n      whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}\n    >\n      {children ?? <ChevronRightIcon size={14} />}\n    </motion.button>\n  );\n};\n\nexport interface AIBranchPageProps {\n  className?: string;\n}\n\nexport const AIBranchPage = ({ className }: AIBranchPageProps) => {\n  const { currentBranch, totalBranches } = useAIBranch();\n\n  return (\n    <span\n      className={cn(\n        \"font-medium text-muted-foreground text-xs tabular-nums\",\n        className\n      )}\n    >\n      {currentBranch + 1} of {totalBranches}\n    </span>\n  );\n};\n\n// Updated type for conversation branches\nexport interface AIBranchData {\n  aiResponse: string;\n  id: string;\n  isActive: boolean;\n  timestamp: Date;\n  userMessage: string;\n}\n\n// Export the type alias for backward compatibility\nexport type AIBranch = AIBranchData;\n\ninterface LegacyAiBranchProps {\n  branches: AIBranchData[];\n  className?: string;\n  onBranchSelect: (branchId: string) => void;\n}\n\n// Updated legacy component to show conversation branches\nexport function LegacyAiBranch({\n  branches,\n  onBranchSelect,\n  className,\n}: LegacyAiBranchProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const [currentBranchIndex, setCurrentBranchIndex] = useState(() =>\n    branches.findIndex((branch) => branch.isActive)\n  );\n\n  const activeBranch = branches[currentBranchIndex];\n\n  const goToPrevious = () => {\n    const newIndex =\n      currentBranchIndex > 0 ? currentBranchIndex - 1 : branches.length - 1;\n    setCurrentBranchIndex(newIndex);\n    onBranchSelect(branches[newIndex].id);\n  };\n\n  const goToNext = () => {\n    const newIndex =\n      currentBranchIndex < branches.length - 1 ? currentBranchIndex + 1 : 0;\n    setCurrentBranchIndex(newIndex);\n    onBranchSelect(branches[newIndex].id);\n  };\n\n  return (\n    <div className={cn(\"w-full max-w-2xl\", className)}>\n      {/* Active Branch Display */}\n      {activeBranch ? (\n        <motion.div\n          animate={{ opacity: 1, y: 0 }}\n          className=\"mb-4 space-y-4\"\n          initial={{ opacity: 0, y: 10 }}\n          transition={SPRING_DEFAULT}\n        >\n          {/* User Message with Branch Navigation */}\n          <div className=\"flex justify-end\">\n            <div className=\"flex flex-col items-end gap-2\">\n              <div className=\"max-w-full rounded-2xl rounded-br-md bg-foreground px-3.5 py-2.5 text-background\">\n                <p className=\"text-sm leading-relaxed\">\n                  {activeBranch.userMessage}\n                </p>\n              </div>\n\n              {/* Branch Navigation Controls */}\n              {branches.length > 1 && (\n                <div className=\"flex items-center gap-1\">\n                  <motion.button\n                    aria-label=\"Copy message\"\n                    className={cn(\n                      \"size-6 shrink-0 cursor-pointer rounded text-foreground/70 transition-colors\",\n                      \"hover:bg-muted hover:text-foreground\",\n                      \"flex items-center justify-center\"\n                    )}\n                    transition={\n                      shouldReduceMotion ? { duration: 0 } : SPRING_SNAPPY\n                    }\n                    type=\"button\"\n                    whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}\n                    whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}\n                  >\n                    <Copy className=\"h-3 w-3\" />\n                  </motion.button>\n\n                  <motion.button\n                    aria-label=\"Edit message\"\n                    className={cn(\n                      \"size-6 shrink-0 cursor-pointer rounded text-foreground/70 transition-colors\",\n                      \"hover:bg-muted hover:text-foreground\",\n                      \"flex items-center justify-center\"\n                    )}\n                    transition={\n                      shouldReduceMotion ? { duration: 0 } : SPRING_SNAPPY\n                    }\n                    type=\"button\"\n                    whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}\n                    whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}\n                  >\n                    <Pencil className=\"h-3 w-3\" />\n                  </motion.button>\n\n                  <motion.button\n                    aria-label=\"Previous branch\"\n                    className={cn(\n                      \"size-6 shrink-0 cursor-pointer rounded text-foreground/70 transition-colors\",\n                      \"hover:bg-muted hover:text-foreground\",\n                      \"disabled:pointer-events-none disabled:opacity-50\",\n                      \"flex items-center justify-center\"\n                    )}\n                    disabled={branches.length <= 1}\n                    onClick={goToPrevious}\n                    transition={\n                      shouldReduceMotion ? { duration: 0 } : SPRING_SNAPPY\n                    }\n                    type=\"button\"\n                    whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}\n                    whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}\n                  >\n                    <ChevronLeftIcon size={12} />\n                  </motion.button>\n\n                  <span className=\"font-medium text-foreground/70 text-xs tabular-nums\">\n                    {currentBranchIndex + 1}/{branches.length}\n                  </span>\n\n                  <motion.button\n                    aria-label=\"Next branch\"\n                    className={cn(\n                      \"size-6 shrink-0 cursor-pointer rounded text-foreground/70 transition-colors\",\n                      \"hover:bg-muted hover:text-foreground\",\n                      \"disabled:pointer-events-none disabled:opacity-50\",\n                      \"flex items-center justify-center\"\n                    )}\n                    disabled={branches.length <= 1}\n                    onClick={goToNext}\n                    transition={\n                      shouldReduceMotion ? { duration: 0 } : SPRING_SNAPPY\n                    }\n                    type=\"button\"\n                    whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}\n                    whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}\n                  >\n                    <ChevronRightIcon size={12} />\n                  </motion.button>\n                </div>\n              )}\n            </div>\n          </div>\n\n          {/* AI Response */}\n          <div className=\"flex justify-start\">\n            <div className=\"max-w-[80%] rounded-2xl rounded-bl-md bg-muted px-3.5 py-2.5\">\n              <p className=\"text-foreground text-sm leading-relaxed\">\n                {activeBranch.aiResponse}\n              </p>\n            </div>\n          </div>\n        </motion.div>\n      ) : null}\n    </div>\n  );\n}\n\n// Export the legacy component as the default for backward compatibility\nexport { LegacyAiBranch as AiBranch };\n\n// Add default export for lazy loading\nexport default LegacyAiBranch;\n","path":"index.tsx","target":"components/smoothui/ai-branch/index.tsx","type":"registry:ui"}],"name":"ai-branch","registryDependencies":[],"title":"Ai Branch","type":"registry:ui"}