{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "message-bubble",
  "version": "3.0.6",
  "category": "messaging",
  "meta": {
    "preview": "https://ui.manifest.build/previews/message-bubble.png",
    "version": "3.0.6",
    "changelog": {
      "1.0.0": "Initial release with text, image, voice and reaction variants",
      "1.0.1": "Added better image captions for accessibility",
      "1.0.2": "Improved alt text to use caption when available",
      "1.1.0": "Added avatarUrl prop for image avatars with letter fallback",
      "2.0.0": "BREAKING: Renamed avatar prop to avatarFallback for clarity",
      "3.0.0": "BREAKING: Renamed caption prop to content in ImageMessageBubble",
      "3.0.1": "Added aria-labels to reaction buttons and voice message controls for accessibility",
      "3.0.3": "Added comprehensive JSDoc documentation",
      "3.0.4": "Updated Props interface with decorative header and sub-parameter JSDoc documentation",
      "3.0.5": "Removed default content data - component only renders explicitly provided data",
      "3.0.6": "Show demo data when rendered without props"
    }
  },
  "changelog": {
    "1.0.0": "Initial release with text, image, voice and reaction variants",
    "1.0.1": "Added better image captions for accessibility",
    "1.0.2": "Improved alt text to use caption when available",
    "1.1.0": "Added avatarUrl prop for image avatars with letter fallback",
    "2.0.0": "BREAKING: Renamed avatar prop to avatarFallback for clarity",
    "3.0.0": "BREAKING: Renamed caption prop to content in ImageMessageBubble",
    "3.0.1": "Added aria-labels to reaction buttons and voice message controls for accessibility",
    "3.0.3": "Added comprehensive JSDoc documentation",
    "3.0.4": "Updated Props interface with decorative header and sub-parameter JSDoc documentation",
    "3.0.5": "Removed default content data - component only renders explicitly provided data",
    "3.0.6": "Show demo data when rendered without props"
  },
  "title": "Message Bubble",
  "author": "MNFST, Inc",
  "description": "Chat message bubbles with text, image, voice, and reaction variants.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "dropdown-menu"
  ],
  "files": [
    {
      "path": "registry/messaging/message-bubble.tsx",
      "content": "'use client'\n\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuTrigger\n} from '@/components/ui/dropdown-menu'\nimport { cn } from '@/lib/utils'\nimport { demoTextMessages, demoImageMessages, demoReactionMessage, demoVoiceMessage } from './demo/messaging'\nimport { Check, CheckCheck, Smile } from 'lucide-react'\nimport { useRef, useState } from 'react'\n\n/**\n * Internal avatar component options.\n * @interface InternalAvatarOptions\n * @property {string} [src] - Avatar image URL\n * @property {string} fallback - Fallback letter when image fails or is missing\n * @property {string} [className] - Additional CSS classes\n */\ninterface InternalAvatarOptions {\n  src?: string\n  fallback: string\n  className?: string\n}\n\nfunction Avatar({ src, fallback, className }: InternalAvatarOptions) {\n  const [imgError, setImgError] = useState(false)\n\n  if (src && !imgError) {\n    return (\n      <img\n        src={src}\n        alt={fallback}\n        onError={() => setImgError(true)}\n        className={cn('h-8 w-8 rounded-full object-cover shrink-0', className)}\n      />\n    )\n  }\n\n  return (\n    <div\n      className={cn(\n        'h-8 w-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-semibold shrink-0',\n        className\n      )}\n    >\n      {fallback}\n    </div>\n  )\n}\n\n/**\n * ═══════════════════════════════════════════════════════════════════════════\n * MessageBubbleProps\n * ═══════════════════════════════════════════════════════════════════════════\n *\n * Props for configuring a text message bubble in chat interfaces with avatar,\n * delivery status, and own/other message styling.\n */\nexport interface MessageBubbleProps {\n  data?: {\n    /** Message text content to display. */\n    content?: string\n    /** URL for the sender's avatar image. */\n    avatarUrl?: string\n    /** Fallback letter to display when avatar image is unavailable. */\n    avatarFallback?: string\n    /** Display name of the message author. */\n    author?: string\n    /** Time display string (e.g., \"10:30 AM\"). */\n    time?: string\n  }\n  appearance?: {\n    /**\n     * Whether this message is from the current user.\n     * @default false\n     */\n    isOwn?: boolean\n  }\n  control?: {\n    /** Message delivery status indicator. */\n    status?: 'sent' | 'delivered' | 'read'\n  }\n}\n\n/**\n * A single text message bubble for chat interfaces.\n * Displays avatar, message content, time, and delivery status.\n *\n * Features:\n * - Own/other message styling with color differentiation\n * - Avatar with image or letter fallback\n * - Delivery status indicators (sent, delivered, read)\n * - Time display\n *\n * @component\n * @example\n * ```tsx\n * <MessageBubble\n *   data={{\n *     content: \"Hey! How are you?\",\n *     avatarUrl: \"https://example.com/avatar.jpg\",\n *     avatarFallback: \"J\",\n *     time: \"10:30 AM\"\n *   }}\n *   appearance={{ isOwn: false }}\n *   control={{ status: \"read\" }}\n * />\n * ```\n */\nexport function MessageBubble({\n  data,\n  appearance,\n  control\n}: MessageBubbleProps) {\n  const resolved: NonNullable<MessageBubbleProps['data']> = data ?? demoTextMessages[0]\n  const content = resolved.content\n  const avatarFallback = resolved.avatarFallback\n  const avatarUrl = resolved.avatarUrl\n  const time = resolved.time\n  const { isOwn = false } = appearance ?? {}\n  const { status } = control ?? {}\n  return (\n    <div className={cn('flex gap-2', isOwn && 'flex-row-reverse')}>\n      {!isOwn && avatarFallback && <Avatar src={avatarUrl} fallback={avatarFallback} />}\n      <div className={cn('max-w-[75%]', isOwn && 'items-end')}>\n        {content && (\n          <div\n            className={cn(\n              'rounded-2xl px-4 py-2',\n              isOwn\n                ? 'bg-primary text-primary-foreground rounded-br-md'\n                : 'bg-muted rounded-bl-md'\n            )}\n          >\n            <p className=\"text-sm\">{content}</p>\n          </div>\n        )}\n        <div\n          className={cn('flex items-center gap-1 mt-1', isOwn && 'justify-end')}\n        >\n          {time && <span className=\"text-[10px] text-muted-foreground\">{time}</span>}\n          {isOwn && status && (\n            <span className=\"text-muted-foreground\">\n              {status === 'sent' && <Check className=\"h-3 w-3\" />}\n              {status === 'delivered' && <CheckCheck className=\"h-3 w-3\" />}\n              {status === 'read' && (\n                <CheckCheck className=\"h-3 w-3 text-foreground\" />\n              )}\n            </span>\n          )}\n        </div>\n      </div>\n    </div>\n  )\n}\n\n/**\n * ═══════════════════════════════════════════════════════════════════════════\n * ImageMessageBubbleProps\n * ═══════════════════════════════════════════════════════════════════════════\n *\n * Props for configuring an image message bubble that displays a shared photo\n * with optional caption and message metadata.\n */\nexport interface ImageMessageBubbleProps {\n  data?: {\n    /** URL of the image to display. */\n    image?: string\n    /** Optional caption text below the image. */\n    content?: string\n    /** URL for the sender's avatar image. */\n    avatarUrl?: string\n    /** Fallback letter to display when avatar image is unavailable. */\n    avatarFallback?: string\n    /** Display name of the message author. */\n    author?: string\n    /** Time display string (e.g., \"10:32 AM\"). */\n    time?: string\n  }\n  appearance?: {\n    /**\n     * Whether this message is from the current user.\n     * @default false\n     */\n    isOwn?: boolean\n  }\n  control?: {\n    /** Message delivery status indicator. */\n    status?: 'sent' | 'delivered' | 'read'\n  }\n}\n\n/**\n * An image message bubble for sharing photos in chat.\n * Displays an image with optional caption and message metadata.\n *\n * Features:\n * - Image display with max width constraint\n * - Optional caption below image\n * - Own/other message styling\n * - Avatar and delivery status support\n *\n * @component\n * @example\n * ```tsx\n * <ImageMessageBubble\n *   data={{\n *     image: \"https://example.com/photo.jpg\",\n *     content: \"Check this out!\",\n *     avatarFallback: \"J\",\n *     time: \"10:32 AM\"\n *   }}\n *   appearance={{ isOwn: false }}\n * />\n * ```\n */\nexport function ImageMessageBubble({\n  data,\n  appearance,\n  control\n}: ImageMessageBubbleProps) {\n  const resolved: NonNullable<ImageMessageBubbleProps['data']> = data ?? demoImageMessages[0]\n  const image = resolved.image\n  const content = resolved.content\n  const avatarFallback = resolved.avatarFallback\n  const avatarUrl = resolved.avatarUrl\n  const time = resolved.time\n  const { isOwn = false } = appearance ?? {}\n  const { status } = control ?? {}\n  return (\n    <div className={cn('flex gap-2', isOwn && 'flex-row-reverse')}>\n      {!isOwn && avatarFallback && <Avatar src={avatarUrl} fallback={avatarFallback} />}\n      <div className={cn('max-w-[75%]', isOwn && 'items-end')}>\n        {image && (\n          <div\n            className={cn(\n              'rounded-2xl overflow-hidden',\n              isOwn ? 'rounded-br-md' : 'rounded-bl-md'\n            )}\n          >\n            <img\n              src={image}\n              alt={content || 'Shared image in chat'}\n              className=\"w-full max-w-[280px] h-auto object-cover\"\n            />\n            {content && (\n              <div\n                className={cn(\n                  'px-3 py-2',\n                  isOwn ? 'bg-primary text-primary-foreground' : 'bg-muted'\n                )}\n              >\n                <p className=\"text-sm\">{content}</p>\n              </div>\n            )}\n          </div>\n        )}\n        <div\n          className={cn('flex items-center gap-1 mt-1', isOwn && 'justify-end')}\n        >\n          {time && <span className=\"text-[10px] text-muted-foreground\">{time}</span>}\n          {isOwn && status && (\n            <span className=\"text-muted-foreground\">\n              {status === 'sent' && <Check className=\"h-3 w-3\" />}\n              {status === 'delivered' && <CheckCheck className=\"h-3 w-3\" />}\n              {status === 'read' && (\n                <CheckCheck className=\"h-3 w-3 text-foreground\" />\n              )}\n            </span>\n          )}\n        </div>\n      </div>\n    </div>\n  )\n}\n\n/**\n * ═══════════════════════════════════════════════════════════════════════════\n * MessageWithReactionsProps\n * ═══════════════════════════════════════════════════════════════════════════\n *\n * Props for configuring a message bubble with emoji reaction support,\n * allowing users to add, toggle, and view reactions on messages.\n */\nexport interface MessageWithReactionsProps {\n  data?: {\n    /** Message text content to display. */\n    content?: string\n    /** URL for the sender's avatar image. */\n    avatarUrl?: string\n    /** Fallback letter to display when avatar image is unavailable. */\n    avatarFallback?: string\n    /** Display name of the message author. */\n    author?: string\n    /** Time display string (e.g., \"2:45 PM\"). */\n    time?: string\n    /** Array of reactions with emoji and count. */\n    reactions?: { emoji: string; count: number }[]\n  }\n  actions?: {\n    /** Called when the user adds or toggles a reaction emoji. */\n    onReact?: (emoji: string) => void\n  }\n  appearance?: {\n    /**\n     * Whether this message is from the current user.\n     * @default false\n     */\n    isOwn?: boolean\n  }\n}\n\n/**\n * Available emoji options for reactions.\n * @constant\n */\nconst availableEmojis = [\n  '❤️',\n  '👍',\n  '👎',\n  '😂',\n  '😮',\n  '😢',\n  '🎉',\n  '🔥',\n  '👏',\n  '💯'\n]\n\n/**\n * A message bubble with emoji reaction support.\n * Allows users to add, toggle, and view reactions on messages.\n *\n * Features:\n * - Emoji reaction picker dropdown\n * - Toggle reactions on/off\n * - Reaction count display\n * - Highlighted user's own reactions\n * - Full reaction emoji set\n *\n * @component\n * @example\n * ```tsx\n * <MessageWithReactions\n *   data={{\n *     content: \"This is great news! 🎉\",\n *     avatarFallback: \"A\",\n *     time: \"2:45 PM\",\n *     reactions: [{ emoji: \"❤️\", count: 3 }, { emoji: \"👍\", count: 2 }]\n *   }}\n *   actions={{\n *     onReact: (emoji) => console.log(\"Reacted with:\", emoji)\n *   }}\n *   appearance={{ isOwn: false }}\n * />\n * ```\n */\nexport function MessageWithReactions({\n  data,\n  actions,\n  appearance\n}: MessageWithReactionsProps) {\n  const resolved: NonNullable<MessageWithReactionsProps['data']> = data ?? demoReactionMessage\n  const content = resolved.content\n  const avatarFallback = resolved.avatarFallback\n  const avatarUrl = resolved.avatarUrl\n  const time = resolved.time\n  const initialReactions = resolved.reactions ?? []\n  const { onReact } = actions ?? {}\n  const { isOwn = false } = appearance ?? {}\n  const [reactions, setReactions] = useState(initialReactions)\n  // Track which emojis the current user has reacted with\n  const [userReactions, setUserReactions] = useState<Set<string>>(new Set())\n\n  const handleReact = (emoji: string) => {\n    const hasUserReacted = userReactions.has(emoji)\n    const existingIndex = reactions.findIndex((r) => r.emoji === emoji)\n\n    if (hasUserReacted) {\n      // User already reacted - toggle off (decrement)\n      if (existingIndex >= 0) {\n        const updated = [...reactions]\n        if (updated[existingIndex].count <= 1) {\n          // Remove reaction entirely if count would become 0\n          updated.splice(existingIndex, 1)\n        } else {\n          updated[existingIndex] = {\n            ...updated[existingIndex],\n            count: updated[existingIndex].count - 1\n          }\n        }\n        setReactions(updated)\n      }\n      // Remove from user's reactions\n      setUserReactions((prev) => {\n        const next = new Set(prev)\n        next.delete(emoji)\n        return next\n      })\n    } else {\n      // User hasn't reacted - add reaction\n      if (existingIndex >= 0) {\n        const updated = [...reactions]\n        updated[existingIndex] = {\n          ...updated[existingIndex],\n          count: updated[existingIndex].count + 1\n        }\n        setReactions(updated)\n      } else {\n        setReactions([...reactions, { emoji, count: 1 }])\n      }\n      // Add to user's reactions\n      setUserReactions((prev) => new Set(prev).add(emoji))\n    }\n    onReact?.(emoji)\n  }\n\n  return (\n    <div className={cn('flex gap-2', isOwn && 'flex-row-reverse')}>\n      {!isOwn && avatarFallback && <Avatar src={avatarUrl} fallback={avatarFallback} />}\n      <div className={cn('max-w-[75%]', isOwn && 'items-end')}>\n        {content && (\n          <div\n            className={cn(\n              'rounded-2xl px-4 py-2',\n              isOwn\n                ? 'bg-primary text-primary-foreground rounded-br-md'\n                : 'bg-muted rounded-bl-md'\n            )}\n          >\n            <p className=\"text-sm\">{content}</p>\n          </div>\n        )}\n        <div\n          className={cn(\n            'flex items-center gap-1 mt-1.5',\n            isOwn ? 'justify-end' : 'justify-start'\n          )}\n        >\n          {reactions && reactions.length > 0 && (\n            <>\n              {reactions.map((reaction, index) => (\n                <button\n                  key={index}\n                  onClick={() => handleReact(reaction.emoji)}\n                  aria-label={`${userReactions.has(reaction.emoji) ? 'Remove' : 'Add'} ${reaction.emoji} reaction, ${reaction.count} ${reaction.count === 1 ? 'reaction' : 'reactions'}`}\n                  className={cn(\n                    'inline-flex items-center gap-0.5 rounded-full px-1.5 py-0.5 text-xs transition-colors cursor-pointer',\n                    userReactions.has(reaction.emoji)\n                      ? 'bg-primary/15 border border-primary/50'\n                      : 'bg-card border hover:bg-muted'\n                  )}\n                >\n                  {reaction.emoji}\n                  <span\n                    className={cn(\n                      userReactions.has(reaction.emoji)\n                        ? 'text-primary'\n                        : 'text-muted-foreground'\n                    )}\n                  >\n                    {reaction.count}\n                  </span>\n                </button>\n              ))}\n            </>\n          )}\n          <DropdownMenu>\n            <DropdownMenuTrigger asChild>\n              <button\n                aria-label=\"Add reaction\"\n                className=\"inline-flex items-center justify-center h-6 w-6 bg-card border rounded-full hover:bg-muted transition-colors cursor-pointer\"\n              >\n                <Smile className=\"h-3.5 w-3.5 text-muted-foreground\" />\n              </button>\n            </DropdownMenuTrigger>\n            <DropdownMenuContent align=\"start\" className=\"p-2\">\n              <div className=\"grid grid-cols-5 gap-1\">\n                {availableEmojis.map((emoji) => (\n                  <button\n                    key={emoji}\n                    onClick={() => handleReact(emoji)}\n                    aria-label={`React with ${emoji}`}\n                    className=\"h-8 w-8 flex items-center justify-center text-lg hover:bg-muted rounded transition-colors cursor-pointer\"\n                  >\n                    {emoji}\n                  </button>\n                ))}\n              </div>\n            </DropdownMenuContent>\n          </DropdownMenu>\n        </div>\n        <div\n          className={cn('flex items-center gap-1 mt-1', isOwn && 'justify-end')}\n        >\n          {time && <span className=\"text-[10px] text-muted-foreground\">{time}</span>}\n        </div>\n      </div>\n    </div>\n  )\n}\n\n/**\n * ═══════════════════════════════════════════════════════════════════════════\n * VoiceMessageBubbleProps\n * ═══════════════════════════════════════════════════════════════════════════\n *\n * Props for configuring a voice/audio message bubble with playback controls,\n * progress bar, and duration display.\n */\nexport interface VoiceMessageBubbleProps {\n  data?: {\n    /** Total duration display string (e.g., \"0:42\"). */\n    duration?: string\n    /** URL for the sender's avatar image. */\n    avatarUrl?: string\n    /** Fallback letter to display when avatar image is unavailable. */\n    avatarFallback?: string\n    /** Display name of the message author. */\n    author?: string\n    /** Time display string (e.g., \"3:15 PM\"). */\n    time?: string\n    /** URL of the audio file to play. */\n    audioSrc?: string\n  }\n  appearance?: {\n    /**\n     * Whether this message is from the current user.\n     * @default false\n     */\n    isOwn?: boolean\n  }\n  control?: {\n    /** Message delivery status indicator. */\n    status?: 'sent' | 'delivered' | 'read'\n  }\n}\n\n/**\n * A voice/audio message bubble with playback controls.\n * Allows users to play, pause, and see progress of audio messages.\n *\n * Features:\n * - Play/pause button with icon toggle\n * - Progress bar showing playback position\n * - Duration and current time display\n * - Own/other message styling\n * - Delivery status indicators\n *\n * @component\n * @example\n * ```tsx\n * <VoiceMessageBubble\n *   data={{\n *     duration: \"0:42\",\n *     audioSrc: \"https://example.com/audio.mp3\",\n *     avatarFallback: \"M\",\n *     time: \"3:15 PM\"\n *   }}\n *   appearance={{ isOwn: false }}\n *   control={{ status: \"delivered\" }}\n * />\n * ```\n */\nexport function VoiceMessageBubble({\n  data,\n  appearance,\n  control\n}: VoiceMessageBubbleProps) {\n  const resolved: NonNullable<VoiceMessageBubbleProps['data']> = data ?? demoVoiceMessage\n  const duration = resolved.duration\n  const avatarFallback = resolved.avatarFallback\n  const avatarUrl = resolved.avatarUrl\n  const time = resolved.time\n  const audioSrc = resolved.audioSrc\n  const { isOwn = false } = appearance ?? {}\n  const { status } = control ?? {}\n  const [isPlaying, setIsPlaying] = useState(false)\n  const [progress, setProgress] = useState(0)\n  const [currentTime, setCurrentTime] = useState('0:00')\n  const audioRef = useRef<HTMLAudioElement>(null)\n\n  const togglePlay = () => {\n    if (audioRef.current) {\n      if (isPlaying) {\n        audioRef.current.pause()\n      } else {\n        audioRef.current.play()\n      }\n      setIsPlaying(!isPlaying)\n    }\n  }\n\n  const handleTimeUpdate = () => {\n    if (audioRef.current) {\n      const current = audioRef.current.currentTime\n      const total = audioRef.current.duration || 1\n      setProgress((current / total) * 100)\n      const mins = Math.floor(current / 60)\n      const secs = Math.floor(current % 60)\n      setCurrentTime(`${mins}:${secs.toString().padStart(2, '0')}`)\n    }\n  }\n\n  const handleEnded = () => {\n    setIsPlaying(false)\n    setProgress(0)\n    setCurrentTime('0:00')\n  }\n\n  return (\n    <div className={cn('flex gap-2', isOwn && 'flex-row-reverse')}>\n      <audio\n        ref={audioRef}\n        src={audioSrc}\n        onTimeUpdate={handleTimeUpdate}\n        onEnded={handleEnded}\n        preload=\"metadata\"\n      />\n      {!isOwn && avatarFallback && <Avatar src={avatarUrl} fallback={avatarFallback} />}\n      <div className={cn('max-w-[75%]', isOwn && 'items-end')}>\n        <div\n          className={cn(\n            'rounded-2xl px-4 py-3 flex items-center gap-3',\n            isOwn\n              ? 'bg-primary text-primary-foreground rounded-br-md'\n              : 'bg-muted rounded-bl-md'\n          )}\n        >\n          <button\n            onClick={togglePlay}\n            aria-label={isPlaying ? 'Pause voice message' : 'Play voice message'}\n            className={cn(\n              'h-8 w-8 rounded-full flex items-center justify-center shrink-0 transition-colors cursor-pointer',\n              isOwn\n                ? 'bg-primary-foreground/20 hover:bg-primary-foreground/30'\n                : 'bg-foreground/10 hover:bg-foreground/20'\n            )}\n          >\n            {isPlaying ? (\n              <svg className=\"h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n                <path d=\"M6 4h4v16H6V4zm8 0h4v16h-4V4z\" />\n              </svg>\n            ) : (\n              <svg\n                className=\"h-4 w-4 ml-0.5\"\n                viewBox=\"0 0 24 24\"\n                fill=\"currentColor\"\n              >\n                <path d=\"M8 5v14l11-7z\" />\n              </svg>\n            )}\n          </button>\n          <div className=\"flex-1 flex items-center gap-2\">\n            <div className=\"flex-1 h-1 bg-current/20 rounded-full overflow-hidden\">\n              <div\n                className=\"h-full bg-current rounded-full transition-all duration-100\"\n                style={{ width: `${progress || 33}%` }}\n              />\n            </div>\n            <span className=\"text-xs font-medium\">\n              {isPlaying ? currentTime : (duration ?? '0:00')}\n            </span>\n          </div>\n        </div>\n        <div\n          className={cn('flex items-center gap-1 mt-1', isOwn && 'justify-end')}\n        >\n          {time && <span className=\"text-[10px] text-muted-foreground\">{time}</span>}\n          {isOwn && status && (\n            <span className=\"text-muted-foreground\">\n              {status === 'sent' && <Check className=\"h-3 w-3\" />}\n              {status === 'delivered' && <CheckCheck className=\"h-3 w-3\" />}\n              {status === 'read' && (\n                <CheckCheck className=\"h-3 w-3 text-foreground\" />\n              )}\n            </span>\n          )}\n        </div>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "components/ui/message-bubble.tsx"
    },
    {
      "path": "registry/messaging/demo/messaging.ts",
      "content": "// Demo data for Messaging category components\n// This file contains sample data used for component previews and documentation\n\nimport type { ChatMessage } from '../types'\n\n// Default messages for ChatConversation\nexport const demoMessages: ChatMessage[] = [\n  {\n    type: 'text',\n    content: 'Hey! Check out this new feature we just shipped',\n    author: 'Sarah',\n    avatarFallback: 'S',\n    time: '10:30 AM',\n    isOwn: false\n  },\n  {\n    type: 'text',\n    content: 'Oh wow, that looks amazing! How long did it take to build?',\n    author: 'You',\n    avatarFallback: 'Y',\n    time: '10:31 AM',\n    isOwn: true,\n    status: 'read'\n  },\n  {\n    type: 'image',\n    content: \"Here's a preview of the dashboard\",\n    image:\n      'https://images.unsplash.com/photo-1618477388954-7852f32655ec?w=400&h=300&fit=crop',\n    author: 'Sarah',\n    avatarFallback: 'S',\n    time: '10:32 AM',\n    isOwn: false\n  },\n  {\n    type: 'text',\n    content: 'This is incredible! The UI is so clean',\n    author: 'You',\n    avatarFallback: 'Y',\n    time: '10:33 AM',\n    isOwn: true,\n    status: 'delivered'\n  }\n]\n\n// Text message bubble data\nexport const demoTextMessages = [\n  {\n    content: 'Hey! How are you doing today?',\n    avatarUrl: 'https://i.pravatar.cc/150?u=sarah',\n    avatarFallback: 'S',\n    time: 'Dec 8, 10:30 AM',\n  },\n  {\n    content: \"I'm doing great, thanks for asking!\",\n    avatarFallback: 'Y',\n    time: 'Dec 8, 10:31 AM',\n    isOwn: true,\n    status: 'read' as const,\n  },\n]\n\n// Image message bubble data\nexport const demoImageMessages = [\n  {\n    image:\n      'https://images.unsplash.com/photo-1682687220742-aba13b6e50ba?w=400&h=300&fit=crop',\n    content: 'Check out this view!',\n    avatarUrl: 'https://i.pravatar.cc/150?u=alex',\n    avatarFallback: 'A',\n    time: 'Dec 8, 2:45 PM',\n  },\n  {\n    image:\n      'https://images.unsplash.com/photo-1618477388954-7852f32655ec?w=400&h=300&fit=crop',\n    time: 'Dec 8, 2:46 PM',\n    isOwn: true,\n    status: 'delivered' as const,\n  },\n]\n\n// Voice message bubble data\nexport const demoVoiceMessage = {\n  duration: '0:42',\n  avatarUrl: 'https://i.pravatar.cc/150?u=mickael',\n  avatarFallback: 'M',\n  time: 'Dec 8, 3:15 PM',\n}\n\n// Reaction message data\nexport const demoReactionMessage = {\n  content: 'We just hit 10,000 users!',\n  avatarFallback: 'T',\n  time: 'Dec 8, 4:20 PM',\n  reactions: [\n    { emoji: '🎉', count: 5 },\n    { emoji: '❤️', count: 3 },\n    { emoji: '👏', count: 2 },\n  ],\n}\n",
      "type": "registry:lib",
      "target": "components/ui/demo/messaging.ts"
    }
  ],
  "categories": [
    "messaging"
  ],
  "type": "registry:block"
}