{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"message-bubble","type":"registry:component","title":"Message Bubble","description":"A focused conversational surface with visual tones, independent alignment, grouped messages, expandable content, and interactive link or button support.","author":"Saurabh <saurabh10102@gmail.com>","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/agents/message-bubble.tsx","type":"registry:component","target":"@components/agents/message-bubble.tsx","content":"\"use client\";\n// beui.dev/components/agents/message-bubble\n\nimport { ChevronDown } from \"lucide-react\";\nimport {\n  type HTMLMotionProps,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  cloneElement,\n  type ComponentPropsWithRef,\n  createContext,\n  type ReactElement,\n  type ReactNode,\n  type Ref,\n  useCallback,\n  useContext,\n  useId,\n  useState,\n} from \"react\";\nimport {\n  EASE_OUT,\n  SPRING_LAYOUT,\n  SPRING_SWAP,\n} from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\nimport { MessageSideContext } from \"@/components/agents/message-context\";\n\nexport type MessageBubbleVariant =\n  | \"solid\"\n  | \"soft\"\n  | \"tint\"\n  | \"outline\"\n  | \"ghost\"\n  | \"danger\";\nexport type MessageBubbleAlign = \"start\" | \"end\";\n\ninterface MessageBubbleContextValue {\n  align?: MessageBubbleAlign;\n  animateIn: boolean;\n  variant: MessageBubbleVariant;\n}\n\nconst MessageBubbleContext = createContext<MessageBubbleContextValue>({\n  animateIn: true,\n  variant: \"soft\",\n});\nconst MessageBubbleLayoutContext = createContext<() => void>(() => {});\n\nexport interface MessageBubbleProps\n  extends Omit<HTMLMotionProps<\"div\">, \"children\"> {\n  variant?: MessageBubbleVariant;\n  /** Defaults to the surrounding Message alignment when omitted. */\n  align?: MessageBubbleAlign;\n  /** Plays the bubble entrance once when this component mounts. */\n  animateIn?: boolean;\n  children?: ReactNode;\n}\n\nexport interface MessageBubbleContentProps\n  extends ComponentPropsWithRef<\"div\"> {\n  /** Replaces the content element while preserving bubble styling. */\n  render?: ReactElement;\n}\n\nexport interface MessageBubbleGroupProps extends ComponentPropsWithRef<\"div\"> {\n  spacing?: \"compact\" | \"default\";\n}\n\nexport interface MessageBubbleCollapsibleProps\n  extends ComponentPropsWithRef<\"div\"> {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  collapsedLines?: 2 | 3 | 4 | 5 | 6;\n  moreLabel?: ReactNode;\n  lessLabel?: ReactNode;\n  contentClassName?: string;\n  triggerClassName?: string;\n  children?: ReactNode;\n}\n\nfunction mergeRefs<T>(...refs: Array<Ref<T> | undefined>) {\n  return (node: T | null) => {\n    for (const ref of refs) {\n      if (typeof ref === \"function\") ref(node);\n      else if (ref) ref.current = node;\n    }\n  };\n}\n\nconst BUBBLE_CONTENT_REVEAL = {\n  duration: 0.12,\n  ease: EASE_OUT,\n  delay: 0.04,\n} as const;\n\n// Sent bubbles should pop into place quickly with one restrained overshoot.\nconst BUBBLE_POP = {\n  type: \"spring\",\n  stiffness: 520,\n  damping: 27,\n  mass: 0.52,\n} as const;\n\nexport function MessageBubble({\n  variant = \"soft\",\n  align,\n  animateIn = false,\n  className,\n  children,\n  initial,\n  animate,\n  exit,\n  transition,\n  layout,\n  ...props\n}: MessageBubbleProps) {\n  const reduce = useReducedMotion() ?? false;\n  const messageSide = useContext(MessageSideContext);\n  const resolvedAlign = align ?? messageSide ?? \"start\";\n\n  return (\n    <MessageBubbleContext.Provider\n      value={{ align: resolvedAlign, animateIn, variant }}\n    >\n      <motion.div\n        data-slot=\"message-bubble\"\n        data-align={resolvedAlign}\n        data-variant={variant}\n        layout={layout}\n        initial={initial ?? false}\n        animate={animate}\n        exit={\n          exit ??\n          (reduce ? { opacity: 0 } : { opacity: 0, y: -3, scale: 0.99 })\n        }\n        transition={transition ?? (reduce ? { duration: 0.12 } : SPRING_LAYOUT)}\n        className={cn(\n          \"group/bubble flex w-full flex-col\",\n          resolvedAlign === \"end\" ? \"items-end\" : \"items-start\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </motion.div>\n    </MessageBubbleContext.Provider>\n  );\n}\n\nfunction bubbleContentClass(\n  variant: MessageBubbleVariant,\n  interactive: boolean,\n) {\n  return cn(\n    \"relative z-0 min-w-9 max-w-[82%] rounded-2xl px-3.5 py-2.5 text-sm leading-6 text-foreground\",\n    \"[&_a]:font-medium [&_a]:underline [&_a]:underline-offset-4 [&_code]:rounded [&_code]:bg-background/60 [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.9em] [&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-5 [&_p+p]:mt-2 [&_pre]:my-2 [&_pre]:overflow-x-auto [&_pre]:rounded-xl [&_pre]:bg-background/60 [&_pre]:p-3 [&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-5\",\n    variant === \"solid\" && \"text-background\",\n    variant === \"ghost\" && \"w-full max-w-none rounded-none px-0 py-0\",\n    variant === \"danger\" && \"text-destructive\",\n    interactive &&\n      \"cursor-pointer text-left outline-none transition-[background-color,color,transform] duration-150 hover:brightness-[0.98] focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.99]\",\n  );\n}\n\nfunction bubbleSurfaceClass(\n  variant: MessageBubbleVariant,\n  align: MessageBubbleAlign,\n) {\n  return cn(\n    \"pointer-events-none absolute inset-0 -z-10 rounded-[inherit]\",\n    align === \"end\" ? \"origin-bottom-right\" : \"origin-bottom-left\",\n    variant === \"solid\" && \"bg-foreground\",\n    variant === \"soft\" && \"bg-muted\",\n    variant === \"tint\" && \"bg-primary/10\",\n    variant === \"outline\" && \"border border-border/70 bg-background\",\n    variant === \"danger\" && \"bg-destructive/10\",\n  );\n}\n\nexport function MessageBubbleContent({\n  render,\n  className,\n  children,\n  ref,\n  ...props\n}: MessageBubbleContentProps) {\n  const reduce = useReducedMotion() ?? false;\n  const { align = \"start\", animateIn, variant } =\n    useContext(MessageBubbleContext);\n  const [layoutVersion, setLayoutVersion] = useState(0);\n  const notifyLayout = useCallback(\n    () => setLayoutVersion((version) => version + 1),\n    [],\n  );\n  const interactive =\n    render?.type === \"button\" || render?.type === \"a\";\n  const classes = cn(bubbleContentClass(variant, interactive), className);\n  const composedChildren = (\n    <>\n      {variant !== \"ghost\" ? (\n        <motion.span\n          aria-hidden=\"true\"\n          layout={reduce ? false : \"size\"}\n          layoutDependency={layoutVersion}\n          initial={\n            animateIn && !reduce\n              ? {\n                  opacity: 0,\n                  scale: 0.92,\n                }\n              : false\n          }\n          animate={{ opacity: 1, scale: 1 }}\n          transition={\n            reduce\n              ? { duration: 0 }\n              : {\n                  opacity: { duration: 0.12, ease: EASE_OUT },\n                  scale: BUBBLE_POP,\n                  layout: SPRING_LAYOUT,\n                }\n          }\n          className={bubbleSurfaceClass(variant, align)}\n        />\n      ) : null}\n      <MessageBubbleLayoutContext.Provider value={notifyLayout}>\n        <motion.div\n          initial={\n            animateIn\n              ? reduce\n                ? { opacity: 0 }\n                : { opacity: 0 }\n              : false\n          }\n          animate={{ opacity: 1 }}\n          transition={\n            reduce ? { duration: 0.12, ease: EASE_OUT } : BUBBLE_CONTENT_REVEAL\n          }\n          className=\"relative\"\n        >\n          {children}\n        </motion.div>\n      </MessageBubbleLayoutContext.Provider>\n    </>\n  );\n\n  if (render) {\n    const child = render as ReactElement<\n      Record<string, unknown> & { className?: string; ref?: Ref<HTMLElement> }\n    >;\n\n    return cloneElement(child, {\n      ...props,\n      ref: mergeRefs(child.props.ref, ref as Ref<HTMLElement> | undefined),\n      className: cn(classes, child.props.className),\n      children: composedChildren,\n      \"data-slot\": \"message-bubble-content\",\n    });\n  }\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"message-bubble-content\"\n      className={classes}\n      {...props}\n    >\n      {composedChildren}\n    </div>\n  );\n}\n\nexport function MessageBubbleGroup({\n  spacing = \"compact\",\n  className,\n  ...props\n}: MessageBubbleGroupProps) {\n  return (\n    <div\n      data-slot=\"message-bubble-group\"\n      className={cn(\n        \"flex w-full flex-col\",\n        spacing === \"compact\" ? \"gap-1.5\" : \"gap-3\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nconst LINE_CLAMP_CLASS = {\n  2: \"line-clamp-2\",\n  3: \"line-clamp-3\",\n  4: \"line-clamp-4\",\n  5: \"line-clamp-5\",\n  6: \"line-clamp-6\",\n} as const;\n\nexport function MessageBubbleCollapsible({\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  collapsedLines = 4,\n  moreLabel = \"Show more\",\n  lessLabel = \"Show less\",\n  contentClassName,\n  triggerClassName,\n  className,\n  children,\n  ...props\n}: MessageBubbleCollapsibleProps) {\n  const reduce = useReducedMotion() ?? false;\n  const contentId = useId();\n  const notifyLayout = useContext(MessageBubbleLayoutContext);\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const currentOpen = open ?? internalOpen;\n\n  const setOpen = useCallback(\n    (next: boolean) => {\n      notifyLayout();\n      if (open === undefined) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [notifyLayout, onOpenChange, open],\n  );\n\n  return (\n    <div\n      data-slot=\"message-bubble-collapsible\"\n      data-state={currentOpen ? \"open\" : \"closed\"}\n      className={cn(\"w-full\", className)}\n      {...props}\n    >\n      <div\n        id={contentId}\n        className={cn(\n          \"transition-[mask-image] duration-200\",\n          !currentOpen && LINE_CLAMP_CLASS[collapsedLines],\n          !currentOpen &&\n            \"[mask-image:linear-gradient(to_bottom,#000_68%,transparent_100%)]\",\n          contentClassName,\n        )}\n      >\n        {children}\n      </div>\n      <button\n        type=\"button\"\n        aria-expanded={currentOpen}\n        aria-controls={contentId}\n        onClick={() => setOpen(!currentOpen)}\n        className={cn(\n          \"mt-2 inline-flex h-7 items-center gap-1 rounded-full px-2 text-xs font-medium text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\",\n          triggerClassName,\n        )}\n      >\n        <span>{currentOpen ? lessLabel : moreLabel}</span>\n        <motion.span\n          aria-hidden=\"true\"\n          animate={{ rotate: currentOpen ? 180 : 0 }}\n          transition={reduce ? { duration: 0 } : SPRING_SWAP}\n        >\n          <ChevronDown className=\"size-3.5\" />\n        </motion.span>\n      </button>\n    </div>\n  );\n}\n"},{"path":"components/agents/message-context.tsx","type":"registry:component","target":"@components/agents/message-context.tsx","content":"\"use client\";\n\nimport { createContext } from \"react\";\n\nexport type MessageSide = \"start\" | \"end\";\n\nexport const MessageSideContext = createContext<MessageSide | undefined>(\n  undefined,\n);\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}