{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chat-bubble",
  "title": "Chat Bubble",
  "author": "AlgorizeTech",
  "description": "Message bubble primitives with sent/received styling, grouping-aware corners, avatars and read receipts. Use them to build a custom message thread.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/ChatBubbleBlock.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Check, CheckCheck } from \"lucide-react\";\nimport React, {\n  createContext,\n  forwardRef,\n  useContext,\n  useRef,\n} from \"react\";\n\n// ==========================================\n// CONTEXT & TYPES\n// ==========================================\nexport type ChatVariant = \"sent\" | \"received\";\nexport type ChatLayout =\n  | \"default\"\n  | \"group-top\"\n  | \"group-middle\"\n  | \"group-bottom\";\n\ninterface ChatContextValue {\n  variant: ChatVariant;\n  layout: ChatLayout;\n}\n\nconst ChatContext = createContext<ChatContextValue | null>(null);\n\nfunction useChat() {\n  const context = useContext(ChatContext);\n  if (!context) {\n    throw new Error(\"Chat components must be used within a ChatBubble root.\");\n  }\n  return context;\n}\n\n// ==========================================\n// MOTION\n// ==========================================\n// Plain CSS, shipped inline by every ChatBubble row so the block stays\n// self-contained. The pop-ins are GSAP's back.out(1.7) / back.out(1.5) — cubics,\n// so the cubic-beziers are exact. The typing dots are one 0.7s staggered\n// rise (0.4s each, 0.15s apart) played forward then back, forever, every move\n// on sine.inOut. All of it is skipped under prefers-reduced-motion; the\n// resting state is fully visible, so a skipped animation leaves nothing hidden.\nconst SINE_IN_OUT =\n  \"linear(0, 0.0024, 0.0096, 0.0215, 0.0381, 0.059, 0.0843, 0.1135, 0.1464, 0.1828, 0.2222, 0.2643, 0.3087, 0.3549, 0.4025, 0.451, 0.5, 0.549, 0.5975, 0.6451, 0.6913, 0.7357, 0.7778, 0.8172, 0.8536, 0.8865, 0.9157, 0.941, 0.9619, 0.9785, 0.9904, 0.9976, 1)\";\n\nconst KEYFRAMES = `\n@keyframes chat-bubble-message-in {\n  from { opacity: 0; transform: translateY(10px) scale(0.95); }\n  to { opacity: 1; transform: translateY(0px) scale(1); }\n}\n@keyframes chat-bubble-typing-in {\n  from { opacity: 0; transform: scale(0.9); }\n  to { opacity: 1; transform: scale(1); }\n}\n@keyframes chat-bubble-dot-0 {\n  0% { transform: translateY(0px); }\n  28.5714% { transform: translateY(-4px); }\n  71.4286% { transform: translateY(-4px); }\n  100% { transform: translateY(0px); }\n}\n@keyframes chat-bubble-dot-1 {\n  0%, 10.7143% { transform: translateY(0px); }\n  39.2857% { transform: translateY(-4px); }\n  60.7143% { transform: translateY(-4px); }\n  89.2857%, 100% { transform: translateY(0px); }\n}\n@keyframes chat-bubble-dot-2 {\n  0%, 21.4286% { transform: translateY(0px); }\n  50% { transform: translateY(-4px); }\n  78.5714%, 100% { transform: translateY(0px); }\n}\n@media (prefers-reduced-motion: no-preference) {\n  .chat-bubble-message { animation: chat-bubble-message-in 0.4s cubic-bezier(0.3333, 1.5667, 0.6667, 1) backwards; }\n  .chat-bubble-typing { animation: chat-bubble-typing-in 0.3s cubic-bezier(0.3333, 1.5, 0.6667, 1) backwards; }\n  .chat-bubble-dot { animation: 1.4s ${SINE_IN_OUT} infinite; }\n  .chat-bubble-dot:nth-child(1) { animation-name: chat-bubble-dot-0; }\n  .chat-bubble-dot:nth-child(2) { animation-name: chat-bubble-dot-1; }\n  .chat-bubble-dot:nth-child(3) { animation-name: chat-bubble-dot-2; }\n}`;\n\n// ==========================================\n// COMPONENTS\n// ==========================================\n\nexport interface ChatBubbleProps extends React.HTMLAttributes<HTMLDivElement> {\n  variant?: ChatVariant;\n  layout?: ChatLayout;\n}\n\nexport const ChatBubble = forwardRef<HTMLDivElement, ChatBubbleProps>(\n  (\n    { children, variant = \"received\", layout = \"default\", className, ...props },\n    ref,\n  ) => {\n    return (\n      <ChatContext.Provider value={{ variant, layout }}>\n        <div\n          ref={ref}\n          className={cn(\n            \"flex w-full gap-3 py-0.5\",\n            variant === \"sent\" ? \"justify-end\" : \"justify-start\",\n            className,\n          )}\n          {...props}\n        >\n          {children}\n          <style>{KEYFRAMES}</style>\n        </div>\n      </ChatContext.Provider>\n    );\n  },\n);\nChatBubble.displayName = \"ChatBubble\";\n\nexport interface ChatAvatarProps extends React.HTMLAttributes<HTMLDivElement> {\n  src?: string;\n  fallback: string;\n}\n\nexport const ChatAvatar = forwardRef<HTMLDivElement, ChatAvatarProps>(\n  ({ src, fallback, className, ...props }, ref) => {\n    const { variant, layout } = useChat();\n    const [imgFailed, setImgFailed] = React.useState(false);\n    // A new `src` gets a fresh attempt. Reset while rendering, when `src`\n    // changes, rather than in an effect.\n    const [prevSrc, setPrevSrc] = React.useState(src);\n    if (src !== prevSrc) {\n      setPrevSrc(src);\n      setImgFailed(false);\n    }\n    const imgRef = useRef<HTMLImageElement>(null);\n\n    // `onError` alone is not enough under SSR: the browser starts loading the\n    // server-rendered <img> before React hydrates, so a 404 fires its error event\n    // with no handler attached and the initials never appear. Re-check the element\n    // on mount — a finished load with zero intrinsic width is a failed load.\n    React.useEffect(() => {\n      // A broken <img> that finished loading before hydration fires no error event, so the element has to be inspected on mount.\n      const node = imgRef.current;\n      if (node?.complete && node.naturalWidth === 0) setImgFailed(true);\n    }, [src]);\n\n    // Hide avatar visually if it's in the middle or top of a group to keep alignment clean\n    const isHidden = layout === \"group-top\" || layout === \"group-middle\";\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"flex-shrink-0 w-8 h-8 rounded-full overflow-hidden flex items-center justify-center text-xs font-semibold select-none mt-auto\",\n          variant === \"sent\" ? \"order-2\" : \"order-1\",\n          isHidden ? \"invisible\" : \"visible\",\n          \"bg-muted text-muted-foreground\",\n          className,\n        )}\n        {...props}\n      >\n        {src && !imgFailed ? (\n          <img\n            ref={imgRef}\n            src={src}\n            alt=\"Avatar\"\n            className=\"w-full h-full object-cover\"\n            // Without this the initials never appear: a 404 or offline image leaves a\n            // broken <img> in place, and `fallback` only rendered when `src` was absent.\n            onError={() => setImgFailed(true)}\n          />\n        ) : (\n          fallback\n        )}\n      </div>\n    );\n  },\n);\nChatAvatar.displayName = \"ChatAvatar\";\n\nexport interface ChatMessageProps extends React.HTMLAttributes<HTMLDivElement> {\n  color?:\n    | \"primary\"\n    | \"default\"\n    | \"blue\"\n    | \"indigo\"\n    | \"rose\"\n    | \"emerald\"\n    | \"lavender\"\n    | (string & {});\n}\n\nexport const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(\n  ({ children, color = \"primary\", className, ...props }, ref) => {\n    const { variant, layout } = useChat();\n    const nodeRef = useRef<HTMLDivElement>(null);\n\n    // Forward ref manually while keeping a local ref\n    React.useImperativeHandle(ref, () => nodeRef.current as HTMLDivElement);\n\n    // The pop-in (`chat-bubble-message`) is an enhancement, never a prerequisite\n    // for being visible. The bubble renders opaque by default and the keyframes\n    // animate *from* transparent, so a blocked or skipped animation leaves a\n    // readable message rather than a blank row.\n\n    // Theming logic for sent messages\n    const sentColors: Record<string, string> = {\n      primary: \"bg-primary text-primary-foreground\",\n      default: \"bg-primary text-primary-foreground\",\n      blue: \"bg-blue-600 text-white\",\n      indigo: \"bg-indigo-600 text-white\",\n      rose: \"bg-rose-600 text-white\",\n      emerald: \"bg-emerald-600 text-white\",\n      lavender: \"bg-[#9b8afb] text-white\",\n    };\n\n    const isSent = variant === \"sent\";\n    const isHex = isSent && color && color.startsWith(\"#\");\n\n    // Dynamic border-radius logic based on message grouping\n    const getBorderRadius = () => {\n      if (layout === \"default\") return \"rounded-2xl\";\n\n      if (isSent) {\n        if (layout === \"group-top\") return \"rounded-2xl rounded-br-sm\";\n        if (layout === \"group-middle\")\n          return \"rounded-2xl rounded-tr-sm rounded-br-sm\";\n        if (layout === \"group-bottom\") return \"rounded-2xl rounded-tr-sm\";\n      } else {\n        if (layout === \"group-top\") return \"rounded-2xl rounded-bl-sm\";\n        if (layout === \"group-middle\")\n          return \"rounded-2xl rounded-tl-sm rounded-bl-sm\";\n        if (layout === \"group-bottom\") return \"rounded-2xl rounded-tl-sm\";\n      }\n      return \"rounded-2xl\";\n    };\n\n    return (\n      <div\n        className={cn(\n          \"flex flex-col max-w-[75%]\",\n          isSent ? \"order-1 items-end\" : \"order-2 items-start\",\n        )}\n      >\n        <div\n          ref={nodeRef}\n          className={cn(\n            \"chat-bubble-message px-4 py-2.5 text-[15px] leading-relaxed shadow-sm\",\n            getBorderRadius(),\n            isSent\n              ? isHex\n                ? \"text-white\"\n                : sentColors[color] || sentColors[\"default\"]\n              : \"bg-muted text-foreground border border-border/50\",\n            className,\n          )}\n          style={\n            isHex\n              ? { backgroundColor: color, color: \"#ffffff\", ...props.style }\n              : props.style\n          }\n          {...props}\n        >\n          {children}\n        </div>\n      </div>\n    );\n  },\n);\nChatMessage.displayName = \"ChatMessage\";\n\nexport interface ChatTimestampProps\n  extends React.HTMLAttributes<HTMLDivElement> {\n  time: string;\n  status?: \"sent\" | \"delivered\" | \"read\";\n}\n\nexport const ChatTimestamp = forwardRef<HTMLDivElement, ChatTimestampProps>(\n  ({ time, status, className, ...props }, ref) => {\n    const { variant } = useChat();\n    const isSent = variant === \"sent\";\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"flex items-center gap-1.5 mt-1 mx-1 text-[11px] font-medium select-none\",\n          isSent\n            ? \"justify-end text-current/75\"\n            : \"justify-start text-muted-foreground\",\n          className,\n        )}\n        {...props}\n      >\n        <span>{time}</span>\n        {isSent && status && (\n          <span className=\"flex items-center\">\n            {status === \"sent\" && <Check className=\"w-3.5 h-3.5 opacity-80\" />}\n            {status === \"delivered\" && (\n              <CheckCheck className=\"w-3.5 h-3.5 opacity-80\" />\n            )}\n            {status === \"read\" && (\n              <CheckCheck className=\"w-3.5 h-3.5 text-sky-300 dark:text-sky-300 [div:not(.text-white)_&]:text-sky-500 dark:[div:not(.text-white)_&]:text-sky-400\" />\n            )}\n          </span>\n        )}\n      </div>\n    );\n  },\n);\nChatTimestamp.displayName = \"ChatTimestamp\";\n\nexport const ChatTypingIndicator = forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n  const { variant } = useChat();\n\n  // Pop-in (`chat-bubble-typing`) and the staggered dot bounce\n  // (`chat-bubble-dot`) are CSS. The bubble animates *from* transparent; the\n  // resting state is opaque, so a skipped animation still leaves the dots visible.\n  return (\n    <div\n      ref={ref}\n      className={cn(\n        \"chat-bubble-typing flex items-center gap-1 px-4 py-3.5 rounded-2xl w-max shadow-sm\",\n        // Matches ChatMessage. Without an explicit order the indicator defaults to\n        // order-0 and sorts *before* the avatar's order-1, flipping a received row.\n        variant === \"sent\" ? \"order-1\" : \"order-2\",\n        variant === \"sent\"\n          ? \"bg-primary rounded-br-sm\"\n          : \"bg-muted rounded-bl-sm border border-border/50\",\n        className,\n      )}\n      {...props}\n    >\n      {[0, 1, 2].map((i) => (\n        <div\n          key={i}\n          className={cn(\n            \"chat-bubble-dot w-1.5 h-1.5 rounded-full\",\n            variant === \"sent\"\n              ? \"bg-primary-foreground/70\"\n              : \"bg-muted-foreground\",\n          )}\n        />\n      ))}\n    </div>\n  );\n});\nChatTypingIndicator.displayName = \"ChatTypingIndicator\";\n",
      "type": "registry:component",
      "target": "components/custom/blocks/ChatBubbleBlock.tsx"
    }
  ],
  "docs": "Named exports: ChatBubble, ChatAvatar, ChatMessage, ChatTimestamp. `layout` is default | group-top | group-middle | group-bottom and adjusts the corner radii so consecutive messages from one sender read as a block. `status` renders sent | delivered | read ticks. These are the pieces `chat` assembles.",
  "categories": [
    "communication"
  ],
  "type": "registry:component"
}
