{"version":3,"file":"chat-react-ui-components.cjs","sources":["../../node_modules/clsx/dist/clsx.mjs","../../src/components/atoms/app-loading.tsx","../../src/hooks/use-message-window.tsx","../../src/context/avatar-context.tsx","../../src/hooks/use-avatar.tsx","../../src/hooks/use-user-avatar.tsx","../../src/components/atoms/avatar.tsx","../../src/components/atoms/button.tsx","../../src/components/atoms/icon.tsx","../../src/components/atoms/text-input.tsx","../../src/components/atoms/tooltip.tsx","../../src/components/molecules/confirm-dialog.tsx","../../src/components/molecules/emoji-picker.tsx","../../src/context/chat-settings-context.tsx","../../src/hooks/use-chat-settings.tsx","../../src/components/molecules/message-actions.tsx","../../src/components/molecules/message-reactions.tsx","../../src/components/molecules/chat-message.tsx","../../src/components/atoms/typing-dots.tsx","../../src/components/molecules/typing-indicators.tsx","../../src/components/molecules/chat-message-list.tsx","../../src/components/molecules/chat-window-footer.tsx","../../src/components/molecules/chat-window-header.tsx","../../src/components/molecules/message-input.tsx","../../src/components/molecules/chat-window.tsx","../../src/components/molecules/empty-state.tsx","../../src/hooks/use-room-avatar.tsx","../../src/components/molecules/participant.tsx","../../src/components/molecules/participant-list.tsx","../../src/components/molecules/presence-count.tsx","../../src/components/molecules/presence-indicators.tsx","../../src/components/molecules/presence-list.tsx","../../src/components/molecules/room-info.tsx","../../src/hooks/use-throttle.tsx","../../src/components/molecules/emoji-burst.tsx","../../src/components/molecules/emoji-wheel.tsx","../../src/components/molecules/room-reaction.tsx","../../src/context/theme-context.tsx","../../src/hooks/use-theme.tsx","../../src/components/molecules/create-room-modal.tsx","../../src/components/molecules/dropdown-menu.tsx","../../src/components/molecules/room-list-item.tsx","../../src/components/molecules/room-list.tsx","../../src/components/molecules/sidebar.tsx","../../src/app/app.tsx","../../src/components/molecules/avatar-editor.tsx","../../src/providers/avatar-provider.tsx","../../src/providers/chat-settings-provider.tsx","../../src/providers/theme-provider.tsx","../../src/index.ts"],"sourcesContent":["function r(e){var t,f,n=\"\";if(\"string\"==typeof e||\"number\"==typeof e)n+=e;else if(\"object\"==typeof e)if(Array.isArray(e)){var o=e.length;for(t=0;t<o;t++)e[t]&&(f=r(e[t]))&&(n&&(n+=\" \"),n+=f)}else for(f in e)e[f]&&(n&&(n+=\" \"),n+=f);return n}export function clsx(){for(var e,t,f=0,n=\"\",o=arguments.length;f<o;f++)(e=arguments[f])&&(t=r(e))&&(n&&(n+=\" \"),n+=t);return n}export default clsx;","import { clsx } from 'clsx';\nimport React, { useMemo } from 'react';\n/**\n * Props for the AppLoading component.\n */\ninterface AppLoadingProps {\n  /** Width of the loading container (default: '50vw') */\n  width?: string | number;\n  /** Height of the loading container (default: '50vh') */\n  height?: string | number;\n}\n\n/**\n * Loading component displayed while connecting to chat services.\n * Shows a spinner and connection status message.\n */\nexport const AppLoading = ({ width = '50vw', height = '50vh' }: AppLoadingProps) => {\n  const containerStyle = useMemo(\n    () => ({\n      width: typeof width === 'number' ? `${String(width)}px` : width,\n      height: typeof height === 'number' ? `${String(height)}px` : height,\n    }),\n    [width, height]\n  );\n\n  return (\n    <div\n      className={clsx(\n        // Layout fundamentals\n        'flex items-center justify-center',\n        // Theme and colors\n        'bg-gray-50 dark:bg-gray-950',\n        'text-gray-900 dark:text-gray-100',\n        // Positioning and overflow\n        'overflow-hidden',\n        // Visual styling\n        'border border-gray-200 dark:border-gray-700',\n        'rounded-lg shadow-lg',\n        // Centering\n        'mx-auto my-8'\n      )}\n      style={containerStyle}\n      role=\"main\"\n      aria-label=\"Loading chat application\"\n    >\n      <div className=\"text-center\">\n        <div className=\"animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-4\"></div>\n        <p className=\"text-gray-600\">Connecting to chat...</p>\n      </div>\n    </div>\n  );\n};\n","import { ChatMessageEvent, ChatMessageEventType, Message, PaginatedResult } from '@ably/chat';\nimport { useMessages, useRoom } from '@ably/chat/react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\n/** Props for the useMessageWindow hook */\nexport interface UseMessageWindowProps {\n  /** Number of rows kept mounted (ex‑overscan). Defaults to 200 */\n  windowSize?: number;\n  /** Extra rows rendered above & below the viewport. Defaults to 20 */\n  overscan?: number;\n  /** historyBatchSize - Number of messages to fetch in a single history request. Defaults to 300 */\n  historyBatchSize?: number;\n}\n\n/** Response interface for the useMessageWindow hook */\nexport interface UseMessageWindowResponse {\n  /** Messages that should be rendered in the UI */\n  activeMessages: Message[];\n  /** Add or update messages */\n  updateMessages: (messages: Message[], prepend?: boolean) => void;\n  /** Jump to the latest message */\n  showLatestMessages: () => void;\n  /** Scroll by ±delta rows (positive = newer) */\n  scrollBy: (delta: number) => void;\n  /** Centre the window on a specific message serial */\n  showMessagesAroundSerial: (serial: string) => void;\n  /** `true` while a history query is running */\n  loading: boolean;\n  /** `true` if their are more messages that can be fetched from history*/\n  hasMoreHistory: boolean;\n  /** Triggers another history fetch from the point of the earliest message*/\n  loadMoreHistory: () => Promise<void>;\n}\n\n/**\n * A React hook to manage message windowing and history for chat.\n *\n * This hook manages a virtualized window of messages from a larger chat history, providing efficient\n * rendering for large conversations.\n *\n * - Must be used within a `ChatRoomProvider` component.\n *\n *\n * Features:\n * - *Virtualized Windowing*: Exposes a subset of the total messages in memory for efficient rendering\n * - *Realtime updates*: Automatically handles new messages, edits, deletions, and reactions.\n * - *History Pagination*: Loads older messages on demand with configurable batch sizes.\n * - *Discontinuity Recovery*: Automatically recovers missing messages after network disruptions.\n * - *Navigation Controls*: Jump to latest, scroll by delta, or center on specific messages\n *\n * @param opts - Configuration options for the message window\n * @returns Hook interface with message data and control methods\n *\n *\n * @example\n * // Basic usage\n * function AdvancedChatRoom() {\n *   const {\n *     activeMessages,\n *     updateMessages,\n *     scrollBy,\n *     showMessagesAroundSerial,\n *     loadMoreHistory,\n *     hasMoreHistory,\n *     loading\n *   } = useMessageWindow({\n *     windowSize: 100, // Smaller window for better performance\n *   });\n *\n *   const handleJumpToMessage = (messageSerial: string) => {\n *     showMessagesAroundSerial(messageSerial);\n *   };\n *\n *   const handleScrollUp = () => {\n *     scrollBy(-10); // Scroll 10 messages toward older\n *   };\n *\n *   const handleScrollDown = () => {\n *     scrollBy(10); // Scroll 10 messages toward newer\n *   };\n *\n *   return (\n *     <ChatRoomProvider roomId=\"support\">\n *       <div>\n *         <div className=\"controls\">\n *           <button onClick={handleScrollUp}>↑ Older</button>\n *           <button onClick={handleScrollDown}>↓ Newer</button>\n *           {hasMoreHistory && (\n *             <button onClick={loadMoreHistory} disabled={loading}>\n *               Load More History\n *             </button>\n *           )}\n *         </div>\n *\n *         <div className=\"messages\">\n *           {activeMessages.map(msg => (\n *             <MessageComponent\n *               key={msg.serial}\n *               message={msg}\n *               onJumpTo={() => handleJumpToMessage(msg.serial)}\n *             />\n *           ))}\n *         </div>\n *       </div>\n *     </ChatRoomProvider>\n *   );\n * }\n *\n */\nexport const useMessageWindow = ({\n  windowSize = 200,\n  overscan = 20,\n  historyBatchSize = 300,\n}: UseMessageWindowProps = {}): UseMessageWindowResponse => {\n  const nextPageRef = useRef<undefined | (() => Promise<PaginatedResult<Message> | null>)>(\n    undefined\n  );\n  const serialSetRef = useRef<Set<string>>(new Set());\n  const initialHistoryLoadedRef = useRef<boolean>(false);\n  const recoveringRef = useRef<boolean>(false);\n\n  /** Entire message history, should not be used for UI display */\n  const allMessagesRef = useRef<Message[]>([]);\n  /** Current version of the message list, used to trigger re-renders */\n  const [version, setVersion] = useState(0);\n  /** Slice to render in UI, typically a couple 100 messages */\n  const [activeMessages, setActiveMessages] = useState<Message[]>([]);\n  /** Anchor row, used to maintain the window position (‑1==latest) */\n  const [anchorIdx, setAnchorIdx] = useState<number>(-1);\n\n  /** Loading state for history queries */\n  const [loading, setLoading] = useState<boolean>(false);\n\n  /** Access the current room context so we can reset state correctly when it changes */\n  const { roomName } = useRoom();\n\n  // Reset state when room changes.\n  useEffect(() => {\n    return () => {\n      // Reset all state\n      allMessagesRef.current = [];\n      serialSetRef.current = new Set();\n      nextPageRef.current = undefined;\n      initialHistoryLoadedRef.current = false;\n      recoveringRef.current = false;\n\n      setVersion(0);\n      setActiveMessages([]);\n      setAnchorIdx(-1);\n    };\n  }, [roomName]);\n\n  const { historyBeforeSubscribe } = useMessages({\n    listener: (event: ChatMessageEvent) => {\n      const { message, type } = event;\n      switch (type) {\n        case ChatMessageEventType.Created:\n        case ChatMessageEventType.Updated:\n        case ChatMessageEventType.Deleted: {\n          updateMessages([message]);\n          break;\n        }\n        default: {\n          console.error('Unknown message event type:', type);\n        }\n      }\n    },\n    reactionsListener: (event) => {\n      setVersion((prevVersion) => {\n        const messageSerial = event.summary.messageSerial;\n        let changed = false;\n\n        // If we don't have the message for this reaction, we can't do anything\n        if (serialSetRef.current.has(messageSerial)) {\n          const idx = findMessageIndex(allMessagesRef.current, messageSerial);\n          if (idx !== -1 && allMessagesRef.current[idx]) {\n            const currentMessage = allMessagesRef.current[idx];\n            const merged = currentMessage.with(event);\n            if (merged !== currentMessage) {\n              allMessagesRef.current[idx] = merged;\n              changed = true;\n            }\n          }\n        }\n\n        return changed ? prevVersion + 1 : prevVersion; // Only increment if changed\n      });\n    },\n    onDiscontinuity: () => {\n      // Get the serial of the last message in the current window\n      const messages = allMessagesRef.current;\n      if (messages.length === 0) return;\n\n      // eslint-disable-next-line unicorn/prefer-at\n      const lastReceivedMessage = messages[messages.length - 1];\n      if (!lastReceivedMessage) return;\n      handleDiscontinuity(lastReceivedMessage.serial);\n    },\n  });\n\n  const [hasMoreHistory, setHasMoreHistory] = useState<boolean>(Boolean(historyBeforeSubscribe));\n\n  /**\n   * Binary search to find message index by serial\n   *\n   * @param messages - Sorted array of messages to search\n   * @param targetSerial - The serial number to find\n   * @param reverse - Whether to search in reverse order. Defaults to false (ascending order), as this is the order in which messages are received.\n   * @returns Index of the message, or -1 if not found\n   */\n  const findMessageIndex = useCallback(\n    (messages: Message[], targetSerial: string, reverse = false): number => {\n      // If no messages, return -1\n      if (messages.length === 0) return -1;\n\n      let left = 0;\n      let right = messages.length - 1;\n\n      while (left <= right) {\n        const mid = Math.floor((left + right) / 2);\n        const midMessage = messages[mid];\n\n        if (!midMessage) return -1;\n\n        const midSerial = midMessage.serial;\n\n        if (midSerial === targetSerial) {\n          return mid;\n        }\n\n        // If we are searching in reverse order, flip the comparison\n        const shouldGoRight = reverse ? midSerial > targetSerial : midSerial < targetSerial;\n\n        if (shouldGoRight) {\n          left = mid + 1;\n        } else {\n          right = mid - 1;\n        }\n      }\n\n      return -1; // Not found\n    },\n    []\n  );\n\n  /**\n   * Binary search to find optimal insertion position for new messages\n   *\n   * @param messages - Current sorted messages array\n   * @param newMessage - Message to insert\n   * @returns Index where the new message should be inserted\n   */\n  const findInsertionIndex = useCallback((messages: Message[], newMessage: Message): number => {\n    let left = 0;\n    let right = messages.length;\n\n    while (left < right) {\n      const mid = Math.floor((left + right) / 2);\n      const midMessage = messages[mid];\n\n      if (!midMessage) {\n        return -1;\n      }\n\n      if (newMessage.before(midMessage)) {\n        right = mid;\n      } else {\n        left = mid + 1;\n      }\n    }\n\n    return left;\n  }, []);\n\n  // TODO: More optimizations may be needed here, but further load testing is required to determine.\n  const updateMessages = useCallback(\n    (msgs: Message[], prepend = false) => {\n      if (msgs.length === 0) return;\n\n      setVersion((prevVersion) => {\n        if (prevVersion === 0 && allMessagesRef.current.length > 0) {\n          // If this is the first update and we already have messages, we need to reset the state\n          allMessagesRef.current = [];\n          serialSetRef.current.clear();\n        }\n        let changed = false;\n        let insertedBeforeAnchor = 0;\n        const allMessages = allMessagesRef.current;\n        for (const m of msgs) {\n          // Handle existing messages\n          if (serialSetRef.current.has(m.serial)) {\n            const idx = findMessageIndex(allMessages, m.serial);\n            const existingMessage = allMessagesRef.current[idx];\n            const merged = existingMessage?.with(m);\n\n            if (merged && merged !== existingMessage) {\n              allMessagesRef.current[idx] = merged;\n              changed = true;\n            }\n            continue;\n          }\n\n          // Handle new messages\n          const firstMessage = allMessages[0];\n          const lastMessage = allMessages.at(-1);\n\n          // Prepend if requested and message is older than first\n          if (prepend && (allMessages.length === 0 || (firstMessage && m.before(firstMessage)))) {\n            allMessages.unshift(m);\n            if (anchorIdx !== -1) insertedBeforeAnchor += 1;\n          }\n          // Append if message is newer than last\n          else if (allMessages.length === 0 || (lastMessage && m.after(lastMessage))) {\n            allMessages.push(m);\n          }\n          // Insert at correct position\n          else {\n            const insIdx = findInsertionIndex(allMessages, m);\n            allMessages.splice(insIdx, 0, m);\n            if (anchorIdx !== -1 && insIdx <= anchorIdx) insertedBeforeAnchor += 1;\n          }\n\n          serialSetRef.current.add(m.serial);\n          changed = true;\n        }\n\n        if (changed && insertedBeforeAnchor) {\n          setAnchorIdx((a) => (a === -1 ? a : a + insertedBeforeAnchor));\n        }\n\n        return changed ? prevVersion + 1 : prevVersion;\n      });\n    },\n    [anchorIdx, findInsertionIndex, findMessageIndex]\n  );\n\n  const handleDiscontinuity = useCallback(\n    (recoverFromSerial: string) => {\n      // Nothing to do if we are already recovering or we have no history API\n      if (recoveringRef.current || !historyBeforeSubscribe) return;\n\n      recoveringRef.current = true;\n      setLoading(true);\n\n      void (async () => {\n        try {\n          let page = await historyBeforeSubscribe({ limit: historyBatchSize });\n          for (;;) {\n            updateMessages(page.items.reverse());\n            // Binary search the sorted list in reverse order, since history is order descending\n            if (findMessageIndex(page.items, recoverFromSerial, true) !== -1) {\n              break;\n            }\n\n            if (page.hasNext()) {\n              const nextPage = await page.next();\n              if (!nextPage) break; // no more pages\n              page = nextPage; // move further back in time\n            } else {\n              break; // no more pages\n            }\n          }\n        } catch (error: unknown) {\n          console.error('Discontinuity recovery failed', error);\n        } finally {\n          recoveringRef.current = false;\n          setLoading(false);\n        }\n      })();\n    },\n    [findMessageIndex, historyBeforeSubscribe, updateMessages, historyBatchSize]\n  );\n\n  /* Reset initial load state when historyBeforeSubscribe changes */\n  useEffect(() => {\n    initialHistoryLoadedRef.current = false;\n  }, [historyBeforeSubscribe]);\n\n  /* Initial load */\n  useEffect(() => {\n    if (!historyBeforeSubscribe || initialHistoryLoadedRef.current) return;\n\n    let cancelled = false;\n    initialHistoryLoadedRef.current = true;\n\n    const load = async () => {\n      setLoading(true);\n      try {\n        // Load enough messages to fill more than the window size and overscan\n        const page = await historyBeforeSubscribe({ limit: windowSize + overscan * 2 });\n        if (cancelled) return;\n\n        updateMessages(page.items, true); // prepend older msgs\n        nextPageRef.current = page.hasNext() ? () => page.next() : undefined;\n        setHasMoreHistory(page.hasNext());\n      } catch (error) {\n        console.error('History load failed', error);\n        initialHistoryLoadedRef.current = false;\n      } finally {\n        if (!cancelled) setLoading(false);\n      }\n    };\n\n    void load();\n    return () => {\n      cancelled = true;\n    };\n  }, [historyBeforeSubscribe, overscan, updateMessages, windowSize]);\n\n  /* Load more history on demand */\n  const loadMoreHistory = useCallback(async () => {\n    if (loading || !hasMoreHistory || !nextPageRef.current) return;\n\n    setLoading(true);\n    try {\n      const page = await nextPageRef.current();\n      if (page) {\n        updateMessages(page.items, true);\n        nextPageRef.current = page.hasNext() ? () => page.next() : undefined;\n        setHasMoreHistory(page.hasNext());\n      } else {\n        nextPageRef.current = undefined;\n        setHasMoreHistory(false);\n      }\n    } catch (error) {\n      console.error('History load failed', error);\n    } finally {\n      setLoading(false);\n    }\n  }, [loading, hasMoreHistory, updateMessages]);\n\n  const computeWindow = useCallback(\n    (arr: Message[], anchor: number): Message[] => {\n      if (arr.length === 0) return [];\n\n      const latest = arr.length - 1;\n      const idx = anchor === -1 ? latest : Math.max(0, Math.min(anchor, latest));\n      const half = Math.floor(windowSize / 2);\n      const start = Math.max(0, idx - half - overscan);\n      const end = Math.min(arr.length, idx + half + overscan + 1);\n      return arr.slice(start, end);\n    },\n    [windowSize, overscan]\n  );\n\n  // Effects depend on version instead of allMessages\n  useEffect(() => {\n    setActiveMessages(computeWindow(allMessagesRef.current, anchorIdx));\n  }, [version, anchorIdx, computeWindow]);\n\n  const showLatestMessages = useCallback(() => {\n    setAnchorIdx(-1);\n  }, []);\n\n  const scrollBy = useCallback((delta: number) => {\n    setAnchorIdx((prev) => {\n      if (allMessagesRef.current.length === 0) return prev;\n      const latest = allMessagesRef.current.length - 1;\n      const base = prev === -1 ? latest : prev;\n      const next = base + delta;\n      if (next >= latest) return -1; // tail‑follow again\n      if (next < 0) return 0;\n      return next;\n    });\n  }, []);\n\n  const showMessagesAroundSerial = useCallback(\n    (serial: string) => {\n      const idx = findMessageIndex(allMessagesRef.current, serial);\n      if (idx !== -1) setAnchorIdx(idx);\n    },\n    [findMessageIndex]\n  );\n\n  return {\n    activeMessages,\n    updateMessages,\n    showLatestMessages,\n    scrollBy,\n    showMessagesAroundSerial,\n    loading,\n    hasMoreHistory,\n    loadMoreHistory,\n  };\n};\n","import { createContext } from 'react';\n\nimport { AvatarData } from '../components/atoms/avatar.tsx';\nimport { AvatarChangeCallback, PersistedAvatarData } from '../providers/avatar-provider.tsx';\n\n/**\n * Shape of the AvatarContext value providing comprehensive avatar management\n */\nexport interface AvatarContextType {\n  /**\n   * Gets an avatar for a user if it exists in the cache\n   * @param clientId - The unique identifier for the user\n   * @param displayName - Optional display name (not used for lookup, only for creation)\n   * @returns The avatar data if it exists, undefined otherwise\n   */\n  getAvatarForUser: (clientId: string, displayName?: string) => AvatarData | undefined;\n\n  /**\n   * Creates an avatar for a user and adds it to the cache\n   * @param clientId - The unique identifier for the user\n   * @param displayName - Optional display name (defaults to clientId if not provided)\n   * @returns The created avatar data\n   */\n  createAvatarForUser: (clientId: string, displayName?: string) => AvatarData;\n\n  /**\n   * Gets an avatar for a room if it exists in the cache\n   * @param roomName - The unique identifier for the room\n   * @param displayName - Optional display name (not used for lookup, only for creation)\n   * @returns The avatar data if it exists, undefined otherwise\n   */\n  getAvatarForRoom: (roomName: string, displayName?: string) => AvatarData | undefined;\n\n  /**\n   * Creates an avatar for a room and adds it to the cache\n   * @param roomName - The unique identifier for the room\n   * @param displayName - Optional display name (defaults to roomName if not provided)\n   * @returns The created avatar data\n   */\n  createAvatarForRoom: (roomName: string, displayName?: string) => AvatarData;\n\n  /**\n   * Updates an existing user avatar or creates a new one\n   * @param clientId - The unique identifier for the user\n   * @param avatar - Partial avatar data to update\n   */\n  setUserAvatar: (clientId: string, avatar: Partial<AvatarData>) => void;\n\n  /**\n   * Updates an existing room avatar or creates a new one\n   * @param roomName - The unique identifier for the room\n   * @param avatar - Partial avatar data to update\n   */\n  setRoomAvatar: (roomName: string, avatar: Partial<AvatarData>) => void;\n\n  /**\n   * Returns all cached user avatars (some values may be undefined)\n   * @returns Record of user ID to avatar data or undefined\n   */\n  getUserAvatars: () => Record<string, AvatarData | undefined>;\n\n  /**\n   * Returns all cached room avatars (some values may be undefined)\n   * @returns Record of room ID to avatar data or undefined\n   */\n  getRoomAvatars: () => Record<string, AvatarData | undefined>;\n\n  /**\n   * Clears all user avatars from cache\n   */\n  clearUserAvatars: () => void;\n\n  /**\n   * Clears all room avatars from cache\n   */\n  clearRoomAvatars: () => void;\n\n  /**\n   * Clears all avatars from cache\n   */\n  clearAllAvatars: () => void;\n\n  /**\n   * Registers a callback for avatar change events\n   * @param callback - Function to call when avatars change\n   * @returns Cleanup function to remove the callback\n   */\n  onAvatarChange: (callback: AvatarChangeCallback) => () => void;\n\n  /**\n   * Exports all avatar data for backup/migration (cleans undefined entries)\n   * @returns Serializable avatar data\n   */\n  exportAvatars: () => PersistedAvatarData;\n\n  /**\n   * Imports avatar data from backup/migration\n   * @param data - Previously exported avatar data\n   */\n  importAvatars: (data: PersistedAvatarData) => void;\n}\n\n/**\n * React context for comprehensive avatar management\n * Provides avatar caching, persistence, and change notifications\n */\nexport const AvatarContext = createContext<AvatarContextType | undefined>(undefined);\n","import { useContext } from 'react';\n\nimport { AvatarContext, type AvatarContextType } from '../context/avatar-context.tsx';\n\n/**\n * Hook to access the avatar context with comprehensive avatar management.\n *\n * @returns The avatar context value\n * @throws Error if used outside of an AvatarProvider\n *\n * @example\n * // Basic usage\n * const { getAvatarForUser, setUserAvatar } = useAvatar();\n * const userAvatar = getAvatarForUser('user-123', 'John Doe');\n *\n * @example\n * // Listen for avatar changes\n * const { onAvatarChange } = useAvatar();\n * useEffect(() => {\n *   const cleanup = onAvatarChange((type, id, avatar, prev) => {\n *     console.log(`${type} avatar changed for ${id}`);\n *   });\n *   return cleanup;\n * }, [onAvatarChange]);\n *\n * @example\n * // Backup and restore avatars\n * const { exportAvatars, importAvatars } = useAvatar();\n * const backup = exportAvatars();\n * // ... later\n * importAvatars(backup);\n */\nexport const useAvatar = (): AvatarContextType => {\n  const context = useContext(AvatarContext);\n\n  if (context === undefined) {\n    throw new Error(\n      'useAvatar must be used within an AvatarProvider. ' +\n        'Make sure your component is wrapped with <AvatarProvider>.'\n    );\n  }\n\n  return context;\n};\n","import { useCallback, useEffect, useState } from 'react';\n\nimport { AvatarData } from '../components/atoms/avatar.tsx';\nimport { useAvatar } from './use-avatar.tsx';\n\n/**\n * Props for the useUserAvatar hook\n */\nexport interface UseUserAvatarProps {\n  /**\n   * The unique identifier for the user.\n   * Used as the primary key for avatar storage and retrieval.\n   * Should be consistent across all components referencing the same user.\n   * This is typically the client ID used in Ably Chat.\n   *\n   * @example\n   * // Using client ID only\n   * const { userAvatar } = useUserAvatar({ clientId: \"user_123\" });\n   *\n   */\n  clientId: string;\n\n  /**\n   * Optional human-readable display name for the user.\n   * Used as the default displayName when creating new avatars.\n   * If not provided, falls back to using clientId as the display name.\n   * Can be updated later using setUserAvatar for personalization.\n   *\n   * @example\n   * // With custom display name\n   * const { userAvatar } = useUserAvatar({\n   *   clientId: \"user_123\",\n   *   displayName: \"Alice Smith\"\n   * });\n   *\n   * @example\n   * // Display name defaults to clientId\n   * const { userAvatar } = useUserAvatar({ clientId: \"alice-smith\" });\n   * // → displayName will be \"alice-smith\"\n   *\n   */\n  displayName?: string;\n}\n\n/**\n * Return type for the useUserAvatar hook\n */\nexport interface UseUserAvatarReturn {\n  /**\n   * The current avatar data for the user.\n   * Contains display name, color, initials, and optional image source.\n   * Undefined during initial loading or if avatar creation fails.\n   * Updates automatically when avatar data changes through context.\n   *\n   * @example\n   * const { userAvatar } = useUserAvatar({ clientId: \"user_123\" });\n   *\n   * if (userAvatar) {\n   *   console.log(userAvatar.displayName); // \"Alice Smith\"\n   *   console.log(userAvatar.color);       // \"#3B82F6\"\n   *   console.log(userAvatar.initials);    // \"AS\"\n   *   console.log(userAvatar.src);         // \"https://...\" or undefined\n   * }\n   */\n  userAvatar: AvatarData | undefined;\n\n  /**\n   * Function to update the user avatar with new data.\n   * Merges provided data with existing avatar properties.\n   * Updates both the avatar context cache and local component state.\n   * Changes persist across component unmounts and remounts.\n   *\n   * @param avatarData - Partial avatar data to merge with existing data\n   *\n   * @example\n   * // Update display name and color\n   * setUserAvatar({\n   *   displayName: \"Alice Johnson\",\n   *   color: \"#EF4444\"\n   * });\n   *\n   * @example\n   * // Add profile picture\n   * setUserAvatar({\n   *   src: \"https://example.com/profile.jpg\"\n   * });\n   *\n   */\n  setUserAvatar: (avatarData: Partial<AvatarData>) => void;\n}\n\n/**\n * Custom hook for managing user avatar data with automatic generation and caching\n *\n * Features:\n * - Automatic avatar retrieval from the avatar context cache\n * - On-demand avatar creation for new users with generated colors and initials\n * - Synchronized across all components using the same clientId under the same context\n * - Persistent storage through the avatar context provider\n * - Optimistic UI updates with immediate local state changes\n * - Fallback display name generation from clientId when not provided\n *\n * Avatar Generation Logic:\n * - Checks context cache for existing user avatar first\n * - Creates new avatar if none exists using clientId and display name\n * - Generates deterministic color using hash algorithm based on clientId\n * - Extracts initials from display name or clientId for fallback display\n * - Caches generated avatar for future use across all components\n *\n *\n * @example\n * // Basic usage in message components\n * const MessageAvatar = ({ message }) => {\n *   const { userAvatar } = useUserAvatar({\n *     clientId: message.clientId,\n *     displayName: message.senderName\n *   });\n *\n *   return (\n *     <Avatar\n *       src={userAvatar?.src}\n *       color={userAvatar?.color}\n *       initials={userAvatar?.initials}\n *       alt={userAvatar?.displayName}\n *       size=\"sm\"\n *     />\n *   );\n * };\n *\n * @example\n * // User profile editing with avatar customization\n * const UserProfile = ({ clientId, name }) => {\n *   const { userAvatar, setUserAvatar } = useUserAvatar({\n *     clientId,\n *     displayName: name\n *   });\n *\n *   const handleNameChange = (newName) => {\n *     setUserAvatar({ displayName: newName });\n *     updateUserProfile({ name: newName });\n *   };\n *\n *   const handleAvatarUpload = async (file) => {\n *     const uploadedUrl = await uploadImage(file);\n *     setUserAvatar({ src: uploadedUrl });\n *   };\n *\n *   return (\n *     <div className=\"profile-editor\">\n *       <AvatarUploader\n *         currentAvatar={userAvatar}\n *         onUpload={handleAvatarUpload}\n *       />\n *       <TextInput\n *         value={userAvatar?.displayName || ''}\n *         onChange={handleNameChange}\n *         placeholder=\"Enter display name\"\n *       />\n *     </div>\n *   );\n * };\n */\nexport const useUserAvatar = ({\n  clientId,\n  displayName,\n}: UseUserAvatarProps): UseUserAvatarReturn => {\n  const { getAvatarForUser, createAvatarForUser, setUserAvatar: updateUserAvatar } = useAvatar();\n  const [avatar, setAvatar] = useState<AvatarData | undefined>();\n\n  useEffect(() => {\n    // Try to get existing avatar\n    const existingAvatar = getAvatarForUser(clientId);\n\n    if (existingAvatar) {\n      setAvatar(existingAvatar);\n    } else {\n      // Create a new avatar if one doesn't exist\n      const newAvatar = createAvatarForUser(clientId, displayName);\n      setAvatar(newAvatar);\n    }\n  }, [getAvatarForUser, createAvatarForUser, clientId, displayName]);\n\n  /**\n   * Updates the user avatar both in the cache and local state\n   *\n   * @param avatarData - Partial avatar data to update\n   */\n  const setUserAvatar = useCallback(\n    (avatarData: Partial<AvatarData>) => {\n      updateUserAvatar(clientId, avatarData);\n      // Update local state to reflect changes immediately\n      setAvatar((prev) => (prev ? { ...prev, ...avatarData } : undefined));\n    },\n    [updateUserAvatar, clientId]\n  );\n\n  return {\n    userAvatar: avatar,\n    setUserAvatar,\n  };\n};\n","import React, { useEffect, useState } from 'react';\n\n/**\n * AvatarData interface defines the structure for avatar data across the application.\n * This standardized format ensures consistent avatar representation.\n *\n * @property src - URL to the avatar image (optional)\n * @property color - Background color for initials fallback (optional, will be generated if not provided)\n * @property initials - Custom initials to display when no image is available (optional, will be generated from displayName)\n * @property displayName - Used for alt text and generating initials (required)\n */\nexport interface AvatarData {\n  src?: string;\n  color?: string;\n  initials?: string;\n  displayName: string;\n}\n\n/**\n * Generates a deterministic color based on text\n * @param text - The text to generate a color from\n * @returns A Tailwind CSS color class\n */\nconst getRandomColor = (text: string): string => {\n  const colors = [\n    'bg-blue-500',\n    'bg-purple-500',\n    'bg-green-500',\n    'bg-orange-500',\n    'bg-red-500',\n    'bg-pink-500',\n    'bg-indigo-500',\n    'bg-yellow-500',\n    'bg-teal-500',\n    'bg-cyan-500',\n    'bg-emerald-500',\n    'bg-violet-500',\n    'bg-amber-500',\n    'bg-rose-500',\n    'bg-fuchsia-500',\n    'bg-sky-500',\n  ];\n\n  // Generate a deterministic hash from the text\n  let hash = 0;\n  for (let i = 0; i < text.length; i++) {\n    hash = ((hash << 5) - hash + (text.codePointAt(i) ?? 0)) & 0xffffffff;\n  }\n\n  const colorIndex = Math.abs(hash) % colors.length;\n  return colors[colorIndex] || 'bg-gray-500';\n};\n\n/**\n * Recommended image dimensions: 256x256 pixels\n * Supported formats: JPG, PNG, WebP, SVG\n * Maximum file size: 5MB\n */\nexport interface AvatarProps {\n  /**\n   * URL to the avatar image\n   */\n  src?: string;\n\n  /**\n   * Alternative text for the avatar image, also used for generating initials if needed\n   */\n  alt?: string;\n\n  /**\n   * Background color for the avatar when no image is provided\n   * Uses Tailwind CSS color classes (e.g., 'bg-blue-500')\n   * If not provided, a color will be generated based on the alt text\n   */\n  color?: string;\n\n  /**\n   * Size of the avatar\n   * - sm: 32px (2rem)\n   * - md: 40px (2.5rem) - default\n   * - lg: 48px (3rem)\n   * - xl: 64px (4rem)\n   */\n  size?: 'sm' | 'md' | 'lg' | 'xl';\n\n  /**\n   * Custom initials to display when no image is available\n   * If not provided, initials will be generated from the alt text\n   */\n  initials?: string;\n\n  /**\n   * Click handler for the avatar, e.g for editing or viewing profiles\n   */\n  onClick?: () => void;\n}\n\n/**\n * Avatar component displays a user or room avatar with fallback to initials\n *\n * TODO: Consider breaking this component into smaller subcomponents:\n * - AvatarImage: Handles image loading and error states\n * - AvatarInitials: Handles initials generation and display\n * - AvatarContainer: Handles sizing and common styling\n *\n * TODO:\n * - Status indicators (online/offline/away)\n * - Avatar groups/stacks for multiple users\n * - Upload functionality for editable avatars\n * - Image optimization and lazy loading\n *\n * @example\n * // Basic usage\n * <Avatar alt=\"John Doe\" />\n *\n * @example\n * // With image\n * <Avatar src=\"https://example.com/avatar.jpg\" alt=\"John Doe\" />\n *\n * @example\n * // With custom color and size\n * <Avatar alt=\"John Doe\" color=\"bg-purple-500\" size=\"lg\" />\n *\n * @example\n * // Using AvatarData object\n * const avatarData = { displayName: \"John Doe\", src: \"https://example.com/avatar.jpg\" };\n * <Avatar alt={avatarData.displayName} src={avatarData.src} color={avatarData.color} initials={avatarData.initials} />\n */\nexport const Avatar = ({ src, alt, color, size = 'md', initials, onClick }: AvatarProps) => {\n  const [imgError, setImgError] = useState(false);\n\n  // Reset image error state if src changes\n  useEffect(() => {\n    setImgError(false);\n  }, [src]);\n\n  // TODO: Extract to separate hook - useAvatarSizing\n  // Size classes mapping\n  const sizeClasses = {\n    sm: 'w-8 h-8 text-sm',\n    md: 'w-10 h-10 text-lg',\n    lg: 'w-12 h-12 text-xl',\n    xl: 'w-16 h-16 text-2xl',\n  };\n\n  // Use provided color or generate one based on alt text\n  const avatarColor = color || getRandomColor(alt ?? 'default');\n\n  // TODO: Extract to separate utility - generateInitials\n  /**\n   * Generates display text (initials) for the avatar\n   * @returns Up to 2 characters of initials\n   */\n  const getDisplayText = () => {\n    // Use provided initials if available\n    if (initials) return initials;\n\n    // Fallback to generating initials from alt text\n    if (!alt || alt.trim() === '') return '??'; // Handle empty or whitespace-only alt text\n\n    const words = alt\n      .trim()\n      .split(/\\s+/)\n      .filter((word) => word.length > 0)\n      .filter((word) => /^[a-zA-Z]/.test(word));\n\n    if (words.length >= 2 && words[0] && words[1]) {\n      const firstChar = words[0][0];\n      const secondChar = words[1][0];\n      if (firstChar && secondChar) {\n        return (firstChar + secondChar).toUpperCase();\n      }\n    }\n\n    if (words.length === 1 && words[0] && words[0].length >= 2) {\n      return words[0].slice(0, 2).toUpperCase();\n    }\n\n    // Final fallback using the original alt text\n    return alt.slice(0, 2).toUpperCase();\n  };\n\n  // Handle image loading error\n  const handleImageError = () => {\n    setImgError(true);\n  };\n\n  // Determine if we're showing an image or initials\n  const showingImage = src && !imgError;\n\n  return (\n    <div\n      className={`${sizeClasses[size]} rounded-full flex items-center justify-center text-white font-medium ${avatarColor} relative ${\n        onClick\n          ? 'cursor-pointer hover:opacity-80 transition-opacity focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'\n          : ''\n      }`}\n      onClick={onClick}\n      onKeyDown={\n        onClick\n          ? (e) => {\n              if (e.key === 'Enter' || e.key === ' ') {\n                e.preventDefault();\n                onClick();\n              }\n            }\n          : undefined\n      }\n      data-testid=\"avatar-component\"\n      role={onClick ? 'button' : showingImage ? undefined : 'img'}\n      tabIndex={onClick ? 0 : undefined}\n      title={alt}\n      aria-label={alt}\n    >\n      {src && !imgError ? (\n        <img\n          src={src}\n          alt={alt}\n          className=\"w-full h-full rounded-full object-cover\"\n          onError={handleImageError}\n          loading=\"lazy\"\n        />\n      ) : (\n        <span aria-hidden=\"true\">{getDisplayText()}</span>\n      )}\n    </div>\n  );\n};\n","import React from 'react';\n\n/**\n * Visual variants for button styling\n */\ntype ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'outline' | 'danger';\n\n/**\n * Size options for button dimensions\n */\ntype ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n\n/**\n * Props for the Button component\n */\nexport interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  /**\n   * Visual variant of the button\n   * - 'primary': Main action button with primary brand color\n   * - 'secondary': Secondary action with muted styling\n   * - 'ghost': Transparent button with minimal styling\n   * - 'outline': Button with border and no background\n   * - 'danger': Destructive action button (red)\n   * @default 'primary'\n   */\n  variant?: ButtonVariant;\n\n  /**\n   * Size of the button\n   * - 'xs': Extra small (px-2 py-1 text-xs)\n   * - 'sm': Small (px-3 py-1.5 text-sm)\n   * - 'md': Medium (px-4 py-2 text-sm) - default\n   * - 'lg': Large (px-6 py-3 text-base)\n   * - 'xl': Extra large (px-8 py-4 text-lg)\n   * @default 'md'\n   */\n  size?: ButtonSize;\n\n  /**\n   * Button content - text, icons, or other React elements\n   */\n  children: React.ReactNode;\n\n  /**\n   * Additional CSS classes to apply to the button\n   */\n  className?: string;\n\n  /**\n   * Loading state - shows spinner and disables interaction\n   * @default false\n   */\n  loading?: boolean;\n\n  /**\n   * Icon to display before the button text\n   */\n  leftIcon?: React.ReactNode;\n\n  /**\n   * Icon to display after the button text\n   */\n  rightIcon?: React.ReactNode;\n\n  /**\n   * Whether the button should take full width of its container\n   * @default false\n   */\n  fullWidth?: boolean;\n\n  /**\n   * Custom loading spinner component\n   * If not provided, uses default spinner\n   */\n  loadingSpinner?: React.ReactNode;\n}\n\n/**\n * Default loading spinner component\n */\nconst DefaultSpinner = ({ size }: { size: ButtonSize }) => {\n  const spinnerSizes = {\n    xs: 'w-3 h-3',\n    sm: 'w-4 h-4',\n    md: 'w-4 h-4',\n    lg: 'w-5 h-5',\n    xl: 'w-6 h-6',\n  };\n\n  return (\n    <svg className={`${spinnerSizes[size]} animate-spin`} fill=\"none\" viewBox=\"0 0 24 24\">\n      <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\" />\n      <path\n        className=\"opacity-75\"\n        fill=\"currentColor\"\n        d=\"m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n      />\n    </svg>\n  );\n};\n\n/**\n * Button component provides a highly customizable button with multiple variants and states\n *\n * Features:\n * - Multiple visual variants (primary, secondary, ghost, outline, danger)\n * - Size variations from extra small to extra large\n * - Loading states with customizable spinners\n * - Icon support (left and right positioning)\n * - Full accessibility support with proper ARIA attributes\n * - Dark mode compatible\n * - Focus management and keyboard navigation\n * - Disabled state handling\n *\n * @example\n * // Basic usage\n * <Button>Click me</Button>\n *\n * @example\n * // Secondary variant with icon\n * <Button variant=\"secondary\" leftIcon={<PlusIcon />}>\n *   Add Item\n * </Button>\n *\n * @example\n * // Loading state\n * <Button loading onClick={handleSubmit}>\n *   {loading ? 'Submitting...' : 'Submit'}\n * </Button>\n *\n * @example\n * // Danger variant for destructive actions\n * <Button\n *   variant=\"danger\"\n *   size=\"lg\"\n *   onClick={() => confirmDelete()}\n * >\n *   Delete Account\n * </Button>\n *\n * @example\n * // Full width button\n * <Button fullWidth variant=\"primary\">\n *   Continue\n * </Button>\n */\nexport const Button = ({\n  variant = 'primary',\n  size = 'md',\n  children,\n  className = '',\n  loading = false,\n  leftIcon,\n  rightIcon,\n  fullWidth = false,\n  loadingSpinner,\n  disabled,\n  ...props\n}: ButtonProps) => {\n  // Base classes applied to all buttons\n  const baseClasses = [\n    'inline-flex items-center justify-center',\n    'font-medium rounded-md',\n    'transition-all duration-200 ease-in-out',\n    'focus:outline-none focus:ring-2 focus:ring-offset-2',\n    'disabled:opacity-50 disabled:pointer-events-none disabled:cursor-not-allowed',\n    // Full width handling\n    fullWidth ? 'w-full' : '',\n  ]\n    .filter(Boolean)\n    .join(' ');\n\n  // Variant-specific styling\n  const variantClasses: Record<ButtonVariant, string> = {\n    primary: [\n      'bg-blue-600 text-white',\n      'hover:bg-blue-700 active:bg-blue-800',\n      'focus:ring-blue-500',\n      'dark:bg-blue-500 dark:hover:bg-blue-600',\n    ].join(' '),\n\n    secondary: [\n      'bg-gray-200 text-gray-900',\n      'hover:bg-gray-300 active:bg-gray-400',\n      'focus:ring-gray-500',\n      'dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600',\n    ].join(' '),\n\n    ghost: [\n      'text-gray-700 bg-transparent',\n      'hover:bg-gray-100 active:bg-gray-200',\n      'focus:ring-gray-500',\n      'dark:text-gray-300 dark:hover:bg-gray-800 dark:active:bg-gray-700',\n    ].join(' '),\n\n    outline: [\n      'border border-gray-300 bg-transparent text-gray-700',\n      'hover:bg-gray-50 active:bg-gray-100',\n      'focus:ring-gray-500',\n      'dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800',\n    ].join(' '),\n\n    danger: [\n      'bg-red-600 text-white',\n      'hover:bg-red-700 active:bg-red-800',\n      'focus:ring-red-500',\n      'dark:bg-red-500 dark:hover:bg-red-600',\n    ].join(' '),\n  };\n\n  // Size-specific styling\n  const sizeClasses: Record<ButtonSize, string> = {\n    xs: 'px-2 py-1 text-xs gap-1',\n    sm: 'px-3 py-1.5 text-sm gap-1.5',\n    md: 'px-4 py-2 text-sm gap-2',\n    lg: 'px-6 py-3 text-base gap-2',\n    xl: 'px-8 py-4 text-lg gap-3',\n  };\n\n  // Determine if button should be disabled\n  const isDisabled = disabled || loading;\n\n  return (\n    <button\n      className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`.trim()}\n      disabled={isDisabled}\n      aria-disabled={isDisabled}\n      {...props}\n    >\n      {/* Left icon or loading spinner */}\n      {loading ? (\n        loadingSpinner || <DefaultSpinner size={size} />\n      ) : leftIcon ? (\n        <span className=\"flex-shrink-0\" aria-hidden=\"true\">\n          {leftIcon}\n        </span>\n      ) : undefined}\n\n      {/* Button content */}\n      <span className={loading ? 'opacity-70' : ''}>{children}</span>\n\n      {/* Right icon (hidden during loading) */}\n      {!loading && rightIcon && (\n        <span className=\"flex-shrink-0\" aria-hidden=\"true\">\n          {rightIcon}\n        </span>\n      )}\n    </button>\n  );\n};\n","import { clsx } from 'clsx';\nimport React from 'react';\n\n/**\n * Available icon names in the icon library\n */\ntype IconName =\n  | 'send'\n  | 'info'\n  | 'moon'\n  | 'sun'\n  | 'phone'\n  | 'video'\n  | 'more'\n  | 'attachment'\n  | 'emoji'\n  | 'thumbsup'\n  | 'edit'\n  | 'delete'\n  | 'close'\n  | 'chevronleft'\n  | 'chevronright'\n  | 'upload';\n\n/**\n * Size options for icons\n */\ntype IconSize = 'sm' | 'md' | 'lg' | 'xl';\n\n/**\n * Props for the Icon component\n */\nexport interface IconProps {\n  /**\n   * Name of the icon to display\n   * Must be one of the predefined icon names in the icon library\n   */\n  name: IconName;\n\n  /**\n   * Size of the icon\n   * - 'sm': 16px (w-4 h-4)\n   * - 'md': 20px (w-5 h-5) - default\n   * - 'lg': 24px (w-6 h-6)\n   * - 'xl': 32px (w-8 h-8)\n   * @default 'md'\n   */\n  size?: IconSize;\n\n  /**\n   * Additional CSS classes to apply to the icon\n   */\n  className?: string;\n\n  /**\n   * Accessible label for the icon\n   * Required for interactive icons, optional for decorative icons\n   */\n  'aria-label'?: string;\n\n  /**\n   * Whether the icon is decorative only (hidden from screen readers)\n   * @default false\n   */\n  'aria-hidden'?: boolean;\n\n  /**\n   * Color variant for the icon\n   * @default 'current' - inherits current text color\n   */\n  color?: 'current' | 'primary' | 'secondary' | 'success' | 'warning' | 'error';\n\n  /**\n   * Click handler for interactive icons\n   */\n  onClick?: (event: React.MouseEvent<SVGSVGElement> | React.KeyboardEvent<SVGSVGElement>) => void;\n\n  /**\n   * Additional props to pass to the SVG element\n   */\n  svgProps?: React.SVGAttributes<SVGSVGElement>;\n}\n\n/**\n * Icon component renders SVG icons from a predefined icon library\n *\n * Features:\n * - Predefined icon library with common UI icons\n * - Multiple size options (sm, md, lg, xl)\n * - Accessibility support with proper ARIA attributes\n * - Interactive support with click handlers and keyboard navigation\n * - Consistent stroke-based design\n\n *\n * @example\n * // Basic usage\n * <Icon name=\"send\" />\n *\n * @example\n * // Large icon with custom color\n * <Icon\n *   name=\"heart\"\n *   size=\"lg\"\n *   color=\"error\"\n *   aria-label=\"Like this post\"\n * />\n *\n * @example\n * // Interactive icon with click handler\n * <Icon\n *   name=\"close\"\n *   onClick={() => setModalOpen(false)}\n *   aria-label=\"Close modal\"\n *   className=\"cursor-pointer hover:text-red-500\"\n * />\n *\n * @example\n * // Decorative icon (hidden from screen readers)\n * <Icon name=\"star\" aria-hidden />\n */\nexport const Icon = ({\n  name,\n  size = 'md',\n  className = '',\n  'aria-label': ariaLabel,\n  'aria-hidden': ariaHidden = false,\n  color = 'current',\n  onClick,\n  svgProps,\n}: IconProps) => {\n  // Size class mappings\n  const sizeClasses: Record<IconSize, string> = {\n    sm: 'w-4 h-4',\n    md: 'w-5 h-5',\n    lg: 'w-6 h-6',\n    xl: 'w-8 h-8',\n  };\n\n  // Color class mappings\n  const colorClasses = {\n    current: 'text-current',\n    primary: 'text-blue-600 dark:text-blue-400',\n    secondary: 'text-gray-600 dark:text-gray-400',\n    success: 'text-green-600 dark:text-green-400',\n    warning: 'text-yellow-600 dark:text-yellow-400',\n    error: 'text-red-600 dark:text-red-400',\n  };\n\n  /**\n   * SVG path definitions for each icon\n   * All icons are designed for a 24x24 viewBox with stroke-based rendering\n   */\n  const iconPaths: Record<IconName, string> = {\n    send: 'M12 19l9-7-9-7v4l-9 3 9 3v4z',\n    info: 'M12 8v4m0 4h.01M21 12c0 4.97-4.03 9-9 9s-9-4.03-9-9 4.03-9 9-9 9 4.03 9 9z',\n    moon: 'M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z',\n    sun: 'M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z',\n    phone:\n      'M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z',\n    video:\n      'M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z',\n    more: 'M12 5v.01M12 12v.01M12 19v.01',\n    attachment:\n      'M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13',\n    emoji:\n      'M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zM8 14s1.5 2 4 2 4-2 4-2M9 9h.01M15 9h.01',\n    thumbsup:\n      'M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V5a2 2 0 00-2-2h-.095c-.5 0-.905.405-.905.905 0 .714-.211 1.412-.608 2.006L7 11v9m7-10h-2M7 20H5a2 2 0 01-2-2v-6a2 2 0 012-2h2.5',\n    edit: 'M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z',\n    delete:\n      'M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16',\n    close: 'M6 18L18 6M6 6l12 12',\n    chevronleft: 'M15 18l-6-6 6-6',\n    chevronright: 'M9 18l6-6-6-6',\n    upload: 'M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12',\n  };\n\n  // Get the path for the requested icon\n  const iconPath = iconPaths[name];\n\n  // Handle missing icons gracefully\n  if (!iconPath) {\n    if (process.env.NODE_ENV === 'development') {\n      console.warn(\n        `Icon \"${name}\" not found. Available icons: ${Object.keys(iconPaths).join(', ')}`\n      );\n    }\n\n    return (\n      <div\n        className={`${sizeClasses[size]} ${className} flex items-center justify-center bg-gray-200 rounded text-gray-500 text-xs`}\n        title={`Missing icon: ${name}`}\n        aria-label={ariaLabel || `Missing icon: ${name}`}\n        role=\"img\"\n      >\n        ?\n      </div>\n    );\n  }\n  // Determine accessibility attributes\n  const isInteractive = !!onClick;\n  const shouldHideFromScreenReader = ariaHidden || (!ariaLabel && !isInteractive);\n\n  return (\n    <svg\n      className={clsx(\n        sizeClasses[size],\n        isInteractive && 'cursor-pointer',\n        colorClasses[color],\n        className\n      )}\n      fill=\"none\"\n      stroke=\"currentColor\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      aria-label={ariaLabel}\n      aria-hidden={shouldHideFromScreenReader}\n      onClick={onClick}\n      role={isInteractive ? 'button' : 'img'}\n      tabIndex={isInteractive ? 0 : undefined}\n      onKeyDown={\n        isInteractive\n          ? (e) => {\n              if (e.key === 'Enter' || e.key === ' ') {\n                e.preventDefault();\n                onClick(e);\n              }\n            }\n          : undefined\n      }\n      {...svgProps}\n    >\n      <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d={iconPath} />\n    </svg>\n  );\n};\n","import { clsx } from 'clsx';\nimport React, { useCallback, useEffect, useRef } from 'react';\n\n/**\n * Visual variants for the TextInput component\n */\ntype TextInputVariant = 'default' | 'message';\n\n/**\n * Size options for the TextInput component\n */\ntype TextInputSize = 'sm' | 'md' | 'lg';\n\n/**\n * Props for the TextInput component\n */\nexport interface TextInputProps\n  extends Omit<\n    React.InputHTMLAttributes<HTMLInputElement> & React.TextareaHTMLAttributes<HTMLTextAreaElement>,\n    'size' | 'prefix' | 'suffix'\n  > {\n  /**\n   * Visual variant of the input field\n   * - 'default': Standard form input with rectangular borders\n   * - 'message': Rounded chat message input optimized for messaging interfaces\n   * @default 'default'\n   */\n  variant?: TextInputVariant;\n\n  /**\n   * Size of the input field\n   * - 'sm': Small padding and text\n   * - 'md': Medium padding and text (default)\n   * - 'lg': Large padding and text\n   * @default 'md'\n   */\n  size?: TextInputSize;\n\n  /**\n   * Error state styling\n   * When true, applies error styling (red borders, etc.)\n   */\n  error?: boolean;\n\n  /**\n   * Success state styling\n   * When true, applies success styling (green borders, etc.)\n   */\n  success?: boolean;\n\n  /**\n   * Additional CSS classes to apply to the input\n   */\n  className?: string;\n\n  /**\n   * Prefix icon or element to display before the input text\n   */\n  prefix?: React.ReactNode;\n\n  /**\n   * Suffix icon or element to display after the input text\n   */\n  suffix?: React.ReactNode;\n\n  /**\n   * Whether to use a multi-line textarea instead of a single-line input\n   * @default false\n   */\n  multiline?: boolean;\n\n  /**\n   * Maximum height for the textarea when multiline is true\n   * After this height is reached, the textarea becomes scrollable\n   * @default '150px'\n   */\n  maxHeight?: string;\n}\n\n/**\n * TextInput component provides a customizable input field with multiple variants and states\n *\n * Features:\n * - Multiple visual variants (default form input, message input)\n * - Size variations (sm, md, lg)\n * - Error and success states with appropriate styling\n * - Dark mode support\n * - Accessibility: Basic ARIA attributes for error states\n * - Forward ref support for form libraries\n * - Prefix/suffix support for icons and buttons\n * - Support for multi-line input with auto-expansion\n * - Auto-scrolling to bottom when typing in multi-line mode\n *\n * @example\n * // Basic usage\n * <TextInput placeholder=\"Enter your name\" />\n *\n * @example\n * // Message variant for chat interfaces\n * <TextInput\n *   variant=\"message\"\n *   placeholder=\"Type a message...\"\n *   size=\"lg\"\n * />\n *\n * @example\n * // Multi-line input for longer messages\n * <TextInput\n *   multiline\n *   maxHeight=\"150px\"\n *   placeholder=\"Type a longer message...\"\n * />\n *\n * @example\n * // With error state\n * <TextInput\n *   error\n *   placeholder=\"Email\"\n *   aria-describedby=\"email-error\"\n * />\n *\n * @example\n * // With prefix and suffix\n * <TextInput\n *   prefix={<SearchIcon />}\n *   suffix={<Button>Submit</Button>}\n *   placeholder=\"Search...\"\n * />\n */\nexport const TextInput = React.forwardRef<HTMLInputElement | HTMLTextAreaElement, TextInputProps>(\n  (\n    {\n      variant = 'default',\n      size = 'md',\n      error = false,\n      success = false,\n      className = '',\n      prefix,\n      suffix,\n      disabled,\n      multiline = false,\n      maxHeight = '150px',\n      value,\n      onChange,\n      'aria-invalid': ariaInvalid,\n      ...props\n    },\n    ref\n  ) => {\n    // Create a local ref for the textarea to handle auto-resize\n    const textareaRef = useRef<HTMLTextAreaElement | null>(null);\n\n    // Base classes applied to all variants\n    const baseClasses = [\n      'transition-colors duration-200 ease-in-out',\n      'focus:outline-none',\n      'disabled:opacity-50 disabled:pointer-events-none disabled:cursor-not-allowed',\n      'placeholder:text-gray-400 dark:placeholder:text-gray-500',\n      'w-full',\n    ].join(' ');\n\n    // Size-specific classes\n    const sizeClasses = {\n      sm: 'px-3 py-1.5 text-sm',\n      md: 'px-4 py-2 text-base',\n      lg: 'px-4 py-3 text-lg',\n    };\n\n    // Variant-specific classes\n    const variantClasses = {\n      default: [\n        'rounded-lg border',\n        'bg-white dark:bg-gray-800',\n        'text-gray-900 dark:text-gray-100',\n        // State-specific border colors\n        error\n          ? 'border-red-500 dark:border-red-400'\n          : success\n            ? 'border-green-500 dark:border-green-400'\n            : 'border-gray-300 dark:border-gray-600',\n        // Focus states\n        error\n          ? 'focus:ring-2 focus:ring-red-500 focus:border-red-500'\n          : success\n            ? 'focus:ring-2 focus:ring-green-500 focus:border-green-500'\n            : 'focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:focus:border-blue-400',\n      ].join(' '),\n\n      message: [\n        'rounded-full border',\n        'bg-gray-50 dark:bg-gray-700',\n        'text-gray-900 dark:text-gray-100',\n        // State-specific border colors\n        error\n          ? 'border-red-500 dark:border-red-400'\n          : success\n            ? 'border-green-500 dark:border-green-400'\n            : 'border-gray-300 dark:border-gray-600',\n        // Focus states\n        error\n          ? 'focus:ring-2 focus:ring-red-500 focus:border-red-500'\n          : success\n            ? 'focus:ring-2 focus:ring-green-500 focus:border-green-500'\n            : 'focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:focus:border-blue-400',\n      ].join(' '),\n    };\n\n    // Additional classes for multiline textarea\n    const multilineClasses = multiline\n      ? [\n          'resize-none',\n          'overflow-y-hidden', // Hide scrollbar by default\n          variant === 'message' ? 'rounded-full' : 'rounded-lg', // Use rounded-full for message variant to match single-line input\n        ].join(' ')\n      : '';\n\n    // Determine aria-invalid based on error state\n    const computedAriaInvalid = ariaInvalid ?? (error ? 'true' : undefined);\n\n    // Auto-resize textarea function\n    const autoResizeTextarea = useCallback(() => {\n      const textarea = textareaRef.current;\n      if (!textarea) return;\n\n      // Reset height to auto to get the correct scrollHeight\n      textarea.style.height = 'auto';\n\n      // Set the height to scrollHeight, but cap it at maxHeight\n      const newHeight = Math.min(textarea.scrollHeight, Number.parseInt(maxHeight));\n      textarea.style.height = `${String(newHeight)}px`;\n\n      // Check if content exceeds max height and show/hide scrollbar accordingly\n      if (textarea.scrollHeight > Number.parseInt(maxHeight)) {\n        // Show scrollbar only when content exceeds max height\n        textarea.classList.remove('overflow-y-hidden');\n        textarea.classList.add('overflow-y-auto');\n\n        // If we're at max height, ensure we're scrolled to the bottom when typing\n        if (textarea.value === value && typeof value === 'string') {\n          textarea.scrollTop = textarea.scrollHeight;\n        }\n      } else {\n        // Hide scrollbar when content fits within max height\n        textarea.classList.remove('overflow-y-auto');\n        textarea.classList.add('overflow-y-hidden');\n      }\n    }, [maxHeight, value]);\n\n    // Auto-resize on value change\n    useEffect(() => {\n      if (multiline) {\n        autoResizeTextarea();\n      }\n    }, [value, multiline, autoResizeTextarea]);\n\n    // Render a textarea for multiline input\n    const renderTextarea = () => {\n      return (\n        <textarea\n          ref={(element) => {\n            // Set the forwarded ref\n            if (typeof ref === 'function') {\n              ref(element);\n            } else if (ref) {\n              (ref as React.RefObject<HTMLTextAreaElement | null>).current = element;\n            }\n            // Set our local ref\n            textareaRef.current = element;\n          }}\n          className={clsx(\n            baseClasses,\n            variantClasses[variant === 'message' ? 'default' : variant],\n            sizeClasses[size],\n            multilineClasses,\n            prefix && 'pl-10',\n            suffix && 'pr-10',\n            className\n          )}\n          style={{ maxHeight }}\n          disabled={disabled}\n          aria-invalid={computedAriaInvalid}\n          rows={1}\n          value={value}\n          onChange={(e) => {\n            if (onChange) {\n              onChange(e);\n            }\n            // Auto-resize after onChange is called\n            setTimeout(autoResizeTextarea, 0);\n          }}\n          {...props}\n        />\n      );\n    };\n\n    // Render a standard input for single-line input\n    const renderInput = () => {\n      return (\n        <input\n          ref={ref as React.Ref<HTMLInputElement>}\n          className={clsx(\n            baseClasses,\n            variantClasses[variant],\n            sizeClasses[size],\n            prefix ? 'pl-10' : '',\n            suffix ? 'pr-10' : '',\n            className\n          )}\n          disabled={disabled}\n          aria-invalid={computedAriaInvalid}\n          value={value}\n          onChange={onChange}\n          {...props}\n        />\n      );\n    };\n\n    // If we have prefix or suffix, wrap in a container\n    if (prefix || suffix) {\n      return (\n        <div\n          className={`relative flex items-center ${variant === 'default' ? 'w-full' : 'flex-1'}`}\n        >\n          {prefix && (\n            <div className=\"absolute left-3 z-10 flex items-center pointer-events-none\">\n              {prefix}\n            </div>\n          )}\n          {multiline ? renderTextarea() : renderInput()}\n          {suffix && <div className=\"absolute right-3 z-10 flex items-center\">{suffix}</div>}\n        </div>\n      );\n    }\n\n    return multiline ? renderTextarea() : renderInput();\n  }\n);\n\nTextInput.displayName = 'TextInput';\n","import { clsx } from 'clsx';\nimport React from 'react';\n\n/**\n * Position options for tooltip placement\n */\ntype TooltipPosition = 'above' | 'below';\n\n/**\n * Props for the Tooltip component\n */\nexport interface TooltipProps extends React.HTMLAttributes<HTMLDivElement> {\n  /**\n   * Position of the tooltip relative to its trigger element\n   * - 'above': Tooltip appears above the trigger\n   * - 'below': Tooltip appears below the trigger\n   */\n  position: TooltipPosition;\n\n  /**\n   * Tooltip content - can be text or React elements\n   */\n  children: React.ReactNode;\n\n  /**\n   * Additional CSS classes to apply to the tooltip\n   */\n  className?: string;\n\n  /**\n   * Maximum width constraint for the tooltip\n   * @default 'max-w-xs' (20rem)\n   */\n  maxWidth?: string;\n\n  /**\n   * Text wrapping behavior for tooltip content\n   * @default 'wrap' - allows text to wrap within the tooltip\n   */\n  wrap?: 'wrap' | 'nowrap' | 'truncate';\n\n  /**\n   * Background color variant for the tooltip\n   * @default 'dark' - dark background with light text\n   */\n  variant?: 'dark' | 'light';\n\n  /**\n   * Size of the tooltip and arrow\n   * @default 'md'\n   */\n  size?: 'sm' | 'md' | 'lg';\n\n  /**\n   * Whether to show the pointing arrow\n   * @default true\n   */\n  showArrow?: boolean;\n\n  /**\n   * Spacing from the trigger element\n   * Set to 'none' when using fixed positioning with custom styles\n   * @default 'default' - adds standard spacing (8px)\n   */\n  spacing?: 'none' | 'sm' | 'default' | 'lg';\n\n  /**\n   * Z-index for tooltip layering\n   * @default 'z-50'\n   */\n  zIndex?: string;\n}\n\n/**\n * Tooltip component renders a complete tooltip with surface and optional arrow\n *\n * Features:\n * - Automatic positioning above or below trigger element\n * - Dark and light theme variants with matching arrows\n * - Multiple sizes with coordinated surface and arrow sizing\n * - Responsive sizing with max-width constraints\n * - Optional arrow with perfect color matching\n *\n * @example\n * // Basic usage\n * <Tooltip position=\"above\">\n *   This is a tooltip\n * </Tooltip>\n *\n * @example\n * // Light variant with large size\n * <Tooltip\n *   position=\"below\"\n *   variant=\"light\"\n *   size=\"lg\"\n *   maxWidth=\"max-w-sm\"\n * >\n *   Custom tooltip content\n * </Tooltip>\n *\n * @example\n * // Without arrow\n * <Tooltip position=\"above\" showArrow={false}>\n *   Simple tooltip without arrow\n * </Tooltip>\n */\nexport const Tooltip = ({\n  position,\n  children,\n  className,\n  maxWidth = 'max-w-xs',\n  wrap = 'wrap',\n  variant = 'dark',\n  size = 'md',\n  showArrow = true,\n  zIndex = 'z-50',\n  spacing = 'default',\n  role = 'tooltip',\n  'aria-hidden': ariaHidden,\n  ...rest\n}: TooltipProps) => {\n  // Size configurations\n  const sizeClasses = {\n    sm: {\n      surface: 'px-2 py-1 text-xs',\n      arrow: {\n        border: 'border-l-2 border-r-2',\n        directional: { above: 'border-t-2', below: 'border-b-2' },\n      },\n    },\n    md: {\n      surface: 'px-3 py-2 text-sm',\n      arrow: {\n        border: 'border-l-4 border-r-4',\n        directional: { above: 'border-t-4', below: 'border-b-4' },\n      },\n    },\n    lg: {\n      surface: 'px-4 py-3 text-base',\n      arrow: {\n        border: 'border-l-6 border-r-6',\n        directional: { above: 'border-t-6', below: 'border-b-6' },\n      },\n    },\n  };\n\n  // Variant-specific styling\n  const variantClasses = {\n    dark: {\n      surface: 'bg-gray-900 dark:bg-gray-700 text-white',\n      arrow: {\n        above: 'border-t-gray-900 dark:border-t-gray-700',\n        below: 'border-b-gray-900 dark:border-b-gray-700',\n      },\n    },\n    light: {\n      surface:\n        'bg-white dark:bg-gray-100 text-gray-900 dark:text-gray-800 border border-gray-200 dark:border-gray-300',\n      arrow: {\n        above: 'border-t-white dark:border-t-gray-100',\n        below: 'border-b-white dark:border-b-gray-100',\n      },\n    },\n  };\n\n  // Position-specific classes\n  const positionClasses = {\n    above: 'bottom-full mb-2',\n    below: 'top-full mt-2',\n  };\n\n  const wrapClasses = {\n    wrap: 'whitespace-normal',\n    nowrap: 'whitespace-nowrap',\n    truncate: 'whitespace-nowrap overflow-hidden text-ellipsis',\n  };\n\n  const spacingClasses = {\n    none: { above: 'bottom-full', below: 'top-full' },\n    sm: { above: 'bottom-full mb-1', below: 'top-full mt-1' },\n    default: { above: 'bottom-full mb-2', below: 'top-full mt-2' },\n    lg: { above: 'bottom-full mb-4', below: 'top-full mt-4' },\n  };\n\n  return (\n    <div\n      className={clsx(\n        // Base positioning and layout - only apply if not using fixed positioning\n        !className?.includes('fixed') && 'absolute left-1/2 transform -translate-x-1/2',\n        // Styling and appearance\n        'rounded-lg shadow-lg',\n        // Responsive sizing\n        maxWidth,\n        // Layering\n        zIndex,\n        // Position-specific classes with spacing - only apply if not using fixed positioning\n        !className?.includes('fixed') && spacingClasses[spacing][position],\n        // Size-specific padding and text\n        sizeClasses[size].surface,\n        // Text wrapping behavior\n        wrapClasses[wrap],\n        // Variant styling\n        variantClasses[variant].surface,\n        // Position-specific classes - only apply if not using fixed positioning\n        !className?.includes('fixed') && positionClasses[position],\n        // Animation and transitions\n        'transition-opacity duration-200 ease-in-out',\n        // Custom classes\n        className\n      )}\n      role={role}\n      aria-hidden={ariaHidden}\n      {...rest}\n    >\n      {children}\n\n      {/* Arrow */}\n      {showArrow && (\n        <div\n          className={clsx(\n            // Base arrow positioning - always centered on tooltip\n            'absolute left-1/2 transform -translate-x-1/2 w-0 h-0',\n            // Position relative to tooltip surface\n            position === 'above' ? 'top-full' : 'bottom-full',\n            // Transparent side borders for triangle shape\n            'border-transparent',\n            // Size-specific border widths\n            sizeClasses[size].arrow.border,\n            // Direction-specific border\n            sizeClasses[size].arrow.directional[position],\n            // Background color to match tooltip surface\n            variantClasses[variant].arrow[position]\n          )}\n          aria-hidden=\"true\"\n        />\n      )}\n    </div>\n  );\n};\n","import React, { useEffect } from 'react';\n\nimport { Button } from '../atoms/button.tsx';\nimport { Icon } from '../atoms/icon.tsx';\n\n/**\n * Props for the ConfirmDialog component\n */\nexport interface ConfirmDialogProps {\n  /**\n   * Whether the dialog is currently open and visible to the user.\n   * Controls the dialog's visibility state.\n   */\n  isOpen: boolean;\n\n  /**\n   * Callback function triggered when the dialog should be closed.\n   * Called on backdrop click, escape key press, or cancel button click.\n   */\n  onClose: () => void;\n\n  /**\n   * Callback function triggered when the user confirms the action.\n   * The dialog will automatically close after this callback is executed.\n   */\n  onConfirm: () => void;\n\n  /**\n   * Title displayed in the dialog header.\n   * Should be concise and clearly indicate the action being confirmed.\n   */\n  title: string;\n\n  /**\n   * Main message content explaining the action and its consequences.\n   * Can include warnings or additional context for the user.\n   */\n  message: string;\n\n  /**\n   * Custom text for the confirm button.\n   * Should be action-oriented (e.g., \"Delete\", \"Save\", \"Continue\").\n   * @default \"Confirm\"\n   */\n  confirmText?: string;\n\n  /**\n   * Custom text for the cancel button.\n   * Should indicate the safe/default action.\n   * @default \"Cancel\"\n   */\n  cancelText?: string;\n\n  /**\n   * Visual variant of the confirm button indicating action severity.\n   * - `primary`: Standard confirmation (blue)\n   * - `secondary`: Neutral action (gray)\n   * - `danger`: Destructive action (red)\n   * @default \"danger\"\n   */\n  confirmVariant?: 'primary' | 'secondary' | 'danger';\n\n  /**\n   * Optional icon to display in the dialog header.\n   * Typically used for warning, error, or information icons.\n   * Icon will be colored red for visual emphasis.\n   */\n  icon?: React.ReactNode;\n}\n\n/**\n * ConfirmDialog component displays a modal confirmation dialog\n *\n * Features:\n * - Consistent modal styling with other components\n * - Support for light/dark theming\n * - Customizable buttons and text\n * - Escape key handling to close\n * - Backdrop click to close\n * - Basic accessibility attributes\n * - Focus management\n *\n * @example\n * // Basic deletion confirmation\n * <ConfirmDialog\n *   isOpen={showDeleteDialog}\n *   onClose={() => setShowDeleteDialog(false)}\n *   onConfirm={handleDeleteMessage}\n *   title=\"Delete Message\"\n *   message=\"Are you sure you want to delete this message? This action cannot be undone.\"\n * />\n *\n * @example\n * // Custom styling with icon\n * <ConfirmDialog\n *   isOpen={showLogoutDialog}\n *   onClose={() => setShowLogoutDialog(false)}\n *   onConfirm={handleLogout}\n *   title=\"Sign Out\"\n *   message=\"Are you sure you want to sign out of your account?\"\n *   confirmText=\"Sign Out\"\n *   cancelText=\"Stay Signed In\"\n *   confirmVariant=\"primary\"\n *   icon={<Icon name=\"logout\" />}\n * />\n *\n * @example\n * // Warning dialog\n * <ConfirmDialog\n *   isOpen={showWarning}\n *   onClose={() => setShowWarning(false)}\n *   onConfirm={handleProceed}\n *   title=\"Unsaved Changes\"\n *   message=\"You have unsaved changes. Are you sure you want to leave this page?\"\n *   confirmText=\"Leave Page\"\n *   cancelText=\"Stay Here\"\n *   confirmVariant=\"danger\"\n *   icon={<Icon name=\"warning\" />}\n * />\n * @see {@link Button} - For button styling variants\n * @see {@link Icon} - For available icon options\n */\nexport const ConfirmDialog = ({\n  isOpen,\n  onClose,\n  onConfirm,\n  title,\n  message,\n  confirmText = 'Confirm',\n  cancelText = 'Cancel',\n  confirmVariant = 'danger',\n  icon,\n}: ConfirmDialogProps) => {\n  // Handle escape key press to close the dialog\n  useEffect(() => {\n    const handleEscapeKey = (e: KeyboardEvent) => {\n      if (isOpen && e.key === 'Escape') {\n        onClose();\n      }\n    };\n    document.addEventListener('keydown', handleEscapeKey);\n    return () => {\n      document.removeEventListener('keydown', handleEscapeKey);\n    };\n  }, [isOpen, onClose]);\n\n  // Handle confirm action\n  const handleConfirm = () => {\n    onConfirm();\n    onClose();\n  };\n\n  if (!isOpen) return;\n\n  return (\n    <>\n      {/* Backdrop */}\n      <div\n        className=\"fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4\"\n        onClick={onClose}\n      >\n        {/* Dialog */}\n        <div\n          className=\"bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md\"\n          onClick={(e) => {\n            e.stopPropagation();\n          }}\n          role=\"dialog\"\n          aria-modal=\"true\"\n          aria-labelledby=\"confirm-dialog-title\"\n          aria-describedby=\"confirm-dialog-message\"\n        >\n          {/* Header */}\n          <div className=\"flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700\">\n            <div className=\"flex items-center gap-3\">\n              {icon && (\n                <div className=\"flex-shrink-0 text-red-500 dark:text-red-400\" aria-hidden=\"true\">\n                  {icon}\n                </div>\n              )}\n              <h2\n                id=\"confirm-dialog-title\"\n                className=\"text-xl font-semibold text-gray-900 dark:text-gray-100\"\n              >\n                {title}\n              </h2>\n            </div>\n            <button\n              onClick={onClose}\n              className=\"text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors\"\n              aria-label=\"Close dialog\"\n            >\n              <Icon name=\"close\" size=\"md\" />\n            </button>\n          </div>\n\n          {/* Content */}\n          <div className=\"p-6\">\n            <p id=\"confirm-dialog-message\" className=\"text-gray-700 dark:text-gray-300 mb-6\">\n              {message}\n            </p>\n\n            {/* Actions */}\n            <div className=\"flex items-center justify-end gap-3\">\n              <Button variant=\"secondary\" onClick={onClose}>\n                {cancelText}\n              </Button>\n              <Button variant={confirmVariant} onClick={handleConfirm} autoFocus>\n                {confirmText}\n              </Button>\n            </div>\n          </div>\n        </div>\n      </div>\n    </>\n  );\n};\n","import React, { useCallback, useEffect, useMemo, useState } from 'react';\n\nconst DEFAULT_EMOJI_STORAGE_KEY = 'ably-chat-recent-emojis';\n\n/**\n * Props for the EmojiPicker component\n */\nexport interface EmojiPickerProps {\n  /**\n   * Callback function triggered when the picker should be closed.\n   * Called on backdrop click, escape key press, or programmatic close.\n   * Should update the parent component's state to hide the picker.\n   *\n   * This callback does not automatically close the picker after emoji selection.\n   * Use onEmojiSelect to handle post-selection behavior.\n   */\n  onClose: () => void;\n\n  /**\n   * Callback function triggered when an emoji is selected.\n   * Receives the selected emoji character as a string parameter.\n   * The selected emoji is automatically added to recent emojis list.\n   *\n   * @param emoji - The selected emoji character (e.g., \"😀\", \"❤️\")\n   *\n   * - Recent emojis are persisted to localStorage\n   * - Maximum of 10 recent emojis are maintained\n   *\n   * @example\n   * ```tsx\n   * onEmojiSelect={(emoji) => {\n   *   addEmojiToInput(emoji);\n   *   setPickerOpen(false); // Close picker after selection\n   * }}\n   * ```\n   */\n  onEmojiSelect: (emoji: string) => void;\n\n  /**\n   * Position coordinates for rendering the picker in viewport coordinates.\n   * Should account for picker dimensions (240px × 320px) to prevent overflow.\n   *\n   * - Consider viewport boundaries to prevent edge overflow\n   * - Add margins for visual spacing from trigger element\n   *\n   * @example\n   * ```tsx\n   * // Position below button with spacing\n   * const rect = buttonRef.current.getBoundingClientRect();\n   * const position = {\n   *   top: rect.bottom + 8,\n   *   left: rect.left\n   * };\n   *\n   * // Position above with overflow protection\n   * const position = {\n   *   top: Math.max(10, rect.top - 330),\n   *   left: Math.min(rect.left, window.innerWidth - 250)\n   * };\n   * ```\n   */\n  position: { top: number; left: number };\n\n  /**\n   * Number of columns to display in the emoji grid.\n   * Affects both main emoji grid and recent emojis section.\n   * Must be a positive integer; decimal values may cause layout issues.\n   *\n   * @default 4\n   *\n   * - Higher values create wider, shorter grids\n   * - Lower values create narrower, taller grids\n   * - Consider emoji button size (32px) when choosing columns\n   * - Recommended range: 3-6 columns for optimal usability\n   */\n  columns?: number;\n\n  /**\n   * Optional custom list of emojis to display instead of the default set.\n   * Useful for creating themed emoji pickers or limiting choices.\n   *\n   * Custom List Behavior:\n   * - Completely replaces the default emoji set\n   * - Recent emojis will only show emojis from this custom list\n   * - Order in array determines display order in picker\n   *\n   * @example\n   * ```tsx\n   * // Reaction-only emojis\n   * emojiList={['👍', '👎', '❤️', '😂', '😮', '😢']}\n   *\n   * // Status emojis\n   * emojiList={['🟢', '🟡', '🔴', '⚫', '🔵']}\n   *\n   * // Celebration emojis\n   * emojiList={['🎉', '🥳', '🎊', '🍾', '🎈', '🎁']}\n   * ```\n   */\n  emojiList?: string[];\n}\n\nconst emojis = [\n  '👍',\n  '❤️',\n  '😊',\n  '😂',\n  '😱',\n  '😢',\n  '🏃',\n  '💯',\n  '🔥',\n  '👏',\n  '☀️',\n  '🎉',\n  '🌈',\n  '🙌',\n  '💡',\n  '🎶',\n  '😎',\n  '🤔',\n  '🧠',\n  '🍕',\n  '🌟',\n  '🚀',\n  '🐶',\n  '🐱',\n  '🌍',\n  '📚',\n  '🎯',\n  '🥳',\n  '🤖',\n  '🎨',\n  '🧘',\n  '🏆',\n  '💥',\n  '💖',\n  '😇',\n  '😜',\n  '🌸',\n  '💬',\n  '📸',\n  '🛠️',\n  '⏰',\n  '🧩',\n  '🗺️',\n];\n\n/**\n * EmojiPicker component displays a grid of emoji characters for selection\n *\n * Features:\n * - Positioned at specified coordinates\n * - Backdrop for easy dismissal\n * - Grid layout of emojis with customizable columns\n * - Recent emojis section showing last 10 used emojis\n * - Scrollable emoji list\n * - Keyboard navigation (Escape to close)\n * - Support for custom emoji lists\n * - Accessible emoji buttons\n * - Persistent recent emojis via localStorage\n * - Optimized rendering with memoization\n *\n * @example\n * // Basic emoji picker triggered by button\n * const [pickerOpen, setPickerOpen] = useState(false);\n * const [pickerPosition, setPickerPosition] = useState({ top: 0, left: 0 });\n *\n * const handleShowPicker = (event: React.MouseEvent) => {\n *   const rect = event.currentTarget.getBoundingClientRect();\n *   setPickerPosition({\n *     top: rect.bottom + 8,\n *     left: rect.left\n *   });\n *   setPickerOpen(true);\n * };\n *\n * return (\n *   <>\n *     <button onClick={handleShowPicker}>😀 Add Emoji</button>\n *     <EmojiPicker\n *       isOpen={pickerOpen}\n *       position={pickerPosition}\n *       onClose={() => setPickerOpen(false)}\n *       onEmojiSelect={(emoji) => {\n *         addEmojiToMessage(emoji);\n *         setPickerOpen(false);\n *       }}\n *     />\n *   </>\n * );\n *\n * @example\n * // Custom emoji list with reaction-specific emojis\n * const reactionEmojis = ['👍', '❤️', '😂', '😮', '😢', '😡'];\n *\n * {showReactionPicker && (<EmojiPicker\n *   position={reactionPosition}\n *   emojiList={reactionEmojis}\n *   columns={3}\n *   onClose={() => setShowReactionPicker(false)}\n *   onEmojiSelect={handleReaction}\n * />)}\n *\n * @example\n * // Chat message emoji picker with positioning\n * const handleEmojiButton = (event: React.MouseEvent, messageId: string) => {\n *   event.stopPropagation();\n *   const rect = event.currentTarget.getBoundingClientRect();\n *\n *   // Position above the button with some spacing\n *   setPickerPosition({\n *     top: rect.top - 330, // picker height + margin\n *     left: Math.max(10, rect.left - 100) // prevent edge overflow\n *   });\n *   setActiveMessageId(messageId);\n *   setPickerOpen(true);\n * };\n */\nexport const EmojiPicker = ({\n  onClose,\n  onEmojiSelect,\n  position,\n  columns = 4,\n  emojiList,\n}: EmojiPickerProps) => {\n  const [recentEmojis, setRecentEmojis] = useState<string[]>([]);\n\n  // Load recent emojis from localStorage on mount\n  useEffect(() => {\n    try {\n      const stored = localStorage.getItem(DEFAULT_EMOJI_STORAGE_KEY);\n      if (stored) {\n        const parsed = JSON.parse(stored) as string[];\n        setRecentEmojis(parsed);\n      }\n    } catch (error) {\n      console.error('Failed to load recent emojis:', error);\n    }\n  }, []);\n\n  // Add emoji to recent list when selected\n  const handleEmojiSelect = useCallback(\n    (emoji: string) => {\n      setRecentEmojis((prev) => {\n        // Remove emoji if it already exists in the list\n        const filtered = prev.filter((e) => e !== emoji);\n        // Add emoji to the beginning of the list and limit to 10\n        const updated = [emoji, ...filtered].slice(0, 10);\n\n        // Save to localStorage\n        try {\n          localStorage.setItem(DEFAULT_EMOJI_STORAGE_KEY, JSON.stringify(updated));\n        } catch (error) {\n          console.error('Failed to save recent emojis:', error);\n        }\n\n        return updated;\n      });\n\n      onEmojiSelect(emoji);\n    },\n    [onEmojiSelect]\n  );\n\n  // Handle Escape key to close the picker\n  useEffect(() => {\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.key === 'Escape') {\n        onClose();\n      }\n    };\n\n    document.addEventListener('keydown', handleKeyDown);\n    return () => {\n      document.removeEventListener('keydown', handleKeyDown);\n    };\n  }, [onClose]);\n\n  // Use provided emoji list or default\n  const displayEmojis = useMemo(() => emojiList || emojis, [emojiList]);\n\n  // Memoize emoji buttons to optimize rendering\n  const emojiButtons = useMemo(() => {\n    return displayEmojis.map((emoji) => (\n      <button\n        key={emoji}\n        className=\"w-8 h-8 flex items-center justify-center text-lg hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors\"\n        onClick={() => {\n          handleEmojiSelect(emoji);\n        }}\n        aria-label={`Emoji ${emoji}`}\n        title={emoji}\n      >\n        {emoji}\n      </button>\n    ));\n  }, [displayEmojis, handleEmojiSelect]);\n\n  // Memoize recent emoji buttons\n  const recentEmojiButtons = useMemo(() => {\n    if (recentEmojis.length === 0) return;\n\n    return (\n      <div className=\"mb-3 pb-3 border-b border-gray-200 dark:border-gray-700\">\n        <p className=\"text-xs text-gray-500 dark:text-gray-400 mb-2\">Recent</p>\n        <div\n          className=\"grid gap-2\"\n          style={{ gridTemplateColumns: `repeat(${String(columns)}, minmax(0, 1fr))` }}\n        >\n          {recentEmojis.map((emoji) => (\n            <button\n              key={`recent-${emoji}`}\n              className=\"w-8 h-8 flex items-center justify-center text-lg hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors\"\n              onClick={() => {\n                handleEmojiSelect(emoji);\n              }}\n              aria-label={`Emoji ${emoji}`}\n              title={emoji}\n            >\n              {emoji}\n            </button>\n          ))}\n        </div>\n      </div>\n    );\n  }, [recentEmojis, columns, handleEmojiSelect]);\n\n  return (\n    <>\n      {/* Backdrop */}\n      <div className=\"fixed inset-0 z-40\" onClick={onClose} aria-hidden=\"true\" />\n\n      {/* Emoji Picker */}\n      <div\n        className=\"fixed z-50 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg overflow-hidden\"\n        style={{\n          top: position.top,\n          left: position.left,\n          width: 'min(240px, calc(100vw - 40px))',\n          height: 'min(320px, calc(100vh - 40px))',\n          minHeight: '120px', // Ensures at least 2 rows of emojis (8px padding + 2 * (32px emoji + 8px gap))\n        }}\n        role=\"dialog\"\n        aria-label=\"Emoji picker\"\n      >\n        {/* Fixed container with proper scrolling */}\n        <div className=\"h-full flex flex-col\">\n          {/* Content area with scrolling */}\n          <div className=\"flex-1 overflow-y-auto p-3\">\n            {recentEmojiButtons}\n\n            <div\n              className=\"grid gap-2\"\n              style={{ gridTemplateColumns: `repeat(${String(columns)}, minmax(0, 1fr))` }}\n            >\n              {emojiButtons}\n            </div>\n          </div>\n        </div>\n      </div>\n    </>\n  );\n};\n","import { createContext } from 'react';\n\n/**\n * Interface defining chat feature settings that control UI behavior\n */\nexport interface ChatSettings {\n  /** Whether users can update their own messages after sending */\n  allowMessageUpdatesOwn: boolean;\n  /** Whether users can update any message (not just their own) */\n  allowMessageUpdatesAny: boolean;\n  /** Whether users can delete their own messages */\n  allowMessageDeletesOwn: boolean;\n  /** Whether users can delete any message (not just their own) */\n  allowMessageDeletesAny: boolean;\n  /** Whether users can add reactions to messages */\n  allowMessageReactions: boolean;\n}\n\n/**\n * Context interface providing access to chat settings globally and per room.\n *\n */\nexport interface ChatSettingsContextType {\n  /** Global default settings applied to all rooms */\n  globalSettings: ChatSettings;\n  /** Room-specific setting overrides */\n  roomSettings: Record<string, Partial<ChatSettings>>;\n  /**\n   * Get effective settings for a room by merging global and room-specific settings.\n   * Room settings take precedence over global settings.\n   *\n   * @param roomName - Optional room name. If not provided, returns global settings\n   * @returns Merged settings for the specified room or global settings\n   */\n  getEffectiveSettings: (roomName?: string) => ChatSettings;\n}\n\n/**\n * React context for chat settings management.\n *\n * @internal\n */\nexport const ChatSettingsContext = createContext<ChatSettingsContextType | undefined>(undefined);\n","import { useContext } from 'react';\n\nimport { ChatSettingsContext, ChatSettingsContextType } from '../context/chat-settings-context.tsx';\n\n/**\n * Hook to access the chat settings context.\n *\n * Provides access to global settings, room settings, and the function\n * to get effective settings for specific rooms.\n *\n * @example\n * ```tsx\n * const { globalSettings, getEffectiveSettings } = useChatSettings();\n * const roomSettings = getEffectiveSettings('general');\n * ```\n *\n * @returns The chat settings context value\n * @throws Error if used outside of ChatSettingsProvider\n *\n * @public\n */\nexport const useChatSettings = (): ChatSettingsContextType => {\n  const context = useContext(ChatSettingsContext);\n  if (!context) {\n    throw new Error('useChatSettings must be used within a ChatSettingsProvider');\n  }\n  return context;\n};\n","import { useRoom } from '@ably/chat/react';\nimport React from 'react';\n\nimport { useChatSettings } from '../../hooks/use-chat-settings.tsx';\nimport { Button } from '../atoms/button.tsx';\nimport { Icon } from '../atoms/icon.tsx';\n\n/**\n * Props for the MessageActions component\n */\nexport interface MessageActionsProps {\n  /**\n   * Callback function triggered when the reaction button is clicked.\n   * Typically opens an emoji picker or emoji wheel for users to select reactions.\n   * Should handle the display of reaction UI components.\n   *\n   *\n   * @example\n   * ```tsx\n   * onReactionButtonClicked={() => {\n   *   setEmojiPickerPosition(getMessagePosition());\n   *   setShowEmojiPicker(true);\n   * }}\n   * ```\n   */\n  onReactionButtonClicked?: () => void;\n\n  /**\n   * Callback function triggered when the edit button is clicked.\n   * Should initiate edit mode for the message, typically replacing the message\n   * content with an editable input field or editor component.\n   * Displayed when:\n   * - The message is owned by the current user and allowMessageUpdatesOwn is true, or allowMessageUpdatesAny is true\n   *\n   * @example\n   * ```tsx\n   * onEditButtonClicked={() => {\n   *   setEditingMessageId(message.id);\n   *   setEditText(message.text);\n   *   setIsEditing(true);\n   * }}\n   * ```\n   */\n  onEditButtonClicked?: () => void;\n\n  /**\n   * Callback function triggered when the delete button is clicked.\n   * Should handle message deletion, typically with confirmation dialog.\n   * Displayed when:\n   * - The message is owned by the current user and allowMessageDeletesOwn is true, or allowMessageDeletesAny is true\n   *\n   * @example\n   * ```tsx\n   * onDeleteButtonClicked={() => {\n   *   setDeleteTarget(message.id);\n   *   setShowDeleteConfirm(true);\n   * }}\n   * ```\n   */\n  onDeleteButtonClicked?: () => void;\n\n  /**\n   * Whether the message belongs to the current user.\n   * Used in combination with chat settings to determine if edit and delete buttons are shown.\n   *\n   * @example\n   * ```tsx\n   * // Basic ownership check\n   * isOwn={message.senderId === currentUser.id}\n   * ```\n   */\n  isOwn: boolean;\n}\n\n/**\n * MessageActions component displays a toolbar of action buttons for a chat message\n *\n * Features:\n * - Reaction button for adding emoji reactions to messages\n * - Edit and delete buttons for the message owner\n * - Positioned relative to the message bubble\n * - Accessible toolbar with proper ARIA attributes\n * - Responsive to theme changes (light/dark)\n * - Smooth hover transitions and visual feedback\n *\n * @example\n * // Basic usage in a chat message component\n * const [actionsVisible, setActionsVisible] = useState(false);\n * const [showEmojiPicker, setShowEmojiPicker] = useState(false);\n *\n * return (\n *   <div\n *     className=\"message-bubble\"\n *     onMouseEnter={() => setActionsVisible(true)}\n *     onMouseLeave={() => setActionsVisible(false)}\n *   >\n *     <div className=\"message-content\">{message.text}</div>\n *\n *     {actionsVisible && (<MessageActions\n *       isOwn={message.senderId === currentUser.id}\n *       onReaction={() => setShowEmojiPicker(true)}\n *       onEdit={() => handleEditMessage(message.id)}\n *       onDelete={() => handleDeleteMessage(message.id)}\n *     />\n *   </div>)}\n * );\n *\n *\n */\nexport const MessageActions = ({\n  onReactionButtonClicked,\n  onEditButtonClicked,\n  onDeleteButtonClicked,\n  isOwn,\n}: MessageActionsProps) => {\n  // Get the current room name\n  const { roomName } = useRoom();\n\n  // Get chat settings for the current room\n  const { getEffectiveSettings } = useChatSettings();\n  const settings = getEffectiveSettings(roomName);\n\n  const {\n    allowMessageUpdatesOwn,\n    allowMessageUpdatesAny,\n    allowMessageDeletesOwn,\n    allowMessageDeletesAny,\n    allowMessageReactions,\n  } = settings;\n\n  // Check if there are any actions to display based on settings and permissions\n  const hasReactionAction = allowMessageReactions && onReactionButtonClicked !== undefined;\n\n  // Can edit if:\n  // - User owns the message AND can edit own messages, OR\n  // - User can edit any message\n  const canEdit = (isOwn && allowMessageUpdatesOwn) || allowMessageUpdatesAny;\n  const hasEditAction = canEdit && onEditButtonClicked !== undefined;\n\n  // Can delete if:\n  // - User owns the message AND can delete own messages, OR\n  // - User can delete any message\n  const canDelete = (isOwn && allowMessageDeletesOwn) || allowMessageDeletesAny;\n  const hasDeleteAction = canDelete && onDeleteButtonClicked !== undefined;\n\n  // If no actions are available, don't render anything\n  if (!hasReactionAction && !hasEditAction && !hasDeleteAction) {\n    return;\n  }\n\n  return (\n    <div\n      className=\"absolute -top-9 right-0 z-10 flex items-center gap-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-md p-1\"\n      role=\"toolbar\"\n      aria-label=\"Message actions\"\n    >\n      {hasReactionAction && (\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          className=\"text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200\"\n          onClick={onReactionButtonClicked}\n          aria-label=\"Add reaction\"\n        >\n          <Icon name=\"emoji\" size=\"md\" aria-hidden={true} />\n        </Button>\n      )}\n\n      {hasEditAction && (\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          className=\"text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200\"\n          onClick={onEditButtonClicked}\n          aria-label=\"Edit message\"\n        >\n          <Icon name=\"edit\" size=\"sm\" aria-hidden={true} />\n        </Button>\n      )}\n\n      {hasDeleteAction && (\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          className=\"text-gray-600 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-400\"\n          onClick={onDeleteButtonClicked}\n          aria-label=\"Delete message\"\n        >\n          <Icon name=\"delete\" size=\"sm\" aria-hidden={true} />\n        </Button>\n      )}\n    </div>\n  );\n};\n","import { Message } from '@ably/chat';\nimport React from 'react';\n\n/**\n * Props for the MessageReactions component\n */\nexport interface MessageReactionsProps {\n  /**\n   * The Chat Message object containing reaction data.\n   * Access reactions via `message.reactions.distinct` which provides a map of emoji to reaction details.\n   * Only renders when the message has existing reactions.\n   */\n  message: Message;\n\n  /**\n   * Optional callback function triggered when a reaction button is clicked.\n   * Receives the emoji character as a parameter for handling reaction add/remove logic.\n   * Should implement toggle behavior - add reaction if user hasn't reacted, remove if they have.\n   *\n   * @param emoji - The emoji character that was clicked\n   *\n   * @example\n   * ```tsx\n   * const handleReactionClick = (emoji: string) => {\n   *   const hasUserReacted = message.reactions?.distinct[emoji]?.clientIds.includes(currentClientId);\n   *   if (hasUserReacted) {\n   *     onReactionRemove(message, emoji);\n   *   } else {\n   *     onReactionAdd(message, emoji);\n   *   }\n   * };\n   *\n   * <MessageReactions\n   *   message={message}\n   *   onReactionClick={handleReactionClick}\n   *   currentClientId={currentClientId}\n   * />\n   * ```\n   */\n  onReactionClick?: (emoji: string) => void;\n\n  /**\n   * Client ID of the current Ably Connection for the room.\n   * Used to determine which reactions the current user has added for visual highlighting.\n   * Reactions added by the current user are displayed with blue styling to indicate participation.\n   */\n  currentClientId: string;\n}\n\n/**\n * MessageReactions component displays emoji reactions for a chat message with interactive toggle functionality\n *\n * Core Features:\n * - Displays all emoji reactions with their total counts\n * - Visual highlighting for reactions added by the current user (blue styling)\n * - Click-to-toggle reactions (add/remove based on current user's participation)\n * - Responsive flexbox layout that wraps on smaller screens\n * - Accessibility with ARIA attributes\n * - Theme-aware styling supporting both light and dark modes\n * - Graceful handling of missing or empty reaction data\n *\n * Data Structure:\n * The component expects `message.reactions.distinct` to contain a map where:\n * - Keys are emoji characters (e.g., \"👍\", \"❤️\", \"😂\")\n * - Values contain `total` (number) and `clientIds` (string array)\n *\n * Styling:\n * • Pill-shaped buttons with rounded corners\n * • Current user's reactions: Blue background with darker blue border\n * • Other reactions: Gray background with hover effects\n * • Emoji and count displayed side-by-side within each button\n *\n * @example\n * // Basic usage within ChatMessage component\n * {message.reactions && Object.keys(message.reactions.distinct || {}).length > 0 && (\n *   <MessageReactions\n *     message={message}\n *     onReactionClick={handleReactionToggle}\n *     currentClientId={currentClientId}\n *   />\n * )}\n *\n * @example\n * // Integration with reaction management system\n * const ChatMessageWithReactions = ({ message, currentClientId }) => {\n *   const handleReactionClick = async (emoji: string) => {\n *     const reaction = message.reactions?.distinct[emoji];\n *     const hasUserReacted = reaction?.clientIds.includes(currentClientId) ?? false;\n *\n *     try {\n *       if (hasUserReacted) {\n *         await room.messages.deleteReaction(message, emoji);\n *       } else {\n *         await room.messages.addReaction(message, emoji);\n *       }\n *     } catch (error) {\n *       console.error('Failed to toggle reaction:', error);\n *     }\n *   };\n *\n *   return (\n *     <div className=\"message-container\">\n *       <div className=\"message-content\">{message.text}</div>\n *       <MessageReactions\n *         message={message}\n *         onReactionClick={handleReactionClick}\n *         currentClientId={currentClientId}\n *       />\n *     </div>\n *   );\n * };\n *\n */\nexport const MessageReactions = ({\n  message,\n  onReactionClick,\n  currentClientId,\n}: MessageReactionsProps) => {\n  const distinct = message.reactions.distinct;\n\n  // Get all emoji names that have reactions\n  const emojiNames = Object.keys(distinct);\n\n  if (emojiNames.length === 0) return;\n\n  return (\n    <div className=\"flex flex-wrap gap-1 mt-2\" role=\"group\" aria-label=\"Message reactions\">\n      {emojiNames.map((emoji) => {\n        const reaction = distinct[emoji];\n        if (!reaction) return;\n        const hasUserReacted = reaction.clientIds.includes(currentClientId);\n        const count = reaction.total;\n        return (\n          <button\n            key={emoji}\n            className={`flex items-center gap-1 px-2 py-1 rounded-full text-xs border transition-colors ${\n              hasUserReacted\n                ? 'bg-blue-100 border-blue-300 text-blue-700 dark:bg-blue-900/30 dark:border-blue-600 dark:text-blue-300'\n                : 'bg-gray-100 border-gray-300 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700'\n            }`}\n            onClick={() => onReactionClick?.(emoji)}\n            aria-label={`${emoji} reaction${hasUserReacted ? ' (you reacted)' : ''}, ${String(count)} ${count === 1 ? 'person' : 'people'}`}\n            aria-pressed={hasUserReacted}\n            type=\"button\"\n          >\n            <span aria-hidden=\"true\">{emoji}</span>\n            <span className=\"font-medium\" aria-hidden=\"true\">\n              {count}\n            </span>\n          </button>\n        );\n      })}\n    </div>\n  );\n};\n","import { Message } from '@ably/chat';\nimport { useChatClient } from '@ably/chat/react';\nimport { clsx } from 'clsx';\nimport React, { useEffect, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\n\nimport { useUserAvatar } from '../../hooks/use-user-avatar.tsx';\nimport { Avatar } from '../atoms/avatar.tsx';\nimport { Button } from '../atoms/button.tsx';\nimport { Icon } from '../atoms/icon.tsx';\nimport { TextInput } from '../atoms/text-input.tsx';\nimport { Tooltip } from '../atoms/tooltip.tsx';\nimport { ConfirmDialog } from './confirm-dialog.tsx';\nimport { EmojiPicker } from './emoji-picker.tsx';\nimport { MessageActions } from './message-actions.tsx';\nimport { MessageReactions } from './message-reactions.tsx';\n\n/**\n * Formats a timestamp into a readable time string\n * For today's dates: HH:MM format\n * For past dates: MM/DD/YYYY, HH:MM format\n *\n * @param timestamp - The timestamp to format (milliseconds since epoch)\n * @returns Formatted time string, e.g. \"12:34\" or \"1/2/2023, 12:34\". It will show the full date if the message is not from today.\n */\nconst formatTime = (timestamp?: number) => {\n  if (!timestamp) return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });\n\n  const messageDate = new Date(timestamp);\n  const today = new Date();\n\n  // Check if the message is from today\n  const isToday =\n    messageDate.getDate() === today.getDate() &&\n    messageDate.getMonth() === today.getMonth() &&\n    messageDate.getFullYear() === today.getFullYear();\n\n  return isToday\n    ? messageDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })\n    : messageDate.toLocaleDateString([], { month: 'numeric', day: 'numeric', year: 'numeric' }) +\n        ', ' +\n        messageDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });\n};\n\n/**\n * Props for the ChatMessage component\n */\nexport interface ChatMessageProps {\n  /**\n   * The Ably Chat message object used to display the message content.\n   */\n  message: Message;\n\n  /**\n   * Optional callback triggered when the user saves an edited message.\n   * @param message - The original message object being edited\n   * @param newText - The updated message text after editing\n   */\n  onEdit?: (message: Message, newText: string) => void;\n\n  /**\n   * Optional callback triggered when the user confirms message deletion.\n   * @param message - The message object to be deleted\n   */\n  onDelete?: (message: Message) => void;\n\n  /**\n   * Optional callback triggered when a user adds an emoji reaction to the message.\n   * @param message - The message object receiving the reaction\n   * @param emoji - The emoji character being added as a reaction\n   */\n  onReactionAdd?: (message: Message, emoji: string) => void;\n\n  /**\n   * Optional callback triggered when a user removes their emoji reaction from the message.\n   * Called when clicking an existing reaction the user has already added.\n   * @param message - The message object losing the reaction\n   * @param emoji - The emoji character being removed from reactions\n   */\n  onReactionRemove?: (message: Message, emoji: string) => void;\n\n  /**\n   * Additional CSS class names to apply to the message container\n   * Useful for custom styling or theming\n   */\n  className?: string;\n}\n\n/**\n * ChatMessage component displays an individual chat message with interactive capabilities\n *\n * Core Features:\n * - Message content display with sender avatar\n * - Edit/delete functionality for own messages with confirmation dialogs\n * - Emoji reactions system with picker and toggle functionality\n * - Avatar editing for message senders (own messages only)\n * - Status indicators (edited, deleted)\n * - Basic ARIA support (role, aria-label)\n * - Hover tooltips showing sender information\n *\n * @example\n * <ChatMessage\n *   message={message}\n *   currentClientId=\"user123\"\n *   onEdit={handleEdit}\n *   onDelete={handleDelete}\n *   onReactionAdd={handleReactionAdd}\n *   onReactionRemove={handleReactionRemove}\n * />\n */\nexport const ChatMessage = ({\n  message,\n  onEdit,\n  onDelete,\n  onReactionAdd,\n  onReactionRemove,\n  className,\n}: ChatMessageProps) => {\n  const [isHovered, setIsHovered] = useState(false);\n  const [isEditing, setIsEditing] = useState(false);\n  const [editText, setEditText] = useState(message.text || '');\n  const [showEmojiPicker, setShowEmojiPicker] = useState(false);\n  const [emojiPickerPosition, setEmojiPickerPosition] = useState({ top: 0, left: 0 });\n\n  // Avatar hover tooltip state\n  const [showAvatarTooltip, setShowAvatarTooltip] = useState(false);\n  const [tooltipPosition, setTooltipPosition] = useState<'above' | 'below'>('above');\n\n  // Confirm dialog state\n  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);\n\n  const messageRef = useRef<HTMLDivElement>(null);\n  const messageBubbleRef = useRef<HTMLDivElement>(null);\n  const avatarRef = useRef<HTMLDivElement>(null);\n  const { clientId } = useChatClient();\n  const isOwn = message.clientId === clientId;\n\n  const { userAvatar } = useUserAvatar({ clientId: message.clientId });\n\n  /**\n   * Enables edit mode for the message\n   */\n  const handleEdit = () => {\n    setIsEditing(true);\n  };\n\n  /**\n   * Saves the edited message text if it has changed\n   * Calls the onEdit callback with the message and new text\n   */\n  const handleSaveEdit = () => {\n    if (editText.trim() && editText !== (message.text || '')) {\n      onEdit?.(message, editText.trim());\n    }\n    setIsEditing(false);\n  };\n\n  /**\n   * Cancels the edit operation and resets the edit text\n   */\n  const handleCancelEdit = () => {\n    setEditText(message.text || '');\n    setIsEditing(false);\n  };\n\n  /**\n   * Shows the delete confirmation dialog\n   */\n  const handleDelete = () => {\n    setShowDeleteConfirm(true);\n  };\n\n  /**\n   * Handles confirmed message deletion\n   */\n  const handleConfirmDelete = () => {\n    onDelete?.(message);\n  };\n\n  /**\n   * Calculates the optimal position for the emoji picker\n   * relative to the message bubble and viewport constraints\n   */\n  const calculateEmojiPickerPosition = () => {\n    const bubbleRect = messageBubbleRef.current?.getBoundingClientRect();\n    if (!bubbleRect) return { top: 0, left: 0 };\n\n    // Use responsive width calculation\n    const pickerWidth = Math.min(240, window.innerWidth - 40);\n    const pickerHeight = Math.min(320, window.innerHeight - 40); // Responsive height\n    let left: number;\n    let top: number;\n\n    // Position the picker centered horizontally relative to the message bubble\n    left = bubbleRect.left + bubbleRect.width / 2 - pickerWidth / 2;\n\n    // Check if there's enough room above the bubble for the picker\n    const spaceAbove = bubbleRect.top;\n    const requiredSpaceAbove = Math.min(pickerHeight, 120) + 40; // Use minimum height if space is limited\n\n    // Determine initial top position\n    top =\n      spaceAbove >= requiredSpaceAbove\n        ? bubbleRect.top - pickerHeight - 20\n        : bubbleRect.bottom + 20;\n\n    // Ensure picker stays within viewport bounds horizontally\n    const maxLeft = window.innerWidth - pickerWidth - 20;\n    const minLeft = 20;\n\n    if (left < minLeft) {\n      left = minLeft;\n    } else if (left > maxLeft) {\n      left = maxLeft;\n    }\n\n    // Ensure picker stays within viewport bounds vertically with a minimum gap\n    const minGap = 20; // Minimum gap from screen edges\n    const maxTop = window.innerHeight - pickerHeight - minGap;\n    const minTop = minGap;\n\n    if (top < minTop) {\n      top = minTop;\n    } else if (top > maxTop) {\n      top = maxTop;\n    }\n\n    return { top, left };\n  };\n\n  /**\n   * Opens the emoji picker and positions it relative to the message bubble\n   * Calculates optimal position to ensure it's visible within the viewport\n   */\n  const handleAddReaction = () => {\n    const position = calculateEmojiPickerPosition();\n    setEmojiPickerPosition(position);\n    setShowEmojiPicker(true);\n  };\n\n  /**\n   * Handles emoji selection from the emoji picker\n   * Adds the selected emoji as a reaction to the message\n   *\n   * @param emoji - The selected emoji\n   */\n  const handleEmojiSelect = (emoji: string) => {\n    onReactionAdd?.(message, emoji);\n    setShowEmojiPicker(false);\n  };\n\n  // Update emoji picker position when window is resized\n  useEffect(() => {\n    if (!showEmojiPicker) return;\n\n    const handleResize = () => {\n      const position = calculateEmojiPickerPosition();\n      setEmojiPickerPosition(position);\n    };\n\n    window.addEventListener('resize', handleResize);\n    return () => {\n      window.removeEventListener('resize', handleResize);\n    };\n  }, [showEmojiPicker]);\n\n  /**\n   * Toggles a reaction on a message when clicking an existing reaction\n   * If the user has already reacted with this emoji, it removes the reaction\n   * Otherwise, it adds the reaction\n   *\n   * @param emoji - The emoji to toggle\n   */\n  const handleReactionClick = (emoji: string) => {\n    const distinct = message.reactions.distinct;\n    const hasUserReacted = distinct[emoji]?.clientIds.includes(clientId);\n\n    if (hasUserReacted) {\n      onReactionRemove?.(message, emoji);\n    } else {\n      onReactionAdd?.(message, emoji);\n    }\n  };\n\n  /**\n   * Handles keyboard events in the edit message input\n   * - Enter (without Shift) saves the edit\n   * - Escape cancels the edit\n   *\n   * @param e - The keyboard event\n   */\n  const handleKeyPress = (e: React.KeyboardEvent) => {\n    if (e.key === 'Enter' && !e.shiftKey) {\n      e.preventDefault();\n      handleSaveEdit();\n    } else if (e.key === 'Escape') {\n      handleCancelEdit();\n    }\n  };\n\n  /**\n   * Handles mouse enter event on the avatar\n   * Calculates optimal tooltip position and shows tooltip with user's clientId\n   *\n   * @param event - The mouse enter event\n   */\n  const handleAvatarMouseEnter = (event: React.MouseEvent) => {\n    const rect = event.currentTarget.getBoundingClientRect();\n    const tooltipHeight = 40; // Approximate tooltip height\n    const spaceAbove = rect.top;\n    const spaceBelow = window.innerHeight - rect.bottom;\n\n    // Position above if there's enough space, otherwise below\n    if (spaceAbove >= tooltipHeight + 10) {\n      setTooltipPosition('above');\n    } else if (spaceBelow >= tooltipHeight + 10) {\n      setTooltipPosition('below');\n    } else {\n      // If neither has enough space, use the side with more space\n      setTooltipPosition(spaceAbove > spaceBelow ? 'above' : 'below');\n    }\n\n    setShowAvatarTooltip(true);\n  };\n\n  /**\n   * Handles mouse leave event on the avatar\n   * Hides the tooltip\n   */\n  const handleAvatarMouseLeave = () => {\n    setShowAvatarTooltip(false);\n  };\n\n  /**\n   * Calculates tooltip position based on avatar location and viewport constraints\n   *\n   * @returns Object containing top and left positioning values\n   */\n  const calculateTooltipPosition = () => {\n    const avatarRect = avatarRef.current?.getBoundingClientRect();\n\n    if (!avatarRect) return;\n\n    // Approximate tooltip height (padding + text + arrow)\n    const tooltipHeight = 40;\n    const spacing = 8; // Space between avatar and tooltip\n\n    // Calculate vertical position with proper spacing\n    const tooltipY =\n      tooltipPosition === 'above'\n        ? avatarRect.top - tooltipHeight - spacing\n        : avatarRect.bottom + spacing;\n\n    // Calculate horizontal position - center on avatar\n    const avatarCenter = (avatarRect.left + avatarRect.right) / 2;\n\n    return {\n      top: tooltipY,\n      left: avatarCenter,\n    };\n  };\n\n  return (\n    <div\n      ref={messageRef}\n      className={clsx(\n        'relative flex items-start gap-2 mb-4',\n        isOwn ? 'flex-row-reverse' : 'flex-row',\n        className\n      )}\n      role=\"article\"\n      aria-label={`Message from ${message.clientId}${message.isDeleted ? ' (deleted)' : ''}${message.isUpdated ? ' (edited)' : ''}`}\n    >\n      {/* Avatar with hover tooltip functionality */}\n      <div className=\"relative\">\n        <div\n          ref={avatarRef}\n          className={`relative`}\n          onMouseEnter={handleAvatarMouseEnter}\n          onMouseLeave={handleAvatarMouseLeave}\n          aria-label={`Avatar for ${message.clientId}`}\n          tabIndex={isOwn ? 0 : undefined}\n        >\n          <Avatar\n            alt={userAvatar?.displayName}\n            src={userAvatar?.src}\n            color={userAvatar?.color}\n            size=\"sm\"\n            initials={userAvatar?.initials}\n          />\n        </div>\n\n        {/* Avatar Hover Tooltip */}\n        {showAvatarTooltip &&\n          (() => {\n            const coords = calculateTooltipPosition();\n\n            if (!coords) return;\n\n            return createPortal(\n              <Tooltip\n                position={tooltipPosition}\n                className=\"fixed transform -translate-x-1/2\"\n                style={{ top: coords.top, left: coords.left }}\n                spacing=\"none\"\n                role=\"tooltip\"\n                aria-live=\"polite\"\n              >\n                <div className=\"text-center text-sm px-2 py-1\">{message.clientId}</div>\n              </Tooltip>,\n              document.body\n            );\n          })()}\n      </div>\n\n      <div\n        className={`flex flex-col max-w-[85%] md:max-w-[80%] lg:max-w-[75%] ${isOwn ? 'items-end' : 'items-start'}`}\n      >\n        <div\n          className=\"relative\"\n          onMouseEnter={() => {\n            setIsHovered(true);\n          }}\n          onMouseLeave={() => {\n            setIsHovered(false);\n          }}\n        >\n          <div\n            ref={messageBubbleRef}\n            className={`relative px-4 py-2 rounded-2xl ${\n              isOwn\n                ? 'bg-gray-900 text-white rounded-br-md'\n                : 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-bl-md'\n            }`}\n            aria-live={message.isUpdated ? 'polite' : 'off'}\n          >\n            {isEditing ? (\n              <div className=\"min-w-[200px]\">\n                <TextInput\n                  value={editText}\n                  onChange={(e) => {\n                    setEditText(e.target.value);\n                  }}\n                  onKeyDown={handleKeyPress}\n                  placeholder=\"Edit message...\"\n                  className=\"text-sm mb-2\"\n                  autoFocus\n                  aria-label=\"Edit message text\"\n                />\n                <div className=\"flex gap-2\">\n                  <Button\n                    variant=\"primary\"\n                    size=\"sm\"\n                    onClick={handleSaveEdit}\n                    disabled={!editText.trim()}\n                  >\n                    Save\n                  </Button>\n                  <Button variant=\"secondary\" size=\"sm\" onClick={handleCancelEdit}>\n                    Cancel\n                  </Button>\n                </div>\n              </div>\n            ) : (\n              <div>\n                {message.isDeleted ? (\n                  <p className=\"text-sm leading-relaxed break-words break-all whitespace-pre-wrap italic text-gray-500 dark:text-gray-400\">\n                    Message deleted\n                  </p>\n                ) : (\n                  <p className=\"text-sm leading-relaxed break-words break-all whitespace-pre-wrap\">\n                    {message.text || ''}\n                    {message.isUpdated && <span className=\"text-xs opacity-60 ml-2\">(edited)</span>}\n                  </p>\n                )}\n              </div>\n            )}\n          </div>\n\n          {/* Message Actions to update/delete/react */}\n          {isHovered && !isEditing && !message.isDeleted && (\n            <MessageActions\n              isOwn={isOwn}\n              onReactionButtonClicked={handleAddReaction}\n              onEditButtonClicked={handleEdit}\n              onDeleteButtonClicked={handleDelete}\n            />\n          )}\n        </div>\n\n        {/* Reactions will be rendered below the relevant message */}\n        {/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */}\n        {!message.isDeleted && Object.keys(message.reactions?.distinct || {}).length > 0 && (\n          <MessageReactions\n            message={message}\n            onReactionClick={handleReactionClick}\n            currentClientId={clientId}\n          />\n        )}\n\n        <div className=\"flex items-center gap-2 mt-1 px-2\">\n          <span className=\"text-xs text-gray-500\">\n            {formatTime(message.timestamp.getTime())}\n            {!message.isDeleted && message.isUpdated && message.updatedAt && (\n              <span className=\"ml-1\">• edited {formatTime(message.updatedAt.getTime())}</span>\n            )}\n          </span>\n        </div>\n      </div>\n\n      {/* Emoji Picker */}\n      {showEmojiPicker && (\n        <EmojiPicker\n          onClose={() => {\n            setShowEmojiPicker(false);\n          }}\n          onEmojiSelect={handleEmojiSelect}\n          position={emojiPickerPosition}\n        />\n      )}\n\n      {/* Delete Confirmation Dialog */}\n      <ConfirmDialog\n        isOpen={showDeleteConfirm}\n        onClose={() => {\n          setShowDeleteConfirm(false);\n        }}\n        onConfirm={handleConfirmDelete}\n        title=\"Delete Message\"\n        message=\"Are you sure you want to delete this message? This action cannot be undone.\"\n        confirmText=\"Delete\"\n        cancelText=\"Cancel\"\n        confirmVariant=\"danger\"\n        icon={<Icon name=\"delete\" size=\"lg\" />}\n      />\n    </div>\n  );\n};\n","import { clsx } from 'clsx';\nimport React from 'react';\n\n/**\n * Props for the TypingDots component\n */\nexport interface TypingDotsProps extends React.HTMLAttributes<HTMLDivElement> {\n  /**\n   * Tailwind size utility classes for each dot\n   * @default 'w-1.5 h-1.5'\n   * @example 'w-2 h-2' for larger dots\n   */\n  dotSizeClassName?: string;\n\n  /**\n   * Custom classes for the container div that wraps all dots\n   */\n  className?: string;\n\n  /**\n   * Custom classes applied to each individual dot\n   */\n  dotClassName?: string;\n\n  /**\n   * Animation duration for the bounce effect\n   * @default '1s'\n   */\n  animationDuration?: string;\n\n  /**\n   * Color of the dots - uses CSS currentColor by default\n   * @default 'bg-current' (inherits text color)\n   */\n  dotColor?: string;\n}\n\n/**\n * Animation delays for each of the three dots to create a wave effect\n */\nconst ANIMATION_DELAYS = ['0ms', '200ms', '400ms'];\n\n/**\n * TypingDots component displays an animated three-dot indicator commonly used to show typing activity\n *\n * Features:\n * - Three dots with staggered bounce animation\n * - Customizable size, color, and animation timing\n * - Respects reduced motion preferences\n * - Basic ARIA support (role, aria-label, aria-live)\n *\n * @example\n * // Basic usage\n * <TypingDots />\n *\n * @example\n * // Custom styling\n * <TypingDots\n *   dotSizeClassName=\"w-2 h-2\"\n *   dotColor=\"bg-blue-500\"\n *   className=\"gap-1\"\n * />\n *\n * @example\n * // With custom animation\n * <TypingDots animationDuration=\"0.8s\" />\n */\nexport const TypingDots = ({\n  dotSizeClassName = 'w-1.5 h-1.5',\n  className,\n  dotClassName,\n  animationDuration = '1s',\n  dotColor = 'bg-current',\n  ...rest\n}: TypingDotsProps) => (\n  <div className={clsx('flex gap-0.5', className)} {...rest}>\n    {ANIMATION_DELAYS.map((delay) => (\n      <div\n        key={delay}\n        className={clsx(dotSizeClassName, 'rounded-full animate-bounce', dotColor, dotClassName)}\n        style={{\n          animationDelay: delay,\n          animationDuration,\n          // Ensure animation doesn't interfere with reduced motion preferences\n          // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n          ...(globalThis.window !== undefined &&\n            globalThis.matchMedia('(prefers-reduced-motion: reduce)').matches && {\n              animation: 'none',\n              opacity: 0.7,\n            }),\n        }}\n        aria-hidden=\"true\"\n      />\n    ))}\n  </div>\n);\n","import { useChatClient, useTyping } from '@ably/chat/react';\nimport { clsx } from 'clsx';\nimport React, { ReactNode, useEffect } from 'react';\n\nimport { TypingDots } from '../atoms/typing-dots.tsx';\n\n/**\n * Props for the TypingIndicators component\n */\nexport interface TypingIndicatorsProps {\n  /**\n   * Maximum number of distinct clients to display by name before collapsing to \"X others\".\n   * Controls the verbosity of the typing message to prevent overly long text.\n   * When exceeded, remaining users are summarized as \"and N other(s)\".\n   *\n   * @default 1\n   * @example\n   * // Show only first user: \"Alice is typing\"\n   * maxClients={1}\n   *\n   * @example\n   * // Show up to 3 users: \"Alice, Bob and Charlie are typing\"\n   * maxClients={3}\n   *\n   * @example\n   * // With overflow: \"Alice, Bob and 5 others are typing\"\n   * maxClients={2} // when 7 users are typing\n   */\n  maxClients?: number;\n\n  /**\n   * Additional CSS classes to apply to the container element.\n   * Merged with default styling classes.\n   *\n   * @example\n   * // Custom positioning and colors\n   * className=\"absolute bottom-2 left-4 text-blue-500\"\n   *\n   * @example\n   * // Integration with flex layouts\n   * className=\"flex-shrink-0 ml-4\"\n   */\n  className?: string;\n\n  /**\n   * CSS classes to apply specifically to the typing text element.\n   * Allows independent styling of the text content without affecting container layout.\n   *\n   * @example\n   * // Custom text styling\n   * textClassName=\"font-medium text-green-600 text-xs\"\n   *\n   * @example\n   * // Responsive text sizing\n   * textClassName=\"text-sm md:text-base\"\n   */\n  textClassName?: string;\n\n  /**\n   * Callback function triggered when the typing state changes.\n   * Useful for parent components to react to typing activity,\n   * @param typingUsers - Array of client IDs currently typing, excluding the current user.\n   *\n   * Example usage:\n   * ```tsx\n   * <TypingIndicators\n   *   onTypingChange={(typingUsers) => {\n   *   console.log('Current typing users:', typingUsers);\n   *   }}\n   *   />\n   *\n   */\n  onTypingChange?: (typingUsers: string[]) => void;\n}\n\n/**\n * TypingIndicators component displays real-time typing activity in a chat room\n *\n * Features:\n * - Animated typing dots\n * - Human-readable sentences showing who is currently typing\n * - Smart participant limiting to prevent overly long messages\n * - Automatic exclusion of the current user from typing displays\n * - Live region support for screen reader announcements\n * - Custom styling of container and text elements\n *\n * Display:\n * • 0 typing: Component returns null (nothing rendered)\n * • 1 user: \"Alice is typing\"\n * • 2 users: \"Alice and Bob are typing\"\n * • 3 users: \"Alice, Bob and Charlie are typing\"\n * • 4+ users (maxClients=3): \"Alice, Bob and 2 others are typing\"\n * • Current user excluded: Never shows \"You are typing\"\n *\n *\n * @example\n * <TypingIndicators\n *   className=\"bg-blue-50 rounded-lg px-3 py-2\"\n *   textClassName=\"text-blue-700 font-medium\"\n *   maxClients={2}\n * />\n *\n */\n\nexport const TypingIndicators = ({\n  maxClients,\n  textClassName,\n  className,\n  onTypingChange,\n}: TypingIndicatorsProps): ReactNode => {\n  const { currentlyTyping } = useTyping();\n  const { clientId } = useChatClient();\n\n  // Exclude yourself from the typing indicators\n  const activeTypingUsers = [...currentlyTyping].filter((id) => id !== clientId);\n\n  useEffect(() => {\n    if (onTypingChange) {\n      onTypingChange(activeTypingUsers);\n    }\n  }, [activeTypingUsers, onTypingChange]);\n\n  if (activeTypingUsers.length === 0) return;\n\n  return (\n    <div\n      className={clsx(\n        'flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400',\n        className\n      )}\n      role=\"status\"\n      aria-live=\"polite\"\n    >\n      <TypingDots aria-hidden=\"true\" />\n      <span className={textClassName}>{buildTypingSentence(activeTypingUsers, maxClients)}</span>\n    </div>\n  );\n};\n\n/**\n * Builds a human-readable \"is / are typing\" sentence with proper grammar and overflow handling\n *\n * Creates grammatically correct sentences showing who is typing, with special handling\n * for different numbers of users and smart truncation when limits are exceeded.\n *\n * Grammar Rules:\n * • Single user: \"Alice is typing\"\n * • Two users: \"Alice and Bob are typing\"\n * • Three users: \"Alice, Bob and Charlie are typing\"\n * • Multiple users: \"Alice, Bob, Charlie and David are typing\"\n * • With overflow: \"Alice, Bob and 3 others are typing\"\n *\n * Overflow Logic:\n * • When user count exceeds maxClients, shows first N names + \"X others\"\n * • Proper pluralization: \"1 other\" vs \"2 others\"\n * • Always maintains readability regardless of participant count\n *\n * @param clientIds - Array of client IDs who are currently typing (excluding current user)\n * @param maxClients - Maximum number of users to display by name before collapsing.\n *                     Values ≤ 0 are normalized to 1 to ensure at least one name shows.\n * @returns A formatted string describing who is typing, or empty string if no users\n *\n * @example\n * // Basic cases\n * buildTypingSentence([\"Alice\"], 3)\n * // → \"Alice is typing\"\n *\n * buildTypingSentence([\"Alice\", \"Bob\"], 3)\n * // → \"Alice and Bob are typing\"\n *\n * buildTypingSentence([\"Alice\", \"Bob\", \"Charlie\"], 3)\n * // → \"Alice, Bob and Charlie are typing\"\n *\n * // Overflow cases\n * buildTypingSentence([\"Alice\", \"Bob\", \"Charlie\", \"David\"], 2)\n * // → \"Alice, Bob and 2 others are typing\"\n *\n * buildTypingSentence([\"Alice\", \"Bob\", \"Charlie\", \"David\", \"Eve\"], 1)\n * // → \"Alice and 4 others are typing\"\n *\n * // Edge cases\n * buildTypingSentence([], 3)\n * // → \"\"\n *\n * buildTypingSentence([\"Alice\"], 0)  // maxClients normalized to 1\n * // → \"Alice is typing\"\n */\n\nfunction buildTypingSentence(clientIds: string[], maxClients = 1): string {\n  const count = clientIds.length;\n  const safeMax = Math.max(1, maxClients); // never smaller than 1\n\n  // No users\n  if (count === 0) return '';\n\n  // All users fit into the limit → list them, nothing to collapse\n  if (count <= safeMax) {\n    if (count === 1) return `${String(clientIds[0])} is typing`;\n    if (count === 2) return `${String(clientIds[0])} and ${String(clientIds[1])} are typing`;\n    if (count === 3)\n      return `${String(clientIds[0])}, ${String(clientIds[1])} and ${String(clientIds[2])} are typing`;\n\n    // >3 but still within the limit – generic join\n    const names = clientIds.slice(0, -1).map(String).join(', ');\n    return `${names} and ${String(clientIds[count - 1])} are typing`;\n  }\n  // Need to collapse the tail into \"…n others\"\n  const displayNames = clientIds.slice(0, safeMax).join(', ');\n  const remaining = count - safeMax;\n\n  return `${displayNames} and ${remaining.toString()} other${remaining > 1 ? 's' : ''} are typing`;\n}\n","import { Message } from '@ably/chat';\nimport { clsx } from 'clsx';\nimport React, {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from 'react';\n\nimport { ChatMessage } from './chat-message.tsx';\nimport { TypingIndicators } from './typing-indicators.tsx';\n\nexport interface ChatMessageListProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {\n  /**\n   * Array of Ably Chat Message objects to render in chronological order.\n   * Each message contains content, metadata, reactions, and status information.\n   */\n  messages: Message[];\n\n  /**\n   * Optional callback triggered when user scrolls near the top of the message list.\n   * Called automatically when scroll position is within loadMoreThreshold pixels from top.\n   * Use this to fetch and prepend older messages to the messages array.\n   */\n  onLoadMoreHistory?: () => void;\n\n  /**\n   * Whether a history loading operation is currently in progress.\n   * When true, displays a \"Loading messages...\" indicator at the top of the list.\n   */\n  isLoading?: boolean;\n\n  /**\n   * Whether there are more historical messages available to load.\n   * When false, displays \"No more messages to load\" indicator instead of loading spinner.\n   */\n  hasMoreHistory?: boolean;\n\n  /**\n   * Callback triggered when the user scrolls to view a specific message.\n   * @param messageSerial - The serial of the message currently in view\n   */\n  onMessageInView?: (messageSerial: string) => void;\n\n  /**\n   * Callback triggered when the user scrolls to the bottom of the message list.\n   */\n  onViewLatest?: () => void;\n\n  /**\n   * Callback triggered when a user saves an edited message.\n   * Passed through to individual ChatMessage components.\n   * @param message - The original message being edited\n   * @param newText - The updated message content\n   */\n  onEdit?: (message: Message, newText: string) => void;\n\n  /**\n   * Callback triggered when a user confirms deletion of their message.\n   * Passed through to individual ChatMessage components.\n   * @param message - The message to be deleted\n   */\n  onDelete?: (message: Message) => void;\n\n  /**\n   * Callback triggered when a user adds an emoji reaction to any message.\n   * Passed through to individual ChatMessage components.\n   * @param message - The message receiving the reaction\n   * @param emoji - The emoji character being added\n   */\n  onReactionAdd?: (message: Message, emoji: string) => void;\n\n  /**\n   * Callback triggered when a user removes their emoji reaction from a message.\n   * Passed through to individual ChatMessage components.\n   * @param message - The message losing the reaction\n   * @param emoji - The emoji character being removed\n   */\n  onReactionRemove?: (message: Message, emoji: string) => void;\n\n  /**\n   * Optional React elements to render after all messages (e.g., TypingIndicators).\n   * Commonly used for typing indicators, system messages, or loading states.\n   */\n  children?: React.ReactNode;\n\n  /**\n   * Whether to automatically scroll to bottom when new messages arrive.\n   * Only scrolls if user is already at/near the bottom to avoid interrupting reading.\n   * @default true\n   */\n  autoScroll?: boolean;\n\n  /**\n   * Distance in pixels from the top edge that triggers onLoadMoreHistory callback.\n   * Lower values require more precise scrolling, higher values load history earlier.\n   * @default 100\n   */\n  loadMoreThreshold?: number;\n\n  /**\n   * Whether to enable built-in typing indicators for other users.\n   * Displays animated dots when other users are typing in the chat room.\n   * @default true\n   */\n  enableTypingIndicators?: boolean;\n\n  /**\n   * Additional CSS classes to apply to the message list container.\n   * Merged with default styling classes using clsx.\n   */\n  className?: string;\n}\n\n/**\n * ChatMessageList component provides a scrollable, virtualized container for chat messages\n *\n * Features:\n * - Infinite scroll with lazy loading of message history\n * - Smart auto-scroll that respects user's current position\n * - Loading states and indicators for history fetching\n * - Maintains scroll position when prepending historical messages\n * - Full accessibility support with ARIA labels\n * - Forward ref support for external scroll control\n *\n * @example\n * // Basic usage\n * <ChatMessageList\n *   messages={messages}\n *   onEdit={handleEdit}\n *   onDelete={handleDelete}\n *   onReactionAdd={handleReactionAdd}\n *   onReactionRemove={handleReactionRemove}\n * />\n *\n * @example\n * // Rendering children like typing indicators\n * <ChatMessageList\n *   messages={messages}\n *   onEdit={handleEdit}\n *   onDelete={handleDelete}\n *   onReactionAdd={handleReactionAdd}\n *   onReactionRemove={handleReactionRemove}\n * >\n *   <TypingIndicator />\n * </ChatMessageList>\n *\n */\nexport const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(\n  (\n    {\n      messages,\n      onLoadMoreHistory,\n      isLoading = false,\n      hasMoreHistory = false,\n      onEdit,\n      onDelete,\n      onReactionAdd,\n      onReactionRemove,\n      onMessageInView,\n      onViewLatest,\n      autoScroll = true,\n      loadMoreThreshold = 100,\n      enableTypingIndicators = true,\n      className = '',\n      ...rest\n    },\n    ref\n  ) => {\n    const containerRef = useRef<HTMLDivElement | null>(null);\n    const lastScrollCheck = useRef(0);\n    const shouldStickAfterPrepend = useRef(false);\n    const prevScrollHeight = useRef(0);\n    const messagesMapRef = useRef<Map<string, HTMLElement>>(new Map());\n\n    const [isAtBottom, setIsAtBottom] = useState(true);\n    const [centerSerial, setCenterSerial] = useState<string | undefined>();\n\n    const isUserAtBottom = useCallback(() => {\n      if (!containerRef.current) return false;\n      const { scrollTop, scrollHeight, clientHeight } = containerRef.current;\n      return scrollHeight - scrollTop - clientHeight < 50; // px threshold\n    }, []);\n\n    const updateIsAtBottom = useCallback(() => {\n      setIsAtBottom((prev) => {\n        const atBottom = isUserAtBottom();\n        return prev === atBottom ? prev : atBottom;\n      });\n    }, [isUserAtBottom]);\n\n    const maybeLoadHistory = useCallback(() => {\n      if (!containerRef.current || !onLoadMoreHistory || !hasMoreHistory || isLoading) return;\n\n      if (containerRef.current.scrollTop < loadMoreThreshold) {\n        shouldStickAfterPrepend.current = true;\n        prevScrollHeight.current = containerRef.current.scrollHeight;\n        onLoadMoreHistory();\n      }\n    }, [onLoadMoreHistory, hasMoreHistory, isLoading, loadMoreThreshold]);\n\n    /** Determine which message is closest to the viewport centre */\n    const reportMessageInView = useCallback(() => {\n      if (!containerRef.current || messages.length === 0) return;\n\n      const rect = containerRef.current.getBoundingClientRect();\n      const viewportCenter = rect.top + rect.height / 2;\n\n      if (isUserAtBottom()) {\n        if (centerSerial !== undefined) setCenterSerial(undefined);\n        onViewLatest?.();\n        return;\n      }\n\n      let best: { serial: string; dist: number } | undefined;\n      for (const [serial, el] of messagesMapRef.current.entries()) {\n        const { top, bottom } = el.getBoundingClientRect();\n        const d = Math.abs((top + bottom) / 2 - viewportCenter);\n        if (!best || d < best.dist) best = { serial, dist: d };\n      }\n\n      if (best && best.serial !== centerSerial) {\n        setCenterSerial(best.serial);\n        onMessageInView?.(best.serial);\n      }\n    }, [centerSerial, isUserAtBottom, messages.length, onMessageInView, onViewLatest]);\n\n    const handleScroll = useCallback(() => {\n      const now = performance.now();\n      if (now - lastScrollCheck.current < 16) return; // ~60fps\n      lastScrollCheck.current = now;\n\n      updateIsAtBottom();\n      maybeLoadHistory();\n      reportMessageInView();\n    }, [updateIsAtBottom, maybeLoadHistory, reportMessageInView]);\n\n    const scrollToBottom = useCallback(() => {\n      if (!containerRef.current) return;\n      containerRef.current.scrollTop = containerRef.current.scrollHeight;\n    }, []);\n\n    const handleTypingChange = useCallback(() => {\n      if (autoScroll && isAtBottom) {\n        // Small delay to ensure DOM is updated\n        requestAnimationFrame(() => {\n          scrollToBottom();\n        });\n      }\n    }, [autoScroll, isAtBottom, scrollToBottom]);\n\n    // After messages prepend, adjust scroll so content doesn't jump\n    useLayoutEffect(() => {\n      if (!shouldStickAfterPrepend.current || !containerRef.current) return;\n      const delta = containerRef.current.scrollHeight - prevScrollHeight.current;\n      containerRef.current.scrollTop += delta;\n      shouldStickAfterPrepend.current = false;\n    }, [messages]);\n\n    // Auto‑scroll on new messages if user is at bottom\n    useLayoutEffect(() => {\n      if (autoScroll && isAtBottom) {\n        scrollToBottom();\n      }\n    }, [messages, autoScroll, isAtBottom, scrollToBottom]);\n\n    useEffect(() => {\n      const node = containerRef.current;\n      if (!node) return;\n\n      // Set initial state\n      updateIsAtBottom();\n      reportMessageInView();\n\n      node.addEventListener('scroll', handleScroll, { passive: true });\n      const resizeObs = new ResizeObserver(() => {\n        if (autoScroll && isUserAtBottom()) {\n          scrollToBottom();\n        }\n      });\n      resizeObs.observe(node);\n\n      return () => {\n        node.removeEventListener('scroll', handleScroll);\n        resizeObs.disconnect();\n      };\n    }, [\n      handleScroll,\n      autoScroll,\n      updateIsAtBottom,\n      reportMessageInView,\n      isUserAtBottom,\n      scrollToBottom,\n    ]);\n\n    const setRefs = useCallback(\n      (el: HTMLDivElement | null) => {\n        if (typeof ref === 'function') {\n          ref(el);\n        } else if (ref) {\n          ref.current = el;\n        }\n        containerRef.current = el;\n      },\n      [ref]\n    );\n\n    return (\n      <div\n        ref={setRefs}\n        className={clsx(\n          'flex-1 overflow-y-auto pt-10 px-6 pb-6 space-y-6 bg-gray-50 dark:bg-gray-950 ably-scrollbar',\n          className\n        )}\n        role=\"log\"\n        aria-label=\"Chat messages\"\n        aria-live=\"polite\"\n        aria-busy={isLoading}\n        aria-describedby={isLoading ? 'loading-status' : undefined}\n        {...rest}\n      >\n        {isLoading && (\n          <div className=\"flex justify-center py-4\" role=\"status\" aria-live=\"polite\">\n            <span className=\"text-sm text-gray-500 dark:text-gray-400\">Loading messages…</span>\n          </div>\n        )}\n\n        {!hasMoreHistory && messages.length > 0 && (\n          <div className=\"flex justify-center py-4\" role=\"status\">\n            <span className=\"text-sm text-gray-500 dark:text-gray-400\">No more messages</span>\n          </div>\n        )}\n\n        {/* Messages */}\n        {messages.map((msg) => {\n          const setEl = (el: HTMLElement | null) => {\n            if (el) messagesMapRef.current.set(msg.serial, el);\n            else messagesMapRef.current.delete(msg.serial);\n          };\n          return (\n            <div key={msg.serial} ref={setEl}>\n              <ChatMessage\n                message={msg}\n                onEdit={onEdit}\n                onDelete={onDelete}\n                onReactionAdd={onReactionAdd}\n                onReactionRemove={onReactionRemove}\n              />\n            </div>\n          );\n        })}\n        {enableTypingIndicators && (\n          <TypingIndicators className=\"px-4\" onTypingChange={handleTypingChange} />\n        )}\n      </div>\n    );\n  }\n);\n\nChatMessageList.displayName = 'ChatMessageList';\n","import { clsx } from 'clsx';\nimport React from 'react';\n\n/**\n * Props for the ChatWindowFooter component\n */\nexport interface ChatWindowFooterProps {\n  /** Content to display in the footer */\n  children?: React.ReactNode;\n  /** Additional CSS classes to apply to the footer container */\n  className?: string;\n  /** Optional ARIA label for the footer */\n  'aria-label'?: string;\n}\n\n/**\n * ChatWindowFooter component provides the footer layout for the chat window\n * Features:\n * - Consistent footer styling with dark mode support\n * - Conditionally renders based on children\n * - Positioned at the bottom of the chat area\n *\n * @example\n * // Basic usage with message input\n * <ChatWindowFooter>\n *   <MessageInput onSend={handleSend} />\n * </ChatWindowFooter>\n *\n * @example\n * // With custom styling\n * <ChatWindowFooter className=\"p-6\">\n *   <CustomFooterContent />\n * </ChatWindowFooter>\n */\nexport const ChatWindowFooter = ({\n  children,\n  className,\n  'aria-label': ariaLabel,\n}: ChatWindowFooterProps) => {\n  return (\n    <div\n      className={clsx(\n        // Layout\n        'flex items-center',\n        // Theme and borders\n        'bg-white dark:bg-gray-900',\n        'border-t border-gray-200 dark:border-gray-700',\n        // Custom classes\n        className\n      )}\n      role=\"contentinfo\"\n      aria-label={ariaLabel || 'Chat window footer'}\n    >\n      {children}\n    </div>\n  );\n};\n\nChatWindowFooter.displayName = 'ChatWindowFooter';\n","import { clsx } from 'clsx';\nimport React from 'react';\n\n/**\n * Props for the ChatWindowHeader component\n */\nexport interface ChatWindowHeaderProps {\n  /** Content to be rendered within the header */\n  children?: React.ReactNode;\n  /** Additional CSS classes to apply to the header container */\n  className?: string;\n  /** Optional ARIA label for the header */\n  'aria-label'?: string;\n}\n\n/**\n * ChatWindowHeader component provides a consistent header area for chat windows\n *\n * Features:\n * - Flexible content slot for custom header components\n * - Consistent styling with dark mode support\n * - Accessible design with proper semantic structure\n * - Fixed positioning to maintain header visibility during scroll\n *\n * @example\n * // Basic usage with room info\n * <ChatWindowHeader>\n *   <RoomInfo roomName=\"general\" />\n * </ChatWindowHeader>\n *\n * @example\n * // With custom content and styling\n * <ChatWindowHeader\n *   className=\"bg-blue-100\"\n *   aria-label=\"Chat room header for general discussion\"\n * >\n *   <div>\n *     <h2>General Discussion</h2>\n *     <span>5 participants online</span>\n *   </div>\n * </ChatWindowHeader>\n *\n * @example\n * // Empty header (spacer only)\n * <ChatWindowHeader />\n */\nexport const ChatWindowHeader = ({\n  children,\n  className = '',\n  'aria-label': ariaLabel,\n}: ChatWindowHeaderProps) => {\n  // Combine base classes with custom className\n  return (\n    <div\n      className={clsx(\n        // Layout\n        'px-6 py-4',\n        // Borders and theme\n        'border-b border-gray-200 dark:border-gray-700',\n        'bg-white dark:bg-gray-900',\n        // Custom classes\n        className\n      )}\n      role=\"banner\"\n      aria-label={ariaLabel || 'Chat window header'}\n    >\n      {children}\n    </div>\n  );\n};\n\nChatWindowHeader.displayName = 'ChatWindowHeader';\n","import { ErrorInfo, Message } from '@ably/chat';\nimport { useMessages, useTyping } from '@ably/chat/react';\nimport React, { ChangeEvent, KeyboardEvent, useCallback, useRef, useState } from 'react';\n\nimport { Button } from '../atoms/button.tsx';\nimport { Icon } from '../atoms/icon.tsx';\nimport { TextInput } from '../atoms/text-input.tsx';\nimport { EmojiPicker } from './emoji-picker.tsx';\n\n/**\n * Props for the MessageInput component\n */\nexport interface MessageInputProps {\n  /**\n   * Callback function triggered when a message is sent.\n   * Receives the built message as a parameter. This is useful for providing\n   * an optimistic UI update or for handling the message in a parent component.\n   *\n   * The input field is automatically cleared after sending.\n   * Typing indicators are stopped when a message is sent.\n   *\n   * @param message - The newly sent message object.\n   *\n   * @example\n   * ```tsx\n   * const handleSentMessage = async (message: Message) => {\n   *  // alert('Message sent: ' + message.text);\n   * };\n   *\n   * <MessageInput onSent={handleSent} />\n   * ```\n   */\n  onSent?: (message: Message) => void;\n\n  /**\n   * Placeholder text displayed in the input field when empty.\n   * Provides context about the input's purpose to users.\n   *\n   * @default \"Type a message...\"\n   *\n   * @example\n   * ```tsx\n   * <MessageInput\n   *   placeholder=\"Send a message to the team...\"\n   *   onSend={handleSend}\n   * />\n   * ```\n   */\n  placeholder?: string;\n\n  /**\n   * Callback function triggered when sending a message fails.\n   * Provides the error object and the text that failed to send.\n   * If not provided, errors will be logged to console.\n   *\n   * @param error - The error that occurred during message sending\n   * @param text - The text that failed to send\n   *\n   * @example\n   * ```tsx\n   * const handleSendError = (error: Error, text: string) => {\n   *   toast.error(`Failed to send message: ${error.message}`);\n   *   console.error('Send error:', error);\n   * };\n   *\n   * <MessageInput\n   *   onSent={handleSent}\n   *   onSendError={handleSendError}\n   * />\n   * ```\n   */\n  onSendError?: (error: ErrorInfo, text: string) => void;\n\n  /**\n   * Whether to enable typing indicators when the user is typing.\n   * When enabled, triggers typing indicators on keystroke and stops them\n   * when the input is cleared or a message is sent.\n   *\n   * @default true\n   *\n   * @example\n   * ```tsx\n   * // Disable typing indicators for performance in large rooms\n   * <MessageInput\n   *   onSent={handleSent}\n   *   enableTyping={false}\n   * />\n   * ```\n   */\n  enableTyping?: boolean;\n}\n\n/**\n * MessageInput component provides a comprehensive text input interface for composing and sending chat messages\n *\n * Core Features:\n * - Multi-line text input with automatic height adjustment (max 150px)\n * - Enter key to send (Shift+Enter for new line)\n * - Integrated emoji picker with cursor position insertion\n * - Typing indicators to alert others when composing messages\n * - Automatic input cleanup and focus management\n * - Accessible form controls with proper ARIA attributes\n * - Theme-aware styling (light/dark mode support)\n *\n * Typing Indicators:\n * - Triggered on each keystroke when content is present\n * - Automatically stopped when input is cleared or message is sent\n *\n * Emoji Integration:\n * - Picker positioned above the emoji button\n * - Smart cursor position handling for emoji insertion\n * - Maintains focus and cursor position after emoji selection\n * - Fallback behavior for browsers without selection API support\n *\n * @example\n * // Basic usage in chat interface\n * const [messages, setMessages] = useState<Message[]>([]);\n *\n * const handleSentMessage = (message) => {\n *   setMessages(prev => [...prev, message]);\n * };\n *\n * return (\n *   <div className=\"chat-container\">\n *     <MessageList messages={messages} />\n *     <MessageInput\n *       onSent={handleSentMessage}\n *       placeholder=\"Type your message here...\"\n *     />\n *   </div>\n * );\n *\n */\n\nexport const MessageInput = ({\n  onSent,\n  placeholder = 'Type a message...',\n  onSendError,\n  enableTyping = true,\n}: MessageInputProps) => {\n  const [message, setMessage] = useState('');\n  const messageRef = useRef('');\n  const [showEmojiPicker, setShowEmojiPicker] = useState(false);\n  const [emojiPickerPosition, setEmojiPickerPosition] = useState({ top: 0, left: 0 });\n  const inputRef = useRef<HTMLTextAreaElement>(null);\n  const { keystroke, stop } = useTyping();\n  const { sendMessage } = useMessages();\n\n  /**\n   * Handles sending the message, clearing the input, and stopping typing indicators\n   */\n  const handleSend = useCallback(() => {\n    const trimmedMessage = messageRef.current.trim();\n    if (trimmedMessage) {\n      sendMessage({ text: trimmedMessage })\n        .then((sentMessage) => {\n          onSent?.(sentMessage);\n          setMessage('');\n          messageRef.current = '';\n          if (enableTyping) {\n            stop().catch((error: unknown) => {\n              console.warn('Stop typing failed:', error);\n            });\n          }\n        })\n        .catch((error: unknown) => {\n          if (onSendError) {\n            onSendError(error as ErrorInfo, trimmedMessage);\n          } else {\n            console.error('Failed to send message:', error);\n          }\n        });\n    }\n  }, [sendMessage, stop, onSent, onSendError, enableTyping]);\n\n  /**\n   * Handles changes to the input field\n   * Updates the message state and manages typing indicators\n   *\n   * @param e - The input change event\n   */\n  const handleInputChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {\n    const newValue = e.target.value;\n    setMessage(newValue);\n    messageRef.current = newValue;\n\n    if (enableTyping) {\n      // Call keystroke on each keypress when there's content\n      if (newValue.trim()) {\n        keystroke().catch((error: unknown) => {\n          console.warn('Keystroke failed:', error);\n        });\n      } else {\n        // Stop typing indicator when all text is deleted\n        stop().catch((error: unknown) => {\n          console.warn('Stop typing failed:', error);\n        });\n      }\n    }\n  };\n\n  /**\n   * Handles keyboard events in the input field\n   * Sends the message when Enter is pressed (without Shift)\n   *\n   * @param e - The keyboard event\n   */\n  const handleKeyPress = (e: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {\n    if (e.key === 'Enter' && !e.shiftKey) {\n      e.preventDefault();\n      handleSend();\n    }\n  };\n\n  /**\n   * Opens the emoji picker and positions it relative to the emoji button\n   */\n  const handleEmojiButtonClick = () => {\n    // Position emoji picker above the emoji button\n    const button = document.querySelector('[data-emoji-button]');\n    if (button) {\n      const rect = button.getBoundingClientRect();\n      const pickerWidth = 200;\n      const pickerHeight = 200;\n\n      // Position above the button\n      const left = Math.max(10, rect.left - pickerWidth / 2 + rect.width / 2);\n      const top = rect.top - pickerHeight - 10;\n\n      setEmojiPickerPosition({ top, left });\n      setShowEmojiPicker(true);\n    }\n  };\n\n  /**\n   * Handles emoji selection from the emoji picker\n   * Inserts the emoji at the current cursor position\n   *\n   * @param emoji - The selected emoji character\n   */\n  const handleEmojiSelect = (emoji: string) => {\n    // Insert emoji at current cursor position or at the end\n    const input = inputRef.current;\n    if (input) {\n      const start = input.selectionStart;\n      const end = input.selectionEnd;\n      const newMessage = message.slice(0, start) + emoji + message.slice(end);\n      setMessage(newMessage);\n      messageRef.current = newMessage; // Keep ref in sync\n\n      // Trigger keystroke for emoji insertion\n      if (enableTyping) {\n        keystroke().catch((error: unknown) => {\n          console.warn('Keystroke failed:', error);\n        });\n      }\n\n      // Focus back on input and set cursor position after emoji\n      setTimeout(() => {\n        input.focus();\n        const newCursorPosition = start + emoji.length;\n        input.setSelectionRange(newCursorPosition, newCursorPosition);\n      }, 0);\n    } else {\n      // Fallback: append to end\n      const newMessage = message + emoji;\n      setMessage(newMessage);\n      messageRef.current = newMessage; // Keep ref in sync\n      if (enableTyping) {\n        keystroke().catch((error: unknown) => {\n          console.warn('Keystroke failed:', error);\n        });\n      }\n    }\n\n    setShowEmojiPicker(false);\n  };\n\n  return (\n    <div className=\"p-4 bg-white dark:bg-gray-900\" role=\"form\" aria-label=\"Message input\">\n      <div className=\"border border-gray-300 dark:border-gray-600 rounded-2xl p-2\">\n        <div className=\"flex items-end gap-3\">\n          {/* Text Input */}\n          <TextInput\n            ref={inputRef as React.Ref<HTMLTextAreaElement>}\n            variant=\"message\"\n            multiline={true}\n            maxHeight=\"150px\"\n            value={message}\n            onChange={handleInputChange}\n            onKeyDown={handleKeyPress}\n            placeholder={placeholder}\n            className=\"flex-1\"\n            aria-label=\"Message text\"\n          />\n\n          {/* Emoji Button */}\n          <Button\n            variant=\"ghost\"\n            size=\"sm\"\n            className=\"text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 self-end mb-1\"\n            onClick={handleEmojiButtonClick}\n            data-emoji-button\n            aria-label=\"Open emoji picker\"\n            aria-haspopup=\"dialog\"\n            aria-expanded={showEmojiPicker}\n          >\n            <Icon name=\"emoji\" size=\"md\" aria-hidden={true} />\n          </Button>\n        </div>\n      </div>\n\n      {/* Emoji Picker */}\n      {showEmojiPicker && (\n        <EmojiPicker\n          onClose={() => {\n            setShowEmojiPicker(false);\n          }}\n          onEmojiSelect={handleEmojiSelect}\n          position={emojiPickerPosition}\n        />\n      )}\n    </div>\n  );\n};\n","import { ErrorInfo, Message, MessageReactionType } from '@ably/chat';\nimport { useMessages, usePresence } from '@ably/chat/react';\nimport { clsx } from 'clsx';\nimport React, { useCallback } from 'react';\n\nimport { useMessageWindow } from '../../hooks/use-message-window.tsx';\nimport { ChatMessageList } from './chat-message-list.tsx';\nimport { ChatWindowFooter } from './chat-window-footer.tsx';\nimport { ChatWindowHeader } from './chat-window-header.tsx';\nimport { MessageInput } from './message-input.tsx';\n\n/**\n * Props for the ChatWindow component\n */\nexport interface ChatWindowProps {\n  /**\n   * Unique identifier for the chat room.\n   * Used for room-specific settings lookup and display customization.\n   * Must be a valid room name as defined by your chat service.\n   */\n  roomName: string;\n\n  /**\n   * Optional custom content for the header area of the chat window.\n   * Typically contains room information, participant counts, settings buttons,\n   * or other room-specific controls and metadata display.\n   *\n   * Content is rendered within the ChatWindowHeader component and inherits\n   * the header's styling and layout constraints.\n   *\n   * @example\n   * customHeaderContent={\n   *   <div className=\"flex items-center gap-2\">\n   *     <RoomInfo roomName={roomName} />\n   *     <ParticipantCount />\n   *     <RoomSettingsButton />\n   *   </div>\n   * }\n   */\n  customHeaderContent?: React.ReactNode;\n\n  /**\n   * Optional custom content for the footer area of the chat window.\n   * Typically contains additional input controls like reaction pickers,\n   * file upload buttons, formatting tools, or other message composition aids.\n   *\n   * Content is rendered alongside the MessageInput within the ChatWindowFooter\n   * and should be designed to complement the primary input functionality.\n   *\n   * @example\n   * customFooterContent={\n   *   <div className=\"flex items-center gap-2\">\n   *     <EmojiPickerButton />\n   *     <FileUploadButton />\n   *     <VoiceRecordButton />\n   *   </div>\n   * }\n   */\n  customFooterContent?: React.ReactNode;\n\n  /**\n   * Whether to show typing indicators in the chat window.\n   * When enabled, shows indicators when other users are typing.\n   *\n   * @default true\n   *\n   * @example\n   * // Disable typing indicators for performance in large rooms\n   * enableTypingIndicators={false}\n   */\n  enableTypingIndicators?: boolean;\n\n  /**\n   * When `true` (default) the user is put into the room's presence set\n   * immediately.  Set to `false` if you need to\n   * join later (e.g. after showing a “Join chat” button).\n   *\n   * @default true\n   */\n  autoEnterPresence?: boolean;\n\n  /**\n   * Controls the window size for rendering messages in UI. A larger window size will\n   * produce a smoother scrolling experience, but at the cost of increased memory usage.\n   * Too high a value may lead to significant performance issues.\n   *\n   * @default 200\n   * windowSize={200}\n   */\n  windowSize?: number;\n\n  /**\n   * Additional CSS class names to apply to the root container.\n   * Useful for custom styling, layout adjustments, theme variations,\n   * or integration with external design systems.\n   *\n   * Applied to the outermost div element and combined with default styling.\n   */\n  className?: string;\n\n  /**\n   * Custom error handling configuration for chat operations.\n   * Provides hooks for handling specific error scenarios instead of default console logging.\n   * All handlers are optional and will fall back to console.error if not provided.\n   *\n   * @example\n   * ```tsx\n   * const onError = {\n   *   onMessageUpdateError: (error, message) => {\n   *     toast.error(`Failed to edit message: ${error.message}`);\n   *     console.error('Edit error:', error);\n   *   },\n   *   onMessageDeleteError: (error, message) => {\n   *     toast.error(`Failed to delete message: ${error.message}`);\n   *   },\n   *   onSendReactionError: (error, message, emoji) => {\n   *     toast.error(`Failed to add ${emoji} reaction: ${error.message}`);\n   *   },\n   *   onMessageSendError: (error, text) => {\n   *     toast.error(`Failed to send message: ${error.message}`);\n   *   }\n   * };\n   *\n   * <ChatWindow\n   *   roomName=\"general\"\n   *   onError={onError}\n   * />\n   * ```\n   */\n  onError?: {\n    /**\n     * Called when message editing fails.\n     * Provides the error object and the message that failed to edit.\n     *\n     * @param error - The error that occurred during message editing\n     * @param message - The message that failed to edit\n     */\n    onMessageUpdateError?: (error: ErrorInfo, message: Message) => void;\n\n    /**\n     * Called when message deletion fails.\n     * Provides the error object and the message that failed to delete.\n     *\n     * @param error - The error that occurred during message deletion\n     * @param message - The message that failed to delete\n     */\n    onMessageDeleteError?: (error: ErrorInfo, message: Message) => void;\n\n    /**\n     * Called when adding a reaction to a message fails.\n     * Provides the error object, the message, and the emoji that failed to add.\n     *\n     * @param error - The error that occurred during reaction addition\n     * @param message - The message that failed to receive the reaction\n     * @param emoji - The emoji that failed to be added as a reaction\n     */\n    onSendReactionError?: (error: ErrorInfo, message: Message, emoji: string) => void;\n\n    /**\n     * Called when removing a reaction from a message fails.\n     * Provides the error object, the message, and the emoji that failed to remove.\n     *\n     * @param error - The error that occurred during reaction removal\n     * @param message - The message that failed to have the reaction removed\n     * @param emoji - The emoji that failed to be removed as a reaction\n     */\n    onRemoveReactionError?: (error: ErrorInfo, message: Message, emoji: string) => void;\n\n    /**\n     * Called when sending a message fails.\n     * Provides the error object and the text that failed to send.\n     *\n     * @param error - The error that occurred during message sending\n     * @param text - The text that failed to send\n     */\n    onMessageSendError?: (error: ErrorInfo, text: string) => void;\n  };\n}\n\n/**\n * ChatWindow component provides the main chat interface for a room.\n *\n * Features:\n * - Message display with history loading\n * - Message editing, deletion, and reactions\n * - Typing indicators and presence\n * - Custom header and footer content\n * - Discontinuity recovery on reconnection\n * - Active chat window management to control which messages are rendered in the UI.\n * - History loading with infinite scroll support\n * - Custom error handling for all chat operations\n *\n * The enableTypingIndicators prop controls both the display of typing indicators in the\n * message list and whether the message input triggers typing events on keystroke.\n *\n * @example\n * // Basic usage\n * <ChatRoomProvider\n *   key={'general'}\n *   name={'general'}\n * >\n *   <ChatWindow\n *     roomName={'general'}\n *   />\n * </ChatRoomProvider>\n *\n * @example\n * // With custom header and footer\n * <ChatRoomProvider\n *   key={'general'}\n *   name={'general'}\n * >\n *   <ChatWindow\n *     roomName={'general'}\n *     customHeaderContent={<RoomInfo />}\n *     customFooterContent={<RoomReaction />}\n *   />\n * </ChatRoomProvider>\n *\n * @example\n * // With typing indicators disabled\n * <ChatRoomProvider\n *   key={'general'}\n *   name={'general'}\n * >\n *   <ChatWindow\n *     roomName={'general'}\n *     enableTypingIndicators={false}\n *   />\n * </ChatRoomProvider>\n *\n * @example\n * // With custom error handling\n * const onError = {\n *   onMessageUpdateError: (error, message) => {\n *     toast.error(`Failed to edit message: ${error.message}`);\n *     console.error('Edit failed:', error);\n *   },\n *   onMessageDeleteError: (error, message) => {\n *     toast.error(`Failed to delete message: ${error.message}`);\n *   },\n *   onSendReactionError: (error, message, emoji) => {\n *     toast.error(`Failed to add ${emoji} reaction: ${error.message}`);\n *   },\n *   onRemoveReactionError: (error, message, emoji) => {\n *     toast.error(`Failed to remove ${emoji} reaction: ${error.message}`);\n *   },\n *   onMessageSendError: (error, text) => {\n *     toast.error(`Failed to send message: ${error.message}`);\n *   }\n * };\n *\n * <ChatRoomProvider\n *   key={'general'}\n *   name={'general'}\n * >\n *   <ChatWindow\n *     roomName={'general'}\n *     onError={onError}\n *   />\n * </ChatRoomProvider>\n */\nexport const ChatWindow = ({\n  roomName,\n  customHeaderContent,\n  customFooterContent,\n  windowSize = 200,\n  enableTypingIndicators = true,\n  autoEnterPresence = true,\n  className,\n  onError,\n}: ChatWindowProps) => {\n  // Initialize presence for the room,\n  usePresence({ autoEnterLeave: autoEnterPresence });\n\n  const { deleteMessage, updateMessage, sendReaction, deleteReaction } = useMessages();\n\n  const {\n    activeMessages,\n    updateMessages,\n    showLatestMessages,\n    showMessagesAroundSerial,\n    loadMoreHistory,\n    hasMoreHistory,\n    loading,\n  } = useMessageWindow({ windowSize });\n\n  const handleRESTMessageUpdate = useCallback(\n    (updated: Message) => {\n      updateMessages([updated]);\n    },\n    [updateMessages]\n  );\n\n  const handleMessageUpdate = useCallback(\n    (msg: Message, newText: string) => {\n      const updated = msg.copy({ text: newText, metadata: msg.metadata, headers: msg.headers });\n\n      updateMessage(msg.serial, updated)\n        .then(handleRESTMessageUpdate)\n        .catch((error: unknown) => {\n          if (onError?.onMessageUpdateError) {\n            onError.onMessageUpdateError(error as ErrorInfo, msg);\n          } else {\n            console.error('Failed to update message:', error);\n          }\n        });\n    },\n    [updateMessage, handleRESTMessageUpdate, onError]\n  );\n\n  const handleMessageDelete = useCallback(\n    (msg: Message) => {\n      deleteMessage(msg, { description: 'deleted by user' })\n        .then(handleRESTMessageUpdate)\n        .catch((error: unknown) => {\n          if (onError?.onMessageDeleteError) {\n            onError.onMessageDeleteError(error as ErrorInfo, msg);\n          } else {\n            console.error('Failed to delete message:', error);\n          }\n        });\n    },\n    [deleteMessage, handleRESTMessageUpdate, onError]\n  );\n\n  const handleReactionAdd = useCallback(\n    (msg: Message, emoji: string) => {\n      sendReaction(msg, { type: MessageReactionType.Distinct, name: emoji }).catch(\n        (error: unknown) => {\n          if (onError?.onSendReactionError) {\n            onError.onSendReactionError(error as ErrorInfo, msg, emoji);\n          } else {\n            console.error('Failed to add reaction:', error);\n          }\n        }\n      );\n    },\n    [sendReaction, onError]\n  );\n\n  const handleReactionRemove = useCallback(\n    (msg: Message, emoji: string) => {\n      deleteReaction(msg, { type: MessageReactionType.Distinct, name: emoji }).catch(\n        (error: unknown) => {\n          if (onError?.onRemoveReactionError) {\n            onError.onRemoveReactionError(error as ErrorInfo, msg, emoji);\n          } else {\n            console.error('Failed to remove reaction:', error);\n          }\n        }\n      );\n    },\n    [deleteReaction, onError]\n  );\n\n  return (\n    <div\n      className={clsx('flex flex-col h-full bg-white dark:bg-gray-900 flex-1', className)}\n      role=\"main\"\n      aria-label={`Chat room: ${roomName}`}\n    >\n      {/* Header */}\n      {customHeaderContent && <ChatWindowHeader>{customHeaderContent}</ChatWindowHeader>}\n\n      {/* Messages */}\n      <ChatMessageList\n        messages={activeMessages}\n        isLoading={loading}\n        onLoadMoreHistory={() => {\n          void loadMoreHistory();\n        }}\n        hasMoreHistory={hasMoreHistory}\n        enableTypingIndicators={enableTypingIndicators}\n        onEdit={handleMessageUpdate}\n        onDelete={handleMessageDelete}\n        onReactionAdd={handleReactionAdd}\n        onReactionRemove={handleReactionRemove}\n        onMessageInView={showMessagesAroundSerial}\n        onViewLatest={showLatestMessages}\n      ></ChatMessageList>\n\n      {/* Footer */}\n      <ChatWindowFooter>\n        <div className=\"flex-1\">\n          <MessageInput\n            onSent={(msg) => {\n              updateMessages([msg]);\n            }}\n            placeholder={`Message ${roomName}...`}\n            aria-label={`Send message to ${roomName}`}\n            onSendError={onError?.onMessageSendError}\n            enableTyping={enableTypingIndicators}\n          />\n        </div>\n        {customFooterContent}\n      </ChatWindowFooter>\n    </div>\n  );\n};\n\nChatWindow.displayName = 'ChatWindow';\n","import { clsx } from 'clsx';\nimport React from 'react';\n\n/**\n * Props for the EmptyState component\n */\nexport interface EmptyStateProps {\n  /**\n   * Custom icon element to display above the title.\n   * If not provided, no icon will be shown.\n   * Should typically be an SVG or Icon component with appropriate styling.\n   */\n  icon?: React.ReactNode;\n\n  /**\n   * Main heading text displayed prominently to describe the empty state.\n   * This should be a clear, concise description of what's missing.\n   */\n  title: string;\n\n  /**\n   * Optional descriptive message providing additional context or instructions.\n   * Displayed below the title in smaller, muted text.\n   */\n  message?: string;\n\n  /**\n   * Optional action element (typically a Button) to help users resolve the empty state.\n   * Displayed below the message text if provided.\n   *\n   * @example\n   * <Button variant=\"primary\" onClick={handleCreateRoom}>\n   *   Create New Room\n   * </Button>\n   */\n  action?: React.ReactNode;\n\n  /**\n   * Additional CSS class names to apply to the root container.\n   * Useful for custom styling, spacing adjustments, or theme variations.\n   */\n  className?: string;\n\n  /**\n   * Optional accessible label for the empty state container.\n   * If not provided, defaults to \"Empty state\".\n   * Used by screen readers to describe the purpose of this section.\n   */\n  ariaLabel?: string;\n\n  /**\n   * Controls the maximum width of the content area.\n   * @default \"md\" - Sets max-width to 28rem (448px)\n   */\n  maxWidth?: 'sm' | 'md' | 'lg' | 'xl';\n\n  /**\n   * Controls the vertical alignment within the container.\n   * @default \"center\" - Centers content vertically in available space\n   */\n  verticalAlign?: 'top' | 'center' | 'bottom';\n\n  /**\n   * Controls the horizontal alignment of text content.\n   * @default \"center\" - Centers all text content\n   */\n  textAlign?: 'left' | 'center' | 'right';\n}\n\n/**\n * EmptyState molecule displays a message when no content is available\n *\n * Features:\n * - Flexible icon display with custom icon support\n * - Clear title and optional descriptive message\n * - Optional action button for user guidance\n * - Responsive design with configurable layout options\n * - Full accessibility support with ARIA labels and semantic HTML\n * - Consistent spacing and typography following design system\n * - Dark mode support with appropriate color variations\n *\n * Layout Structure:\n * - Icon (optional) - displayed prominently at the top\n * - Title - main heading describing the empty state\n * - Message (optional) - supporting text with additional context\n * - Action (optional) - call-to-action button or link\n *\n * @example\n * // Basic usage with title only\n * <EmptyState title=\"No messages yet\" />\n *\n * @example\n * // With custom icon and descriptive message\n * <EmptyState\n *   icon={<Icon name=\"chat\" size=\"xl\" className=\"text-gray-400\" />}\n *   title=\"No rooms selected\"\n *   message=\"Choose a room from the sidebar to start chatting with your team\"\n * />\n *\n * @example\n * // With action button to resolve the empty state\n * <EmptyState\n *   icon={<Icon name=\"plus-circle\" size=\"xl\" className=\"text-blue-400\" />}\n *   title=\"No rooms available\"\n *   message=\"Create your first room to start collaborating\"\n *   action={\n *     <Button variant=\"primary\" onClick={handleCreateRoom}>\n *       Create New Room\n *     </Button>\n *   }\n * />\n *\n * @example\n * // Custom styling and layout\n * <EmptyState\n *   title=\"Search returned no results\"\n *   message=\"Try adjusting your search terms or filters\"\n *   maxWidth=\"lg\"\n *   textAlign=\"left\"\n *   className=\"bg-gray-50 dark:bg-gray-800 rounded-lg p-8\"\n * />\n */\nexport const EmptyState = ({\n  icon,\n  title,\n  message,\n  action,\n  className,\n  ariaLabel = 'Empty state',\n  maxWidth = 'md',\n  verticalAlign = 'center',\n  textAlign = 'center',\n}: EmptyStateProps) => {\n  /**\n   * Maps maxWidth prop to Tailwind CSS classes\n   */\n  const maxWidthClasses = {\n    sm: 'max-w-sm',\n    md: 'max-w-md',\n    lg: 'max-w-lg',\n    xl: 'max-w-xl',\n  };\n\n  /**\n   * Maps verticalAlign prop to Tailwind CSS flexbox classes\n   */\n  const verticalAlignClasses = {\n    top: 'justify-start',\n    center: 'justify-center',\n    bottom: 'justify-end',\n  };\n\n  /**\n   * Maps textAlign prop to Tailwind CSS text alignment classes\n   */\n  const textAlignClasses = {\n    left: 'text-left',\n    center: 'text-center',\n    right: 'text-right',\n  };\n\n  return (\n    <div\n      className={clsx('flex-1 flex items-center', verticalAlignClasses[verticalAlign], className)}\n      role=\"status\"\n      aria-label={ariaLabel}\n    >\n      <div\n        className={clsx(\n          maxWidthClasses[maxWidth],\n          'mx-auto px-4 py-8',\n          textAlignClasses[textAlign]\n        )}\n      >\n        {/* Icon Section */}\n        {icon && (\n          <div className=\"mb-6\" aria-hidden=\"true\">\n            {icon}\n          </div>\n        )}\n\n        {/* Title Section */}\n        <h3 className=\"text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3\">{title}</h3>\n\n        {/* Message Section */}\n        {message && (\n          <p className=\"text-sm text-gray-600 dark:text-gray-400 leading-relaxed mb-6\">{message}</p>\n        )}\n\n        {/* Action Section */}\n        {action && (\n          <div className={clsx('mt-6', textAlign !== 'center' && 'flex justify-start')}>\n            {action}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n};\n\n// Set display name for better debugging experience\nEmptyState.displayName = 'EmptyState';\n","import { useCallback, useEffect, useState } from 'react';\n\nimport { AvatarData } from '../components/atoms/avatar.tsx';\nimport { useAvatar } from './use-avatar.tsx';\n\n/**\n * Props for the useRoomAvatar hook\n */\nexport interface UseRoomAvatarProps {\n  /**\n   * The unique identifier for the room.\n   * Used as the primary key for avatar storage and retrieval.\n   * Should be consistent across all components referencing the same room.\n   *\n   * @example\n   * // Using a unique room name\n   * const { roomAvatar } = useRoomAvatar({ roomName: \"room_123\" });\n   */\n  roomName: string;\n\n  /**\n   * Optional human-readable display name for the room.\n   * Used as the default displayName when creating new avatars.\n   * If not provided, falls back to using roomName as the display name.\n   * Can be updated later using setRoomAvatar.\n   *\n   * @example\n   * // With custom display name\n   * const { roomAvatar } = useRoomAvatar({\n   *   roomName: \"room_123\",\n   *   displayName: \"General Discussion\"\n   * });\n   *\n   * @example\n   * // Display name defaults to roomName\n   * const { roomAvatar } = useRoomAvatar({ roomName: \"general-chat\" });\n   * // → displayName will be \"general-chat\"\n   */\n  displayName?: string;\n}\n\n/**\n * Return type for the useRoomAvatar hook\n */\nexport interface UseRoomAvatarReturn {\n  /**\n   * The current avatar data for the room.\n   * Contains display name, color, initials, and optional image source.\n   * Undefined during initial loading or if avatar creation fails.\n   * Updates automatically when avatar data changes.\n   *\n   * @example\n   * const { roomAvatar } = useRoomAvatar({ roomName: \"room_123\" });\n   *\n   * if (roomAvatar) {\n   *   console.log(roomAvatar.displayName); // \"General Discussion\"\n   *   console.log(roomAvatar.color);       // \"#3B82F6\"\n   *   console.log(roomAvatar.initials);    // \"GD\"\n   * }\n   */\n  roomAvatar: AvatarData | undefined;\n\n  /**\n   * Function to update the room avatar with new data.\n   * Merges provided data with existing avatar properties.\n   * Updates both the avatar context cache and local component state.\n   * Changes persist across component unmounts and remounts.\n   *\n   * @param avatarData - Partial avatar data to merge with existing data\n   *\n   * @example\n   * // Update display name and color\n   * setRoomAvatar({\n   *   displayName: \"Updated Room Name\",\n   *   color: \"#EF4444\"\n   * });\n   *\n   * @example\n   * // Add custom avatar image\n   * setRoomAvatar({\n   *   src: \"https://example.com/room-avatar.jpg\"\n   * });\n   *\n   * @example\n   * // Update only the color\n   * setRoomAvatar({ color: \"#10B981\" });\n   */\n  setRoomAvatar: (avatarData: Partial<AvatarData>) => void;\n}\n\n/**\n * Custom hook for managing room avatar data with automatic generation and caching\n *\n * Features:\n * - Automatic avatar retrieval from the avatar context cache\n * - On-demand avatar creation for new rooms with generated colors and initials\n * - Fallback display name generation from roomName when not provided\n * - Optimistic UI updates with immediate local state changes\n * - Used in conjunction with the <Avatar /> component for display\n *\n * @example\n * // Basic usage in room components\n * const RoomHeader = ({ roomId }) => {\n *   const { roomAvatar, setRoomAvatar } = useRoomAvatar({ roomName: roomId });\n *\n *   return (\n *     <div className=\"flex items-center gap-3\">\n *       <Avatar\n *         src={roomAvatar?.src}\n *         color={roomAvatar?.color}\n *         initials={roomAvatar?.initials}\n *         alt={roomAvatar?.displayName}\n *       />\n *       <h1>{roomAvatar?.displayName}</h1>\n *     </div>\n *   );\n * };\n */\n\nexport const useRoomAvatar = ({\n  roomName,\n  displayName,\n}: UseRoomAvatarProps): UseRoomAvatarReturn => {\n  const { getAvatarForRoom, createAvatarForRoom, setRoomAvatar: updateRoomAvatar } = useAvatar();\n  const [avatar, setAvatar] = useState<AvatarData | undefined>();\n\n  useEffect(() => {\n    // Try to get existing avatar\n    const existingAvatar = getAvatarForRoom(roomName);\n\n    if (existingAvatar) {\n      setAvatar(existingAvatar);\n    } else {\n      // Create a new avatar if one doesn't exist\n      const newAvatar = createAvatarForRoom(roomName, displayName);\n      setAvatar(newAvatar);\n    }\n  }, [getAvatarForRoom, createAvatarForRoom, roomName, displayName]);\n\n  /**\n   * Updates the room avatar both in the cache and local state\n   *\n   * @param avatarData - Partial avatar data to update\n   */\n  const setRoomAvatar = useCallback(\n    (avatarData: Partial<AvatarData>) => {\n      updateRoomAvatar(roomName, avatarData);\n      setAvatar((prev) => (prev ? { ...prev, ...avatarData } : undefined));\n    },\n    [updateRoomAvatar, roomName]\n  );\n\n  return {\n    roomAvatar: avatar,\n    setRoomAvatar,\n  };\n};\n","import React from 'react';\n\nimport { useUserAvatar } from '../../hooks/use-user-avatar.tsx';\nimport { Avatar, AvatarData } from '../atoms/avatar.tsx';\nimport { TypingDots } from '../atoms/typing-dots.tsx';\n\n/**\n * Props for the Participant component\n */\nexport interface ParticipantProps {\n  /**\n   * Unique clientId for the participant.\n   * Used for avatar generation and display name when no custom avatar is provided.\n   */\n  clientId: string;\n\n  /**\n   * Whether the participant is currently present/online in the room.\n   * Controls the presence indicator color (green for online, gray for offline).\n   */\n  isPresent: boolean;\n\n  /**\n   * Whether this participant represents the current client.\n   * When true, displays \"(you)\" label and hides typing indicators for self.\n   */\n  isSelf: boolean;\n\n  /**\n   * Whether the participant is currently typing in the chat.\n   * Shows animated typing dots and \"typing...\" text when true (except for current user).\n   */\n  isTyping: boolean;\n\n  /**\n   * Optional custom avatar data for the participant.\n   * If not provided, uses the useUserAvatar hook to generate/retrieve avatar data.\n   */\n  avatar?: AvatarData;\n}\n\n/**\n * Participant component displays detailed information about a chat room participant\n *\n * Features:\n * - Avatar display with automatic fallback to generated avatars via useUserAvatar hook\n * - Real-time presence indicator (green dot for online, gray for offline)\n * - Typing status with animated dots and text indicator\n * - Current user identification with \"(you)\" label\n * - Accessible design with proper ARIA attributes and screen reader support\n * - Hover effects for interactive feel within participant lists\n * - Theme-aware styling supporting light and dark modes\n *\n * Styling:\n * • Status line showing either typing animation, online, or offline state\n * • Proper text truncation for long participant names\n *\n *\n * @example\n * // Basic participant in a list\n * <Participant\n *   clientId=\"user123\"\n *   isPresent={true}\n *   isSelf={false}\n *   isTyping={false}\n * />\n *\n * @example\n * // Current user with custom avatar\n * <Participant\n *   clientId=\"currentUser\"\n *   isPresent={true}\n *   isSelf={true}\n *   isTyping={false}\n *   avatar={{\n *     displayName: \"John Doe\",\n *     src: \"https://example.com/avatar.jpg\",\n *     color: \"bg-blue-500\"\n *   }}\n * />\n *\n *\n */\n\nexport const Participant = ({\n  clientId,\n  isPresent,\n  isSelf,\n  isTyping,\n  avatar: propAvatar,\n}: ParticipantProps) => {\n  // Use the custom hook to get or create user avatar\n  const { userAvatar } = useUserAvatar({ clientId });\n  const avatarData = propAvatar || userAvatar;\n\n  // Use the helper function\n  const statusText = getParticipantStatus(isTyping, isPresent, isSelf);\n\n  return (\n    <div\n      className=\"flex items-center gap-3 p-3 hover:bg-gray-50 dark:hover:bg-gray-700\"\n      role=\"listitem\"\n      aria-label={`${isSelf ? 'You' : clientId}, ${statusText}`}\n    >\n      <div className=\"relative\">\n        <Avatar\n          alt={avatarData?.displayName}\n          src={avatarData?.src}\n          color={avatarData?.color}\n          size=\"sm\"\n          initials={avatarData?.initials}\n        />\n        {/* Presence Icon */}\n        <div\n          className={`absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-white dark:border-gray-800 ${\n            isPresent ? 'bg-green-500' : 'bg-gray-400'\n          }`}\n          aria-hidden=\"true\"\n          title={isPresent ? 'Online' : 'Offline'}\n        />\n      </div>\n\n      <div className=\"flex-1 min-w-0\">\n        <div className=\"flex items-center gap-2\">\n          <h4 className=\"font-medium text-gray-900 dark:text-gray-100 truncate\">\n            {clientId}\n            {isSelf && <span className=\"ml-1 text-xs text-gray-500\">(you)</span>}\n          </h4>\n        </div>\n        {/* Status */}\n        <div className=\"flex items-center gap-2 mt-0.5\">\n          {/* Check if this participant is currently typing */}\n          {isTyping && !isSelf ? (\n            <span className=\"text-xs text-blue-600 dark:text-blue-400 flex items-center gap-1\">\n              <TypingDots aria-hidden=\"true\" />\n              typing...\n            </span>\n          ) : isPresent ? (\n            <span className=\"text-sm text-green-600 dark:text-green-400\">Online</span>\n          ) : (\n            <span className=\"text-sm text-gray-500 dark:text-gray-400\">Offline</span>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n};\n\n/**\n * Helper function to determine participant status text\n *\n * @param isTyping - Whether the participant is currently typing\n * @param isPresent - Whether the participant is currently present/online\n * @param isSelf - Whether this participant represents the current user\n * @returns Status text for the participant\n */\nconst getParticipantStatus = (isTyping: boolean, isPresent: boolean, isSelf: boolean): string => {\n  if (isTyping && !isSelf) return 'typing';\n  return isPresent ? 'online' : 'offline';\n};\n","import { PresenceMember } from '@ably/chat';\nimport React from 'react';\n\nimport { Button } from '../atoms/button.tsx';\nimport { Icon } from '../atoms/icon.tsx';\nimport { Participant } from './participant.tsx';\n\n/**\n * Props for the ParticipantList component\n */\nexport interface ParticipantListProps {\n  /**\n   * Array of Ably Chat presence members currently in the room.\n   * Used to display all participants with their online status.\n   */\n  presenceData: PresenceMember[];\n\n  /**\n   * Client ID of the current Ably Connection for the room.\n   * Used to sort the current user to the top of the list and for self-identification.\n   */\n  currentClientId: string;\n\n  /**\n   * Set of client IDs for users who are currently typing.\n   * Used to show typing indicators next to participant names.\n   */\n  currentlyTyping: Set<string>;\n\n  /**\n   * Callback function to toggle the list open/closed state.\n   * Called when the close button is clicked or when backdrop interaction occurs.\n   */\n  onToggle: () => void;\n\n  /**\n   * Absolute positioning coordinates for rendering the modal.\n   * The modal will be positioned relative to the viewport using these coordinates.\n   * Typically calculated based on the trigger element's position.\n   */\n  position: { top: number; left: number };\n}\n\n/**\n * ParticipantList component displays a positioned modal showing all room participants\n *\n * Features:\n * - Modal overlay with absolute positioning based on provided coordinates\n * - Participant list with avatars, names, and status indicators\n * - Smart sorting: current user appears first, followed by alphabetical order\n * - Typing indicators for active participants\n * - Participant count display in header\n * - Scrollable list with fixed maximum height for large participant counts\n * - Accessible modal dialog with proper ARIA attributes and focus management\n * - Theme-aware styling supporting both light and dark modes\n *\n * @example\n * // Integration with presence and typing hooks\n * const { presenceData } = usePresenceListener();\n * const { currentlyTyping } = useTyping();\n * const { clientId } = useChatClient();\n *\n * {participantListOpen && (<ParticipantList\n *   presenceData={presenceData || []}\n *   currentClientId={clientId}\n *   currentlyTyping={currentlyTyping}\n *   onToggle={toggleParticipantList}\n *   position={{ top: 100, left: 200 }}\n * />)}\n */\nexport const ParticipantList = ({\n  presenceData,\n  currentClientId,\n  currentlyTyping,\n  onToggle,\n  position,\n}: ParticipantListProps) => {\n  // Calculate present count from unique clientIds in presence data\n  const presentCount = presenceData.length || 0;\n\n  // Sort participants: current user first, then by clientId\n  const sortedParticipants = [...presenceData].sort((a, b) => {\n    if (a.clientId === currentClientId && b.clientId !== currentClientId) return -1;\n    if (a.clientId !== currentClientId && b.clientId === currentClientId) return 1;\n    return a.clientId.localeCompare(b.clientId);\n  });\n\n  return (\n    <div\n      className=\"absolute bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg min-w-80 max-h-96 overflow-hidden z-50\"\n      style={{\n        top: position.top,\n        left: position.left,\n        transform: 'translateX(-50%)',\n      }}\n      role=\"dialog\"\n      aria-modal=\"true\"\n      aria-labelledby=\"participants-heading\"\n    >\n      {/* Header */}\n      <div className=\"p-4 border-b border-gray-200 dark:border-gray-600\">\n        <div className=\"flex items-center justify-between\">\n          <h3 id=\"participants-heading\" className=\"font-semibold text-gray-900 dark:text-gray-100\">\n            Participants ({presentCount})\n          </h3>\n          <Button variant=\"ghost\" size=\"sm\" onClick={onToggle} aria-label=\"Close participants list\">\n            <Icon name=\"close\" size=\"sm\" aria-hidden={true} />\n          </Button>\n        </div>\n        <p className=\"text-sm text-gray-500 dark:text-gray-400 mt-1\" aria-live=\"polite\">\n          {presentCount} {presentCount === 1 ? 'person' : 'people'} present\n        </p>\n      </div>\n\n      {/* Participants List */}\n      <div className=\"max-h-64 overflow-y-auto\" role=\"list\" aria-label=\"Room participants\">\n        {sortedParticipants.map((member) => {\n          // Get the avatar for this user from the AvatarProvider\n          return (\n            <Participant\n              key={member.clientId}\n              clientId={member.clientId}\n              isPresent={true}\n              isSelf={member.clientId === currentClientId}\n              isTyping={currentlyTyping.has(member.clientId)}\n            />\n          );\n        })}\n      </div>\n    </div>\n  );\n};\n","import { PresenceMember } from '@ably/chat';\nimport React from 'react';\n\n/**\n * Props for the PresenceCount component\n */\nexport interface PresenceCountProps {\n  /**\n   * Array of Ably Chat presence members currently active in the room.\n   * Each member represents a unique participant with clientId.\n   * When null/undefined, the count defaults to 0 and the badge is hidden.\n   */\n  presenceData: PresenceMember[];\n}\n\n/**\n * PresenceCount component displays a small circular badge showing the number of online participants\n *\n * Features:\n * - Small circular badge positioned absolutely for overlay on avatars or containers\n * - Green background color indicating positive/active status\n * - Smart display logic: hides completely when count is zero\n * - Caps display at \"99+\" for very large numbers to maintain badge size\n * - Accessible with proper ARIA attributes for screen readers\n * - Compact 24x24px (w-6 h-6) size suitable for overlaying on avatars\n *\n * @example\n * // With presence data from hooks\n * const { presenceData } = usePresenceListener();\n *\n * <div className=\"relative\">\n *   <RoomIcon roomName={roomName} />\n *   <PresenceCount presenceData={presenceData || []} />\n * </div>\n *\n * @example\n * // Different count scenarios\n * // No badge shown (count = 0)\n * <PresenceCount presenceData={[]} />\n *\n * // Shows \"1\" (count = 1)\n * <PresenceCount presenceData={[{ clientId: \"user1\" }]} />\n *\n * // Shows \"99+\" (count > 99)\n * <PresenceCount presenceData={arrayWith150Members} />\n */\nexport const PresenceCount = ({ presenceData }: PresenceCountProps) => {\n  const presentCount = presenceData.length || 0;\n\n  if (presentCount === 0) {\n    return;\n  }\n\n  return (\n    <div\n      className=\"absolute -bottom-1 -right-1 bg-green-500 text-white text-xs rounded-full w-6 h-6 flex items-center justify-center font-medium\"\n      aria-label={`${String(presentCount)} ${presentCount === 1 ? 'person' : 'people'} online`}\n      role=\"status\"\n    >\n      {presentCount > 99 ? '99+' : presentCount}\n    </div>\n  );\n};\n","import { usePresenceListener } from '@ably/chat/react';\nimport React, { useEffect, useState } from 'react';\n\n/**\n * Props for the PresenceIndicators component\n */\nexport interface PresenceIndicatorsProps {\n  /**\n   * Additional CSS classes to apply to the component's text element.\n   * Merged with the component's default styling for customization.\n   * Useful for adjusting text size, spacing, or alignment within parent layouts.\n   */\n  className?: string;\n}\n\n/**\n * PresenceIndicators component displays human-readable text about room participant count\n *\n * Features:\n * - Automatic deduplication of participants by clientId (handles multiple connections)\n * - Smart color indication: green when participants present, gray when empty\n * - Proper singular/plural text formatting (\"1 person\" vs \"N people\")\n * - Accessible live region for screen reader announcements of presence changes\n * - Automatically updates when presence data changes\n *\n * @example\n * // Basic usage within RoomInfo component (actual usage)\n * <div className=\"flex items-center gap-2\">\n *   <PresenceIndicators />\n *   <TypingIndicators className=\"text-xs\" />\n * </div>\n *\n * @example\n * // Different presence scenarios and display\n * // 0 participants: \"0 people present\" (gray text)\n * // 1 participant: \"1 person present\" (green text)\n * // 5 participants: \"5 people present\" (green text)\n * // Duplicate clientIds are automatically deduplicated\n */\nexport const PresenceIndicators = ({ className = '' }: PresenceIndicatorsProps) => {\n  const { presenceData } = usePresenceListener();\n  const [presenceText, setPresenceText] = useState('0 people present');\n\n  useEffect(() => {\n    const presentCount = new Set(presenceData.map((p) => p.clientId)).size;\n    const newText =\n      presentCount === 1 ? '1 person present' : `${String(presentCount)} people present`;\n    setPresenceText(newText);\n  }, [presenceData]);\n\n  // Determine if anyone is present\n  const isAnyonePresent = (presenceData.length || 0) > 0;\n\n  return (\n    <p\n      className={`text-sm ${\n        isAnyonePresent ? 'text-green-500' : 'text-gray-500 dark:text-gray-400'\n      } ${className}`}\n      role=\"status\"\n      aria-live=\"polite\"\n    >\n      {presenceText}\n    </p>\n  );\n};\n","import { PresenceMember } from '@ably/chat';\nimport { usePresenceListener } from '@ably/chat/react';\nimport { clsx } from 'clsx';\nimport React, { useEffect, useState } from 'react';\nimport { createPortal } from 'react-dom';\n\nimport { Tooltip } from '../atoms/tooltip.tsx';\n\n/**\n * Props for the PresenceList component\n */\nexport interface PresenceListProps extends React.HTMLAttributes<HTMLDivElement> {\n  /**\n   * Positioning of the tooltip relative to its trigger element.\n   * - `above`: Tooltip appears above trigger with downward-pointing arrow\n   * - `below`: Tooltip appears below trigger with upward-pointing arrow\n   */\n  tooltipPosition: 'above' | 'below';\n\n  /**\n   * Absolute viewport coordinates (in pixels) where the tooltip should be\n   * rendered. Calculated by the parent component.\n   */\n  coords?: { top: number; left: number } | null;\n\n  /**\n   * Optional CSS classes to apply to the Tooltip component.\n   * Allows customization of the tooltip's background, padding, shadows, etc.\n   * Merged with default tooltip styling using clsx.\n   */\n  tooltipClassName?: string;\n\n  /**\n   * Optional CSS classes to apply to the tooltip text content.\n   * Allows customization of font size, weight, color, alignment, etc.\n   * Merged with default text styling (centered, truncated) using clsx.\n   */\n  textClassName?: string;\n}\n\n/**\n * Builds a human-readable sentence describing who is present in the room\n *\n * - Shows first 3 participant names explicitly\n * - For additional participants, shows count as \"and N more participant(s)\"\n * - Handles proper pluralization for both names and remaining count\n * - Uses proper grammar with \"is/are\" based on participant count\n *\n * @param presenceData - Array of Ably Chat presence members\n * @returns A formatted string describing current room participants\n *\n * @example\n * // Examples of generated text:\n * // []                           → \"No one is currently present\"\n * // [\"Alice\"]                    → \"Alice is present\"\n * // [\"Alice\", \"Bob\"]             → \"Alice, Bob are present\"\n * // [\"Alice\", \"Bob\", \"Charlie\"]  → \"Alice, Bob and 2 more are present\"\n */\nconst buildPresenceSentence = (presenceData: PresenceMember[]): string => {\n  if (presenceData.length === 0) {\n    return 'No one is currently present';\n  }\n\n  const names = presenceData.slice(0, 2).map((m) => m.clientId);\n  const remaining = presenceData.length - 2;\n\n  return remaining > 0\n    ? `${names.join(', ')} and ${String(remaining)} more are present`\n    : `${names.join(', ')} ${names.length > 1 ? 'are' : 'is'} present`;\n};\n\n/**\n * PresenceList component displays a tooltip with detailed information about room participants\n *\n * Core Features:\n * - Human-readable participant list with smart truncation and formatting\n * - Flexible positioning (above/below) with proper arrow orientation\n * - Accessible tooltip with ARIA attributes and live region updates\n * - Customizable styling through multiple className props\n * - Theme-aware styling supporting both light and dark modes\n * - Maximum width constraint (max-w-96) with text truncation for long lists\n *\n *\n * @example\n * // Basic usage within RoomInfo hover interaction\n * <PresenceList\n *   tooltipPosition={tooltipPosition}\n * />\n *\n * @example\n * // With custom styling\n * <PresenceList\n *   tooltipPosition=\"above\"\n *   surfaceClassName=\"bg-blue-900 border-blue-700\"\n *   textClassName=\"text-blue-100 font-medium\"\n * />\n *\n *\n * @example\n * // Different presence scenarios and generated text\n * // presenceData = [] → \"No one is currently present\"\n * // presenceData = [{ clientId: \"Alice\" }] → \"Alice is present\"\n * // presenceData = [{ clientId: \"Alice\" }, { clientId: \"Bob\" }] → \"Alice, Bob are present\"\n * // presenceData = [5 members] → \"Alice, Bob, Charlie and 2 more participants are present\"\n */\n\nexport const PresenceList = ({\n  tooltipPosition,\n  coords,\n  tooltipClassName,\n  textClassName,\n  ...rest\n}: PresenceListProps) => {\n  const { presenceData } = usePresenceListener();\n  const [presenceText, setPresenceText] = useState('No one is currently present');\n\n  useEffect(() => {\n    const newText = buildPresenceSentence(presenceData);\n    setPresenceText(newText);\n  }, [presenceData]);\n\n  if (!coords) return;\n\n  return createPortal(\n    <Tooltip\n      position={tooltipPosition}\n      zIndex=\"z-50\"\n      className={clsx('fixed transform -translate-x-1/2', tooltipClassName)}\n      maxWidth=\"max-w-96\"\n      role=\"tooltip\"\n      aria-live=\"polite\"\n      style={{ top: coords.top, left: coords.left }}\n      {...rest}\n    >\n      <div className={clsx('text-center truncate', textClassName)}>{presenceText}</div>\n    </Tooltip>,\n    document.body\n  );\n};\n","import { useChatClient, usePresenceListener, useRoom, useTyping } from '@ably/chat/react';\nimport { clsx } from 'clsx';\nimport React, { useState } from 'react';\n\nimport { useRoomAvatar } from '../../hooks/use-room-avatar.tsx';\nimport { Avatar, AvatarData } from '../atoms/avatar.tsx';\nimport { ParticipantList } from './participant-list.tsx';\nimport { PresenceCount } from './presence-count.tsx';\nimport { PresenceIndicators } from './presence-indicators.tsx';\nimport { PresenceList } from './presence-list.tsx';\nimport { TypingIndicators } from './typing-indicators.tsx';\n\n/**\n * Props for the RoomInfo component\n */\nexport interface RoomInfoProps {\n  /**\n   * Optional avatar data for the room to override context-managed avatar.\n   * When provided, bypasses the AvatarContext and uses this data directly.\n   * Useful for scenarios with external avatar management or testing.\n   * If not provided, automatically fetches or creates avatar via useRoomAvatar hook.\n   *\n   * @example\n   * // Using context-managed avatar (recommended)\n   * <RoomInfo />\n   *\n   * @example\n   * // Providing custom avatar data\n   * <RoomInfo\n   *   roomAvatar={{\n   *     displayName: \"VIP Lounge\",\n   *     color: \"#FFD700\",\n   *     initials: \"VL\",\n   *     src: \"https://example.com/vip-avatar.jpg\"\n   *   }}\n   * />\n   */\n  roomAvatar?: AvatarData;\n\n  /**\n   * Position coordinates for rendering the participant list dropdown.\n   * Defines where the participant list appears relative to the viewport.\n   * Adjust based on component placement to prevent edge overflow.\n   *\n   * @default { top: 0, left: 150 }\n   *\n   * @example\n   * // Position for sidebar placement\n   * <RoomInfo\n   *   position={{ top: 60, left: 250 }}\n   * />\n   *\n   */\n  position?: { top: number; left: number };\n\n  /**\n   * Additional CSS class names to apply to the root container element.\n   * Use for spacing, sizing, positioning, and theme customizations.\n   *\n   * @example\n   * // Custom spacing and background\n   * <RoomInfo\n   *   className=\"p-4 bg-blue-50 rounded-lg shadow-sm\"\n   * />\n   *\n   * @example\n   * // Compact mobile layout\n   * <RoomInfo\n   *   className=\"px-2 py-1 gap-2\"\n   * />\n   *\n   * @example\n   * // Fixed positioning for overlays\n   * <RoomInfo\n   *   className=\"fixed top-4 left-4 bg-white shadow-lg rounded-full px-4 py-2\"\n   * />\n   *\n   * @example\n   * // Responsive design patterns\n   * <RoomInfo\n   *   className=\"flex-col md:flex-row gap-2 md:gap-3\"\n   * />\n   */\n  className?: string;\n}\n\n/**\n * RoomInfo component displays comprehensive information about a chat room with interactive features.\n * It must be used within the context of a ChatRoomProvider and AvatarProvider to function correctly.\n *\n * Features:\n * - Room avatar display\n * - Live presence count badge showing active participants\n * - Interactive hover tooltip with participant preview\n * - Expandable participant list with detailed user information\n * - In-place avatar editing with color and image customization\n * - Presence and typing indicators built-in\n * - Accessibility support with ARIA roles and keyboard navigation\n * - Customizable positioning\n *\n * Presence:\n * - Live participant count with visual badge\n * - Hover tooltip showing recent participants\n * - Detailed participant list with status indicators\n * - Current user highlighting and status\n *\n * Typing Indicators:\n * - Typing activity display\n * - Smart user exclusion (doesn't show own typing)\n * - Configurable display limits\n *\n * @example\n * // Sidebar room list item\n * const SidebarRoomItem = ({ roomName, isActive }) => {\n *   return (\n *     <div className={`room-item ${isActive ? 'active' : ''}`}>\n *       <RoomInfo\n *         className=\"px-3 py-2 hover:bg-gray-100 rounded-lg cursor-pointer\"\n *         position={{ top: 0, left: 220 }}\n *       />\n *     </div>\n *   );\n * };\n *\n *\n * @example\n * // Custom avatar with external management\n * const ExternalAvatarRoom = ({ roomName, externalAvatar }) => {\n *   return (\n *     <RoomInfo\n *       roomAvatar={{\n *         displayName: room.customName,\n *         src: externalAvatar.imageUrl,\n *         color: room.themeColor,\n *         initials: room.customName.substring(0, 2).toUpperCase()\n *       }}\n *       className=\"p-4 bg-gradient-to-r from-blue-50 to-purple-50\"\n *     />\n *   );\n * };\n *\n */\nexport const RoomInfo = ({\n  roomAvatar: propRoomAvatar,\n  position = { top: 0, left: 150 },\n  className,\n}: RoomInfoProps) => {\n  const { roomName } = useRoom();\n  const { presenceData } = usePresenceListener();\n  const { currentlyTyping } = useTyping();\n  const chatClient = useChatClient();\n  const currentClientId = chatClient.clientId;\n\n  const [showTooltip, setShowTooltip] = useState(false);\n  const [tooltipPosition, setTooltipPosition] = useState<'above' | 'below'>('above');\n  const [tooltipCoords, setTooltipCoords] = useState<{ top: number; left: number } | undefined>();\n  const [isOpen, setIsOpen] = useState(false);\n  const { roomAvatar } = useRoomAvatar({ roomName });\n  const roomAvatarData = propRoomAvatar || roomAvatar;\n\n  const onToggle = () => {\n    setShowTooltip(false); // Hide tooltip when toggling participant list\n    setIsOpen(!isOpen);\n  };\n\n  /**\n   * Handles mouse enter event on the room avatar\n   * Calculates optimal tooltip position based on available space\n   *\n   * @param event - The mouse enter event\n   */\n  const handleMouseEnter = (event: React.MouseEvent) => {\n    const rect = event.currentTarget.getBoundingClientRect();\n    const tooltipHeight = 30; // Approximate tooltip height\n    const spacing = 5; // Space between avatar and tooltip\n    const spaceAbove = rect.top;\n    const spaceBelow = window.innerHeight - rect.bottom;\n\n    // Position above if there's enough space, otherwise below\n    let finalTooltipPosition: 'above' | 'below';\n    if (spaceAbove >= tooltipHeight + spacing + 10) {\n      finalTooltipPosition = 'above';\n    } else if (spaceBelow >= tooltipHeight + spacing + 10) {\n      finalTooltipPosition = 'below';\n    } else {\n      // If neither has enough space, use the side with more space\n      finalTooltipPosition = spaceAbove > spaceBelow ? 'above' : 'below';\n    }\n\n    setTooltipPosition(finalTooltipPosition);\n\n    // Calculate coordinates for fixed positioning (viewport-relative)\n    const horizontalCenter = (rect.left + rect.right) / 2;\n    const verticalPos =\n      finalTooltipPosition === 'above' ? rect.top - tooltipHeight - spacing : rect.bottom + spacing;\n\n    setTooltipCoords({ top: verticalPos, left: horizontalCenter });\n\n    setShowTooltip(true);\n  };\n\n  return (\n    <div className={clsx('flex items-center gap-3', className)}>\n      <div className=\"relative\">\n        {/* Room Avatar with Hover Tooltip */}\n        <div\n          className=\"relative cursor-pointer\"\n          onMouseEnter={handleMouseEnter}\n          onMouseLeave={() => {\n            setShowTooltip(false);\n          }}\n          onClick={onToggle}\n          role=\"button\"\n          aria-haspopup=\"dialog\"\n          aria-expanded={isOpen}\n          aria-label={`${roomAvatarData?.displayName || roomName} (${String(presenceData.length || 0)} participants)`}\n          tabIndex={0}\n          onKeyDown={(e) => {\n            if (e.key === 'Enter' || e.key === ' ') {\n              e.preventDefault();\n              onToggle();\n            }\n          }}\n        >\n          <div className=\"relative\">\n            <Avatar\n              alt={roomAvatarData?.displayName}\n              src={roomAvatarData?.src}\n              color={roomAvatarData?.color}\n              size=\"lg\"\n              initials={roomAvatarData?.initials}\n            />\n          </div>\n\n          {/* Present Count Badge */}\n          <PresenceCount presenceData={presenceData} />\n        </div>\n\n        {/* Hover Tooltip */}\n        {showTooltip && <PresenceList tooltipPosition={tooltipPosition} coords={tooltipCoords} />}\n\n        {/* Participants Dropdown */}\n        {isOpen && (\n          <ParticipantList\n            presenceData={presenceData}\n            currentClientId={currentClientId}\n            currentlyTyping={currentlyTyping}\n            onToggle={onToggle}\n            position={position}\n          />\n        )}\n      </div>\n\n      {/* Room Information */}\n      <div className=\"flex-1\">\n        <h2 className=\"font-semibold text-gray-900 dark:text-gray-100\">\n          {roomAvatarData?.displayName || roomName}\n        </h2>\n        <div className=\"flex items-center gap-2\">\n          <PresenceIndicators />\n          {/* Typing Indicators */}\n          <TypingIndicators className=\"text-xs\" />\n        </div>\n      </div>\n    </div>\n  );\n};\n","import { useCallback, useEffect, useRef } from 'react';\n\n/**\n * Custom hook for throttling function calls with trailing execution\n *\n * @param fn - The function to throttle\n * @param delay - The throttle delay in milliseconds\n * @returns A throttled version of the function\n *\n * Features:\n * - Throttles function calls to at most once per delay period\n * - Ensures trailing execution: if calls happen during throttle window,\n *   the last call executes at the end of the window\n * - Proper cleanup on component unmount\n * - Handles rapid successive calls by using the latest arguments\n *\n * @example\n * ```tsx\n * const sendReaction = useCallback(async (emoji: string) => {\n *   await send({ type: emoji });\n * }, [send]);\n *\n * const throttledSend = useThrottle(sendReaction, 200);\n *\n * // Usage\n * throttledSend('👍'); // Will execute at end of 200ms window\n * throttledSend('❤️'); // Will override previous call\n * throttledSend('😂'); // Will execute with '😂' after 200ms\n * ```\n */\nexport function useThrottle<Args extends unknown[], R>(\n  fn: (...args: Args) => R,\n  delay: number\n): (...args: Args) => R {\n  const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);\n  const lastArgsRef = useRef<Args | undefined>(undefined);\n  const isThrottledRef = useRef(false);\n\n  // Cleanup function to clear pending timeouts\n  const cleanup = useCallback(() => {\n    if (timeoutRef.current) {\n      clearTimeout(timeoutRef.current);\n      timeoutRef.current = undefined;\n    }\n    lastArgsRef.current = undefined;\n    isThrottledRef.current = false;\n  }, []);\n\n  // Cleanup on unmount\n  useEffect(() => {\n    return cleanup;\n  }, [cleanup]);\n\n  return useCallback(\n    (...args: Args) => {\n      lastArgsRef.current = args;\n\n      if (!isThrottledRef.current) {\n        isThrottledRef.current = true;\n\n        timeoutRef.current = setTimeout(() => {\n          if (lastArgsRef.current) {\n            fn(...lastArgsRef.current);\n            lastArgsRef.current = undefined;\n          }\n          isThrottledRef.current = false;\n          timeoutRef.current = undefined;\n        }, delay);\n      }\n\n      // If we're already in a throttle period, the timeout will use the updated lastArgsRef\n      // This ensures the latest call's arguments are used when the throttle period ends\n      return undefined as unknown as R;\n    },\n    [fn, delay]\n  );\n}\n","import React, { useEffect, useState } from 'react';\n\n/**\n * Props for the EmojiBurst component\n */\nexport interface EmojiBurstProps {\n  /**\n   * Whether the burst animation is currently active.\n   * When set to true, starts a new burst animation from the specified position.\n   * Setting to false will not stop an ongoing animation - use onComplete for cleanup.\n   */\n  isActive: boolean;\n\n  /**\n   * The position where the burst should originate from, in viewport coordinates.\n   * Typically obtained from element.getBoundingClientRect() or mouse event coordinates.\n   *\n   * @example\n   * ```tsx\n   * // From button center\n   * const rect = buttonRef.current.getBoundingClientRect();\n   * const position = {\n   *   x: rect.left + rect.width / 2,\n   *   y: rect.top + rect.height / 2\n   * };\n   *\n   * // From mouse click\n   * const position = { x: event.clientX, y: event.clientY };\n   * ```\n   */\n  position: { x: number; y: number };\n\n  /**\n   * The emoji to display in the burst animation.\n   * Special behavior: When set to '👍', automatically uses diverse skin tone variants.\n   *\n   * @default \"👍\"\n   *\n   * @example\n   * ```tsx\n   * emoji=\"🎉\"  // Party celebration\n   * emoji=\"❤️\"  // Love reaction\n   * emoji=\"😂\"  // Laughter\n   * emoji=\"👍\"  // Uses skin tone variants: 👍, 👍🏻, 👍🏽, 👍🏿\n   * ```\n   */\n  emoji?: string;\n\n  /**\n   * Duration of the burst animation in milliseconds.\n   * Controls how long emojis remain visible before fading out completely.\n   * Longer durations allow emojis to travel further and fade more gradually.\n   *\n   *\n   * - Animation uses requestAnimationFrame for smooth 60fps performance\n   * - Emojis begin fading after 2/3 of the duration\n   */\n  duration: number;\n\n  /**\n   * Callback function called when the animation completes and all emojis have faded out.\n   * Use this to clean up state, typically by setting isActive to false.\n   *\n   * @example\n   * ```tsx\n   * onComplete={() => {\n   *   setBurstActive(false);\n   *   // Optional: trigger other effects\n   *   playSound('burst-complete');\n   * }}\n   * ```\n   */\n  onComplete: () => void;\n}\n\n/**\n * Internal interface representing a single emoji in the burst animation\n */\ninterface FlyingEmoji {\n  /** Unique identifier for the emoji */\n  id: number;\n  /** The emoji character to display */\n  emoji: string;\n  /** Current x-coordinate position */\n  x: number;\n  /** Current y-coordinate position */\n  y: number;\n  /** Velocity in the x direction */\n  vx: number;\n  /** Velocity in the y direction */\n  vy: number;\n  /** Current rotation angle in degrees */\n  rotation: number;\n  /** Speed of rotation */\n  rotationSpeed: number;\n  /** Current opacity value (0-1) */\n  opacity: number;\n  /** Current scale factor */\n  scale: number;\n}\n\n/**\n * EmojiBurst component creates an animated burst of emoji characters\n *\n * Features:\n * - Creates a circular burst of emojis with different skin tones (for thumbs-up) or the specified emoji\n * - Animates emojis with physics-based motion (velocity, gravity, rotation)\n * - Automatically fades out and cleans up after animation completes\n * - Non-interactive visual effect (pointer-events-none)\n * - Randomized trajectories and rotation for natural movement\n * - Adaptive emoji variants for inclusive representation\n *\n * @example\n * // Basic reaction burst on button click\n * const [burstActive, setBurstActive] = useState(false);\n * const [burstPosition, setBurstPosition] = useState({ x: 0, y: 0 });\n * const [burstDuration] = useState(2000); // 2 seconds\n *\n * const handleReaction = (event: React.MouseEvent) => {\n *   const rect = event.currentTarget.getBoundingClientRect();\n *   setBurstPosition({\n *     x: rect.left + rect.width / 2,\n *     y: rect.top + rect.height / 2\n *   });\n *   setBurstActive(true);\n * };\n *\n * return (\n *   <>\n *     <button onClick={handleReaction}>👍 Like</button>\n *     <EmojiBurst\n *       isActive={burstActive}\n *       position={burstPosition}\n *       duration={burstDuration}\n *       onComplete={() => setBurstActive(false)}\n *     />\n *   </>\n * );\n *\n * @example\n * // Custom emoji with longer duration\n * <EmojiBurst\n *   isActive={showCelebration}\n *   position={{ x: window.innerWidth / 2, y: window.innerHeight / 2 }}\n *   emoji=\"🎉\"\n *   duration={3000}\n *   onComplete={() => setShowCelebration(false)}\n * />\n *\n * @example\n * // Triggered from mouse coordinates\n * const handleEmojiReaction = (event: MouseEvent, selectedEmoji: string) => {\n *   setBurstPosition({ x: event.clientX, y: event.clientY });\n *   setBurstEmoji(selectedEmoji);\n *   setBurstActive(true);\n * };\n *\n * <EmojiBurst\n *   isActive={burstActive}\n *   position={burstPosition}\n *   emoji={burstEmoji}\n *   onComplete={() => setBurstActive(false)}\n * />\n */\nexport const EmojiBurst = ({\n  isActive,\n  position,\n  emoji = '👍',\n  duration,\n  onComplete,\n}: EmojiBurstProps) => {\n  const [emojis, setEmojis] = useState<FlyingEmoji[]>([]);\n\n  useEffect(() => {\n    if (!isActive) return;\n\n    // Create burst of emojis\n    const newEmojis: FlyingEmoji[] = [];\n\n    // Use skin tone variations for thumbs-up, otherwise use the provided emoji\n    const emojiVariants = emoji === '👍' ? ['👍', '👍🏻', '👍🏽', '👍🏿'] : [emoji];\n\n    for (let i = 0; i < 12; i++) {\n      const angle = (i / 12) * Math.PI * 2;\n      const speed = 3 + Math.random() * 4;\n\n      newEmojis.push({\n        id: i,\n        emoji: emojiVariants[Math.floor(Math.random() * emojiVariants.length)] || emoji,\n        x: position.x,\n        y: position.y,\n        vx: Math.cos(angle) * speed,\n        vy: Math.sin(angle) * speed - 2, // Slight upward bias\n        rotation: 0,\n        rotationSpeed: (Math.random() - 0.5) * 20,\n        opacity: 1,\n        scale: 0.8 + Math.random() * 0.4,\n      });\n    }\n\n    setEmojis(newEmojis);\n\n    // Animation loop\n    let animationFrame: number;\n    const startTime = Date.now();\n\n    const animate = () => {\n      const elapsed = Date.now() - startTime;\n      const progress = elapsed / duration;\n\n      if (progress >= 1) {\n        setEmojis([]);\n        onComplete();\n        return;\n      }\n\n      setEmojis((currentEmojis) =>\n        currentEmojis.map((emoji) => ({\n          ...emoji,\n          x: emoji.x + emoji.vx,\n          y: emoji.y + emoji.vy,\n          vy: emoji.vy + 0.3, // Gravity\n          rotation: emoji.rotation + emoji.rotationSpeed,\n          opacity: Math.max(0, 1 - progress * 1.5), // Fade out\n          scale: emoji.scale * (1 - progress * 0.3), // Shrink slightly\n        }))\n      );\n\n      animationFrame = requestAnimationFrame(animate);\n    };\n\n    animate();\n\n    return () => {\n      if (animationFrame) {\n        cancelAnimationFrame(animationFrame);\n      }\n    };\n  }, [isActive, position, emoji, duration, onComplete]);\n\n  if (!isActive || emojis.length === 0) return;\n\n  return (\n    <div className=\"fixed inset-0 pointer-events-none z-50\" aria-hidden=\"true\" role=\"presentation\">\n      {emojis.map((emoji) => (\n        <div\n          key={emoji.id}\n          className=\"absolute text-2xl select-none\"\n          style={{\n            left: emoji.x - 12,\n            top: emoji.y - 12,\n            transform: `rotate(${String(emoji.rotation)}deg) scale(${String(emoji.scale)})`,\n            opacity: emoji.opacity,\n            transition: 'none',\n          }}\n          aria-hidden=\"true\"\n        >\n          {emoji.emoji}\n        </div>\n      ))}\n    </div>\n  );\n};\n","import React, { useEffect, useState } from 'react';\n\nimport { Icon } from '../atoms/icon.tsx';\n\n// Default set of emoji reactions for the wheel\nconst DEFAULT_EMOJIS = ['👍', '❤️', '😂', '😮', '😢', '😡', '👏', '🎉'];\n\n/**\n * Props for the EmojiWheel component\n */\nexport interface EmojiWheelProps {\n  /**\n   * Whether the emoji wheel is currently visible and usable.\n   * Controls both visibility and animation states.\n   * When transitioning from true to false, exit animation plays before hiding.\n   */\n  isOpen: boolean;\n\n  /**\n   * Position where the wheel should appear, in viewport coordinates.\n   * The wheel automatically adjusts position to stay within viewport boundaries.\n   *\n   * - Wheel centers itself on the provided coordinates\n   * - Automatically repositioned if it would extend beyond viewport edges\n   * - Ensures the wheel is fully visible on screen\n   * - Total wheel diameter: ~208px (80px radius + 48px button + padding)\n   *\n   * @example\n   * ```tsx\n   * // From mouse event\n   * position={{ x: event.clientX, y: event.clientY }}\n   *\n   * // From element center\n   * const rect = element.getBoundingClientRect();\n   * position={{\n   *   x: rect.left + rect.width / 2,\n   *   y: rect.top + rect.height / 2\n   * }}\n   *\n   * // From touch event\n   * const touch = event.touches[0];\n   * position={{ x: touch.clientX, y: touch.clientY }}\n   * ```\n   */\n  position: { x: number; y: number };\n\n  /**\n   * Optional list of emojis to display on the wheel.\n   * If not provided, a default set of 8 emojis will be used.\n   *\n   * For optimal user experience, it's recommended to use exactly 8 emojis,\n   * as the wheel is designed to display this number in a circular arrangement.\n   * Using fewer or more emojis may affect the visual layout and usability.\n   *\n   * @example\n   * ```tsx\n   * // Custom emoji set\n   * emojis={['🔥', '🚀', '👀', '🙌', '💯', '🎯', '🌟', '✨']}\n   * ```\n   */\n  emojis?: string[];\n\n  /**\n   * Callback function triggered when an emoji is selected from the wheel.\n   * Receives the selected emoji character as a string parameter.\n   * The wheel does not automatically close after selection.\n   *\n   * @param emoji - The selected emoji from the set (default or custom)\n   *\n   * @remarks\n   * The callback should handle:\n   * - Adding the reaction to your data model\n   * - Closing the wheel (by setting isOpen to false)\n   * - Any additional UI updates or animations\n   *\n   * @example\n   * ```tsx\n   * onEmojiSelect={(emoji) => {\n   *   // Add reaction to message\n   *   sendReaction(emoji);\n   *\n   *   // Close the wheel\n   *   setWheelOpen(false);\n   *\n   *   // Optional: Show feedback\n   *   showToast(`Reacted with ${emoji}`);\n   * }}\n   * ```\n   */\n  onEmojiSelect: (emoji: string) => void;\n\n  /**\n   * Callback function triggered when the wheel should be closed.\n   * Called when user clicks outside the wheel, clicks the center close button,\n   * or triggers other dismissal actions.\n   *\n   * To dismiss the wheel, you can:\n   * - Clicking outside the wheel area\n   * - Clicking the center close button\n   * - Programmatic closure (escape key, etc.)\n   *\n   * This callback should update parent state to set isOpen to false.\n   * The component handles exit animations automatically.\n   *\n   * @example\n   * ```tsx\n   * onClose={() => {\n   *   setWheelOpen(false);\n   *   // Optional: cleanup or additional actions\n   *   clearSelection();\n   * }}\n   * ```\n   */\n  onClose: () => void;\n}\n\n/**\n * EmojiWheel component displays a circular selection of emoji reactions\n *\n * Features:\n * - Circular arrangement of 8 emoji reactions\n * - Animated appearance with scaling and rotation\n * - Click outside to close\n * - Hover effects for better UX\n * - Optimized for touch and mouse interaction\n * - Safe positioning prevents off-screen rendering\n * - Staggered animation entrance for visual appeal\n * - Center close button for easy dismissal\n *\n *\n * @example\n * // Quick reaction button in chat interface\n * const [showReactionWheel, setShowReactionWheel] = useState(false);\n * const reactionButtonRef = useRef<HTMLButtonElement>(null);\n *\n * const handleReactionClick = () => {\n *   if (reactionButtonRef.current) {\n *     const rect = reactionButtonRef.current.getBoundingClientRect();\n *     setWheelPosition({\n *       x: rect.left + rect.width / 2,\n *       y: rect.top + rect.height / 2\n *     });\n *     setShowReactionWheel(true);\n *   }\n * };\n *\n * <button\n *   ref={reactionButtonRef}\n *   onClick={handleReactionClick}\n *   className=\"reaction-trigger\"\n * >\n *   😀 React\n * </button>\n *\n * @example\n * // Touch-optimized mobile usage\n * const handleTouchStart = (event: React.TouchEvent) => {\n *   const touch = event.touches[0];\n *   setWheelPosition({ x: touch.clientX, y: touch.clientY });\n *   setWheelOpen(true);\n * };\n *\n * <div\n *   onTouchStart={handleTouchStart}\n *   className=\"touch-target\"\n * >\n *   Hold to react\n * </div>\n */\nexport const EmojiWheel = ({\n  isOpen,\n  position,\n  emojis: customEmojis,\n  onEmojiSelect,\n  onClose,\n}: EmojiWheelProps) => {\n  const [isAnimating, setIsAnimating] = useState(false);\n\n  // Use custom emojis or fall back to default set\n  const emojis = customEmojis || DEFAULT_EMOJIS;\n\n  useEffect(() => {\n    if (isOpen) {\n      setIsAnimating(true);\n      // Add click outside listener\n      const handleClickOutside = (event: MouseEvent) => {\n        const target = event.target as Element;\n        if (!target.closest('[data-emoji-wheel]')) {\n          onClose();\n        }\n      };\n\n      document.addEventListener('mousedown', handleClickOutside);\n      return () => {\n        document.removeEventListener('mousedown', handleClickOutside);\n      };\n    } else {\n      // Delay hiding to allow exit animation\n      const timer = setTimeout(() => {\n        setIsAnimating(false);\n      }, 200);\n      return () => {\n        clearTimeout(timer);\n      };\n    }\n  }, [isOpen, onClose]);\n\n  // Force re-render on window resize to ensure proper positioning\n  const [, setWindowSize] = useState({ width: window.innerWidth, height: window.innerHeight });\n\n  useEffect(() => {\n    if (!isOpen && !isAnimating) return;\n\n    const handleResize = () => {\n      setWindowSize({ width: window.innerWidth, height: window.innerHeight });\n    };\n\n    window.addEventListener('resize', handleResize);\n    return () => {\n      window.removeEventListener('resize', handleResize);\n    };\n  }, [isOpen, isAnimating]);\n\n  if (!isOpen && !isAnimating) return;\n\n  const radius = 80; // Distance from center to emoji buttons\n  const buttonSize = 48; // Size of each emoji button\n  const wheelSize = (radius + buttonSize) * 2; // Total wheel size\n\n  // Calculate safe position to prevent wheel from going off-screen\n  const minMargin = 20; // Minimum margin from screen edges\n  const safePosition = {\n    x: Math.max(\n      wheelSize / 2 + minMargin,\n      Math.min(window.innerWidth - wheelSize / 2 - minMargin, position.x)\n    ),\n    y: Math.max(\n      wheelSize / 2 + minMargin,\n      Math.min(window.innerHeight - wheelSize / 2 - minMargin, position.y)\n    ),\n  };\n\n  return (\n    <div\n      className=\"fixed inset-0 z-50 pointer-events-none\"\n      role=\"dialog\"\n      aria-label=\"Emoji reaction selector\"\n    >\n      {/* Backdrop */}\n      <div\n        className={`absolute inset-0 transition-opacity duration-200 pointer-events-auto ${\n          isOpen ? 'opacity-30' : 'opacity-0'\n        }`}\n        style={{ backgroundColor: 'rgba(0, 0, 0, 0.2)' }}\n        onClick={onClose}\n      />\n\n      {/* Emoji Wheel Container */}\n      <div\n        data-emoji-wheel=\"true\"\n        className={`absolute pointer-events-auto transition-all duration-300 ease-out ${\n          isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-50'\n        }`}\n        style={{\n          left: safePosition.x - wheelSize / 2,\n          top: safePosition.y - wheelSize / 2,\n          width: wheelSize,\n          height: wheelSize,\n        }}\n      >\n        {/* Center background circle */}\n        <div className=\"absolute inset-0 rounded-full bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm shadow-2xl border border-gray-200 dark:border-gray-600\" />\n\n        {/* Center close button */}\n        <button\n          onClick={onClose}\n          className=\"absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors flex items-center justify-center shadow-md z-10\"\n          aria-label=\"Close emoji selector\"\n        >\n          <Icon name=\"close\" size=\"sm\" />\n        </button>\n\n        {/* Emoji buttons arranged in circle */}\n        {emojis.map((emoji, index) => {\n          const angle = (index / emojis.length) * 2 * Math.PI - Math.PI / 2; // Start from top\n          const x = Math.cos(angle) * radius;\n          const y = Math.sin(angle) * radius;\n\n          return (\n            <button\n              key={emoji}\n              onClick={() => {\n                onEmojiSelect(emoji);\n              }}\n              className={`absolute rounded-full bg-white dark:bg-gray-800 shadow-lg hover:shadow-xl hover:scale-125 transition-all duration-200 flex items-center justify-center text-2xl border-2 border-gray-200 dark:border-gray-600 hover:border-blue-400 dark:hover:border-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 ${\n                isOpen ? 'animate-pulse' : ''\n              }`}\n              style={{\n                width: buttonSize,\n                height: buttonSize,\n                left: '50%',\n                top: '50%',\n                transform: `translate(${String(x - buttonSize / 2)}px, ${String(y - buttonSize / 2)}px)`,\n                animationDelay: `${String(index * 75)}ms`,\n                animationDuration: '800ms',\n                animationIterationCount: '1',\n                animationFillMode: 'both',\n              }}\n              aria-label={`React with ${emoji}`}\n            >\n              {emoji}\n            </button>\n          );\n        })}\n      </div>\n    </div>\n  );\n};\n","import { ErrorInfo, RoomReactionEvent } from '@ably/chat';\nimport { useRoomReactions } from '@ably/chat/react';\nimport { clsx } from 'clsx';\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { useThrottle } from '../../hooks/use-throttle.tsx';\nimport { EmojiBurst } from './emoji-burst.tsx';\nimport { EmojiWheel } from './emoji-wheel.tsx';\n\n/**\n * Props for the RoomReaction component\n */\nexport interface RoomReactionProps {\n  /**\n   * Duration for the emoji burst animation in milliseconds.\n   * Controls how long the emoji animation is visible before automatically hiding.\n   * Longer durations provide more noticeable feedback but may feel slower.\n   * @default 500\n   */\n  emojiBurstDuration?: number;\n\n  /**\n   * Fixed position for the burst animation when receiving reactions from others.\n   * When provided, all incoming reactions will animate at this position.\n   * When not provided, incoming reactions animate at screen center.\n   * Own reactions always animate from the button position\n   *\n   * @example\n   * // Fixed position in top-right corner\n   * emojiBurstPosition={{ x: window.innerWidth - 100, y: 100 }}\n   *\n   * @example\n   * // Center of a specific element\n   * const rect = element.getBoundingClientRect();\n   * emojiBurstPosition={{\n   *   x: rect.left + rect.width / 2,\n   *   y: rect.top + rect.height / 2\n   * }}\n   */\n  emojiBurstPosition?: { x: number; y: number };\n\n  /**\n   * Additional CSS classes to apply to the reaction button container.\n   * Allows customization of spacing, alignment, and styling.\n   * Merged with default padding and container classes using clsx.\n   *\n   * @example\n   * // Custom spacing and positioning\n   * className=\"px-6 py-2 fixed bottom-4 right-4\"\n   *\n   * @example\n   * // Integration with flex layouts\n   * className=\"flex-shrink-0 ml-auto\"\n   */\n  className?: string;\n\n  /**\n   * Custom error handling configuration for room reaction operations.\n   * Provides hooks for handling specific error scenarios instead of default console logging.\n   * All handlers are optional and will fall back to console.error if not provided.\n   *\n   * @example\n   * ```tsx\n   * const onError = {\n   *   onSendRoomReactionError: (error, emoji) => {\n   *     toast.error(`Failed to send ${emoji} reaction: ${error.message}`);\n   *     console.error('Room reaction error:', error);\n   *   }\n   * };\n   *\n   * <RoomReaction onError={onError} />\n   * ```\n   */\n  onError?: {\n    /**\n     * Called when sending a room reaction fails.\n     * Provides the error object and the emoji that failed to send.\n     *\n     * @param error - The error that occurred while sending the reaction\n     * @param emoji - The emoji that failed to send\n     */\n    onSendRoomReactionError?: (error: ErrorInfo, emoji: string) => void;\n  };\n}\n\n/**\n * RoomReaction component provides ephemeral room reaction functionality for chat rooms\n *\n * Core Features:\n * - Quick reaction button with customizable default emoji (starts with 👍)\n * - Long press (500ms) to open circular emoji selection wheel\n * - Selected emoji becomes new default for subsequent quick reactions\n * - Immediate visual feedback with emoji burst animations\n * - Throttled sending (max 1 reaction per 200ms)\n * - Handles both outgoing and incoming room reactions\n * - Mobile-friendly with touch event support and haptic feedback\n * - Accessible with proper ARIA labels and keyboard interaction\n *\n * Interaction:\n * • Short click/tap: Sends current default emoji immediately\n * • Long press/hold: Opens emoji wheel for selection\n * • Emoji wheel selection: Updates default and sends chosen emoji\n * • Click outside wheel: Closes wheel without sending\n *\n *\n * Room Reactions vs Message Reactions:\n * Room reactions are ephemeral like typing indicators - they provide momentary\n * feedback without being stored in chat history. They're useful for quick\n * acknowledgments, applause, or ambient reactions during conversations.\n *\n * @example\n * // With custom animation settings\n * <RoomReaction\n *   emojiBurstDuration={1000}\n *   emojiBurstPosition={{ x: 400, y: 300 }}\n * />\n *\n * @example\n * // Custom positioning and styling\n * <RoomReaction\n *   className=\"fixed bottom-4 right-4 bg-white rounded-full shadow-lg p-2\"\n *   emojiBurstDuration={750}\n * />\n *\n * @example\n * // With custom error handling\n * <RoomReaction\n *   onError={{\n *     onSendRoomReactionError: (error, emoji) => {\n *       toast.error(`Failed to send ${emoji} reaction: ${error.message}`);\n *       console.error('Room reaction error:', error);\n *     }\n *   }}\n * />\n *\n */\nexport const RoomReaction = ({\n  emojiBurstDuration = 500,\n  emojiBurstPosition: initialEmojiBurstPosition,\n  className,\n  onError,\n}: RoomReactionProps) => {\n  const [showEmojiBurst, setShowEmojiBurst] = useState(false);\n  const [emojiBurstPosition, setEmojiBurstPosition] = useState(\n    initialEmojiBurstPosition || { x: 0, y: 0 }\n  );\n  const [burstEmoji, setBurstEmoji] = useState('👍');\n  const [showEmojiWheel, setShowEmojiWheel] = useState(false);\n  const [emojiWheelPosition, setEmojiWheelPosition] = useState({ x: 0, y: 0 });\n  const [defaultEmoji, setDefaultEmoji] = useState('👍'); // Track current default emoji\n\n  const reactionButtonRef = useRef<HTMLButtonElement>(null);\n  const longPressTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);\n  const isLongPressRef = useRef(false);\n\n  /**\n   * Handles incoming room reactions from other users.\n   * This function will be throttled to prevent excessive UI updates.\n   *\n   * @param reaction - The incoming room reaction event\n   */\n  const handleIncomingReaction = useCallback(\n    (reaction: RoomReactionEvent) => {\n      if (reaction.reaction.isSelf) {\n        // If the reaction is from ourselves, we don't need to show the burst animation\n        // (we already showed it locally for immediate feedback)\n        return;\n      }\n\n      // Set the emoji and show burst for incoming reactions\n      setBurstEmoji(reaction.reaction.name);\n\n      // Use provided position or default to screen center for incoming reactions\n      if (initialEmojiBurstPosition) {\n        setEmojiBurstPosition(initialEmojiBurstPosition);\n      } else {\n        // Show burst in the screen center for incoming reactions\n        setEmojiBurstPosition({\n          x: window.innerWidth / 2, // horizontal center\n          y: window.innerHeight / 2, // vertical center\n        });\n      }\n\n      setShowEmojiBurst(true);\n    },\n    [initialEmojiBurstPosition]\n  );\n\n  const throttledHandleIncomingReaction = useThrottle(handleIncomingReaction, 200);\n\n  const { sendRoomReaction } = useRoomReactions({\n    listener: throttledHandleIncomingReaction,\n  });\n\n  /**\n   * Shows the local burst animation at the button position.\n   * This provides immediate visual feedback regardless of network throttling.\n   * Always animates from the button location for own reactions.\n   *\n   * @param emoji - The emoji character to animate in the burst\n   */\n  const showLocalBurst = useCallback((emoji: string) => {\n    const button = reactionButtonRef.current;\n    if (button) {\n      const rect = button.getBoundingClientRect();\n      setBurstEmoji(emoji);\n      setEmojiBurstPosition({\n        x: rect.left + rect.width / 2,\n        y: rect.top + rect.height / 2,\n      });\n      setShowEmojiBurst(true);\n    }\n  }, []);\n\n  /**\n   * Sends a room reaction to all participants (without throttling).\n   * This is the base function that communicates with Ably Chat.\n   * Will be wrapped by useThrottle to prevent excessive API calls.\n   *\n   * @param emoji - The emoji reaction to send to the room\n   */\n  const sendReactionToRoom = useCallback(\n    (emoji: string): void => {\n      sendRoomReaction({ name: emoji }).catch((error: unknown) => {\n        if (onError?.onSendRoomReactionError) {\n          onError.onSendRoomReactionError(error as ErrorInfo, emoji);\n        } else {\n          console.error('Failed to send room reaction:', error);\n        }\n      });\n    },\n    [sendRoomReaction, onError]\n  );\n\n  // Create throttled version of the send function to avoid excessive network calls\n  // Limits to maximum 1 reaction per 200ms while preserving immediate visual feedback\n  const throttledSendReaction = useThrottle(sendReactionToRoom, 200);\n\n  /**\n   * Handles sending a room reaction with immediate visual feedback and throttled network call.\n   * Shows local animation instantly, then sends throttled network request.\n   * This pattern ensures responsive UX even with network throttling.\n   *\n   * @param emoji - The emoji reaction to send and animate\n   */\n  const sendReaction = useCallback(\n    (emoji: string) => {\n      // Always show local burst for immediate feedback\n      showLocalBurst(emoji);\n\n      // Send throttled network request\n      throttledSendReaction(emoji);\n    },\n    [showLocalBurst, throttledSendReaction]\n  );\n\n  /**\n   * Handles clicking the reaction button (short press).\n   * Sends the current default emoji reaction if this wasn't part of a long press.\n   * Long press detection prevents accidental reactions when opening emoji wheel.\n   */\n  const handleReactionClick = useCallback(() => {\n    // Only send reaction if this wasn't a long press\n    if (!isLongPressRef.current) {\n      sendReaction(defaultEmoji);\n    }\n    // Reset long press flag for next interaction\n    isLongPressRef.current = false;\n  }, [sendReaction, defaultEmoji]);\n\n  /**\n   * Handles starting a potential long press interaction.\n   * Sets up timer to detect long press (500ms threshold).\n   * When timer completes, opens emoji wheel at button position.\n   */\n  const handleMouseDown = useCallback(() => {\n    isLongPressRef.current = false;\n    longPressTimerRef.current = setTimeout(() => {\n      isLongPressRef.current = true;\n\n      // Show emoji wheel at button position\n      const button = reactionButtonRef.current;\n      if (button) {\n        const rect = button.getBoundingClientRect();\n        setEmojiWheelPosition({\n          x: rect.left + rect.width / 2,\n          y: rect.top + rect.height / 2,\n        });\n        setShowEmojiWheel(true);\n\n        // Add haptic feedback if available (mobile devices)\n        navigator.vibrate(50);\n      }\n    }, 500); // 500ms threshold for long press detection\n  }, []);\n\n  /**\n   * Handles ending a potential long press interaction.\n   * Clears the long press timer to prevent wheel from opening.\n   * Called on mouse up, mouse leave, touch end events.\n   */\n  const handleMouseUp = useCallback(() => {\n    if (longPressTimerRef.current) {\n      clearTimeout(longPressTimerRef.current);\n      longPressTimerRef.current = undefined;\n    }\n  }, []);\n\n  /**\n   * Handles touch start for mobile long press detection.\n   * Delegates to mouse down handler for unified behavior.\n   */\n  const handleTouchStart = useCallback(() => {\n    handleMouseDown();\n  }, [handleMouseDown]);\n\n  /**\n   * Handles touch end for mobile long press detection.\n   * Delegates to mouse up handler for unified behavior.\n   */\n  const handleTouchEnd = useCallback(() => {\n    handleMouseUp();\n  }, [handleMouseUp]);\n\n  /**\n   * Handles emoji selection from the emoji wheel.\n   * Updates the default emoji for future quick reactions and sends the selected emoji.\n   * Closes the wheel after selection completes.\n   *\n   * @param emoji - The emoji selected from the wheel\n   */\n  const handleEmojiSelect = useCallback(\n    (emoji: string) => {\n      setShowEmojiWheel(false);\n      setDefaultEmoji(emoji); // Update default emoji for future quick reactions\n      sendReaction(emoji);\n    },\n    [sendReaction]\n  );\n\n  /**\n   * Handles closing the emoji wheel without selecting an emoji.\n   * Called when clicking outside the wheel or pressing escape.\n   */\n  const handleEmojiWheelClose = useCallback(() => {\n    setShowEmojiWheel(false);\n  }, []);\n\n  /**\n   * Updates the emoji wheel position when the window is resized\n   * Ensures the wheel stays properly positioned relative to the button\n   */\n  useEffect(() => {\n    if (!showEmojiWheel) return;\n\n    const handleResize = () => {\n      const button = reactionButtonRef.current;\n      if (button) {\n        const rect = button.getBoundingClientRect();\n        setEmojiWheelPosition({\n          x: rect.left + rect.width / 2,\n          y: rect.top + rect.height / 2,\n        });\n      }\n    };\n\n    window.addEventListener('resize', handleResize);\n    return () => {\n      window.removeEventListener('resize', handleResize);\n    };\n  }, [showEmojiWheel]);\n\n  /**\n   * Callback when the emoji burst animation completes.\n   * Hides the animation component to clean up the UI.\n   */\n  const handleEmojiBurstComplete = useCallback(() => {\n    setShowEmojiBurst(false);\n  }, []);\n\n  return (\n    <div className={clsx('px-4 py-4', className)}>\n      {/* Reaction Button */}\n      <button\n        ref={reactionButtonRef}\n        className=\"inline-flex items-center justify-center px-3 py-1.5 text-sm rounded-md text-gray-500 hover:text-yellow-500 dark:text-gray-400 dark:hover:text-yellow-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors select-none\"\n        onClick={handleReactionClick}\n        onMouseDown={handleMouseDown}\n        onMouseUp={handleMouseUp}\n        onMouseLeave={handleMouseUp}\n        onTouchStart={handleTouchStart}\n        onTouchEnd={handleTouchEnd}\n        aria-label={`Send ${defaultEmoji} reaction (long press for more options)`}\n        type=\"button\"\n      >\n        <span className=\"text-xl\" aria-hidden=\"true\">\n          {defaultEmoji}\n        </span>\n      </button>\n\n      {/* Emoji Selection Wheel */}\n      <EmojiWheel\n        isOpen={showEmojiWheel}\n        position={emojiWheelPosition}\n        onEmojiSelect={handleEmojiSelect}\n        onClose={handleEmojiWheelClose}\n      />\n\n      {/* Emoji Burst Animation */}\n      <EmojiBurst\n        isActive={showEmojiBurst}\n        position={emojiBurstPosition}\n        emoji={burstEmoji}\n        duration={emojiBurstDuration}\n        onComplete={handleEmojiBurstComplete}\n      />\n    </div>\n  );\n};\n","/**\n * ThemeContext provides theme state management (light/dark mode) across the application.\n *\n * @module ThemeContext\n */\nimport { createContext } from 'react';\n\nimport { ThemeChangeCallback, ThemeType } from '../providers/theme-provider.tsx';\n\n/**\n * Shape of the ThemeContext value providing comprehensive theme management\n */\nexport interface ThemeContextType {\n  /**\n   * Current active theme\n   */\n  theme: ThemeType;\n\n  /**\n   * Toggles between light and dark themes\n   */\n  toggleTheme: () => void;\n\n  /**\n   * Sets a specific theme\n   * @param theme - The theme to set\n   */\n  setTheme: (theme: ThemeType) => void;\n\n  /**\n   * Indicates if the current theme is dark\n   */\n  isDark: boolean;\n\n  /**\n   * Indicates if the current theme is light\n   */\n  isLight: boolean;\n\n  /**\n   * Indicates if system theme detection is supported\n   */\n  supportsSystemTheme: boolean;\n\n  /**\n   * Gets the current system theme preference\n   * @returns The system theme preference or undefined if not available\n   */\n  getSystemTheme: () => ThemeType | undefined;\n\n  /**\n   * Resets theme to system preference (if available) or default\n   */\n  resetToSystemTheme: () => void;\n\n  /**\n   * Registers a callback for theme change events\n   * @param callback - Function to call when theme changes\n   * @returns Cleanup function to remove the callback\n   */\n  onThemeChange: (callback: ThemeChangeCallback) => () => void;\n}\n\n/**\n * React context for comprehensive theme management\n * Provides theme state, persistence, and system integration\n */\nexport const ThemeContext = createContext<ThemeContextType | undefined>(undefined);\n","import { useContext } from 'react';\n\nimport { ThemeContext, ThemeContextType } from '../context/theme-context.tsx';\n\n/**\n * Hook to access the theme context with comprehensive theme management\n *\n * Provides access to current theme state, theme switching functionality,\n * and system theme integration capabilities.\n *\n * @returns The theme context value with all theme management methods\n * @throws Error if used outside of a ThemeProvider\n *\n * @example\n * // Basic theme usage\n * const { theme, toggleTheme } = useTheme();\n *\n * return (\n *   <button\n *     onClick={toggleTheme}\n *     className={theme === 'dark' ? 'bg-gray-800' : 'bg-white'}\n *   >\n *     Switch to {theme === 'dark' ? 'Light' : 'Dark'} Mode\n *   </button>\n * );\n *\n * @example\n * // Using boolean helpers\n * const { isDark, isLight, setTheme } = useTheme();\n *\n * return (\n *   <div className={isDark ? 'dark-mode' : 'light-mode'}>\n *     <button onClick={() => setTheme('light')}>\n *       Light Mode\n *     </button>\n *     <button onClick={() => setTheme('dark')}>\n *       Dark Mode\n *     </button>\n *   </div>\n * );\n *\n * @example\n * // System theme integration\n * const { supportsSystemTheme, getSystemTheme, resetToSystemTheme } = useTheme();\n *\n * return (\n *   <div>\n *     {supportsSystemTheme && (\n *       <button onClick={resetToSystemTheme}>\n *         Use System Theme ({getSystemTheme()})\n *       </button>\n *     )}\n *   </div>\n * );\n *\n * @example\n * // Theme change notifications\n * const { onThemeChange } = useTheme();\n *\n * useEffect(() => {\n *   const cleanup = onThemeChange((newTheme, previousTheme) => {\n *     console.log(`Theme changed from ${previousTheme} to ${newTheme}`);\n *     // Update analytics, save preferences, etc.\n *   });\n *\n *   return cleanup; // Important: cleanup to prevent memory leaks\n * }, [onThemeChange]);\n */\nexport const useTheme = (): ThemeContextType => {\n  const context = useContext(ThemeContext);\n\n  if (context === undefined) {\n    throw new Error(\n      'useTheme must be used within a ThemeProvider. ' +\n        'Make sure your component is wrapped with <ThemeProvider>. ' +\n        'Example: <ThemeProvider><YourComponent /></ThemeProvider>'\n    );\n  }\n\n  return context;\n};\n","import React, { useCallback, useEffect, useState } from 'react';\n\nimport { Button } from '../atoms/button.tsx';\nimport { Icon } from '../atoms/icon.tsx';\nimport { TextInput } from '../atoms/text-input.tsx';\n\n/**\n * Props for the CreateRoomModal component\n */\nexport interface CreateRoomModalProps {\n  /**\n   * Whether the modal is currently open and visible.\n   * Controls the modal's visibility state and enables keyboard event handling.\n   */\n  isOpen: boolean;\n\n  /**\n   * Callback function triggered when the modal should be closed.\n   * Called on backdrop click, escape key press, cancel button click, or successful room creation.\n   * Should update the parent component's state to hide the modal.\n   */\n  onClose: () => void;\n\n  /**\n   * Callback function triggered when a new room should be created.\n   * Receives the room name as a parameter.\n   * The modal will automatically close after this callback is executed.\n   *\n   * @param roomName - The name of the room to be created.\n   *\n   * @example\n   * ```tsx\n   * const handleCreateRoom = async (roomName: string) => {\n   *   try {\n   *     await chatService.createRoom(roomName);\n   *     // Room created successfully, modal will close automatically\n   *   } catch (error) {\n   *     // Handle error - modal stays open for retry\n   *     console.error('Failed to create room:', error);\n   *   }\n   * };\n   * ```\n   */\n  onCreateRoom: (roomName: string) => void;\n}\n\n/**\n * Modal component for creating a new chat room\n *\n * Features:\n * - Form validation with real-time feedback\n * - Escape key handling to close modal\n * - Backdrop click to close\n * - Auto-focus on room name input\n * - Disabled submit when input is empty/whitespace\n * - Automatic input cleanup on close\n * - Accessible ARIA attributes for screen readers\n * - Support for light/dark theming\n *\n * @example\n * // Basic usage\n * <CreateRoomModal\n *   isOpen={showCreateModal}\n *   onClose={() => setShowCreateModal(false)}\n *   onCreateRoom={handleCreateRoom}\n * />\n *\n * @example\n * // With state management\n * const [isCreating, setIsCreating] = useState(false);\n * const [showModal, setShowModal] = useState(false);\n *\n * const handleCreateRoom = async (roomName: string) => {\n *   setIsCreating(true);\n *   try {\n *     await chatService.createRoom(roomName);\n *     setShowModal(false);\n *     // Navigate to new room or refresh room list\n *   } catch (error) {\n *     console.error('Failed to create room:', error);\n *   } finally {\n *     setIsCreating(false);\n *   }\n * };\n *\n * <CreateRoomModal\n *   isOpen={showModal}\n *   onClose={() => setShowModal(false)}\n *   onCreateRoom={handleCreateRoom}\n * />\n */\nexport const CreateRoomModal = ({ isOpen, onClose, onCreateRoom }: CreateRoomModalProps) => {\n  const [roomName, setRoomName] = useState('');\n\n  /**\n   * Handles closing the modal and resetting form state.\n   *\n   * Clears the room name input field and triggers the onClose callback\n   * to notify the parent component that the modal should be hidden.\n   *\n   */\n  const handleClose = useCallback(() => {\n    setRoomName('');\n    onClose();\n  }, [onClose]);\n\n  // Handle escape key press to close the modal\n  useEffect(() => {\n    const handleEscapeKey = (e: KeyboardEvent) => {\n      if (isOpen && e.key === 'Escape') {\n        handleClose();\n      }\n    };\n    document.addEventListener('keydown', handleEscapeKey);\n    return () => {\n      document.removeEventListener('keydown', handleEscapeKey);\n    };\n  }, [handleClose, isOpen]);\n\n  if (!isOpen) return;\n\n  /**\n   * Handles form submission when creating a new room\n   * Validates the room name, calls the onCreateRoom callback, and closes the modal\n   *\n   * @param e - The form submission event\n   */\n  const handleSubmit = (e: React.FormEvent) => {\n    e.preventDefault();\n    if (!roomName.trim()) return;\n    onCreateRoom(roomName.trim());\n    setRoomName('');\n    onClose();\n  };\n\n  return (\n    <>\n      {/* Backdrop */}\n      <div\n        className=\"fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4\"\n        onClick={handleClose}\n      >\n        {/* Modal */}\n        <div\n          className=\"bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md\"\n          onClick={(e) => {\n            e.stopPropagation();\n          }}\n          role=\"dialog\"\n          aria-modal=\"true\"\n          aria-labelledby=\"modal-title\"\n        >\n          {/* Header */}\n          <div className=\"flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700\">\n            <h2 id=\"modal-title\" className=\"text-xl font-semibold text-gray-900 dark:text-gray-100\">\n              Create New Room\n            </h2>\n            <button\n              onClick={handleClose}\n              className=\"text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors\"\n              aria-label=\"Close modal\"\n            >\n              <Icon name=\"close\" size=\"md\" />\n            </button>\n          </div>\n\n          {/* Content */}\n          <form onSubmit={handleSubmit} className=\"p-6\">\n            <div className=\"mb-4\">\n              <label className=\"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2\">\n                Room Name\n              </label>\n              <TextInput\n                value={roomName}\n                onChange={(e) => {\n                  setRoomName(e.target.value);\n                }}\n                placeholder=\"Enter room name...\"\n                className=\"w-full\"\n                autoFocus\n                aria-label=\"Room name\"\n                aria-required=\"true\"\n              />\n            </div>\n\n            {/* Actions */}\n            <div className=\"flex items-center justify-end gap-3\">\n              <Button type=\"button\" variant=\"secondary\" onClick={handleClose}>\n                Cancel\n              </Button>\n              <Button type=\"submit\" variant=\"primary\" disabled={!roomName.trim()}>\n                Create Room\n              </Button>\n            </div>\n          </form>\n        </div>\n      </div>\n    </>\n  );\n};\n","import React, { useEffect, useRef, useState } from 'react';\n\n/**\n * Interface for dropdown menu items\n */\nexport interface DropdownMenuItem {\n  /**\n   * Unique identifier for the menu item.\n   * Used as the React key and for accessibility purposes.\n   */\n  id: string;\n\n  /**\n   * Display text for the menu item.\n   * Should be descriptive and action-oriented (e.g., \"Edit Message\", \"Delete\").\n   */\n  label: string;\n\n  /**\n   * Optional icon to display before the label.\n   * Can be an emoji, Unicode character, or React component.\n   * Will be rendered with gray color styling.\n   */\n  icon?: string;\n\n  /**\n   * Callback function triggered when the menu item is clicked.\n   * The dropdown will automatically close after this function is called.\n   *\n   * @remarks\n   * This function should handle the specific action for the menu item.\n   * Any async operations should be handled within this callback.\n   */\n  onClick: () => void;\n}\n\n/**\n * Props for the DropdownMenu component\n */\nexport interface DropdownMenuProps {\n  /**\n   * React node that will trigger the dropdown when clicked.\n   * Can be any clickable element like buttons, icons, text, or custom components.\n   * Will automatically receive click and keyboard event handlers.\n   *\n   * @example\n   * ```tsx\n   * // Button trigger\n   * trigger={<Button>Options</Button>}\n   *\n   * // Icon trigger\n   * trigger={<Icon name=\"more-vertical\" />}\n   *\n   * // Custom trigger\n   * trigger={<div className=\"cursor-pointer\">⋮</div>}\n   * ```\n   */\n  trigger: React.ReactNode;\n\n  /**\n   * Array of menu items to display in the dropdown.\n   * Items will be rendered in the order provided.\n   * Each item must have a unique ID within the dropdown.\n   */\n  items: DropdownMenuItem[];\n\n  /**\n   * Horizontal alignment of the dropdown relative to the trigger element.\n   * - `left`: Dropdown aligns to the left edge of the trigger\n   * - `right`: Dropdown aligns to the right edge of the trigger\n   *\n   * @default \"right\"\n   */\n  align?: 'left' | 'right';\n}\n\n/**\n * DropdownMenu component displays a toggleable menu with customizable items\n *\n * Features:\n * - Custom trigger element (button, icon, etc.)\n * - Configurable alignment (left or right)\n * - Support for icons in menu items\n * - Automatically closes when clicking outside\n * - Accessible keyboard navigation\n * - Escape key support\n * - Focus management\n *\n * Behavior:\n * - Clicking outside the dropdown closes it automatically\n * - Clicking any menu item closes the dropdown after executing the onClick handler\n * - Dropdown is positioned absolutely relative to the trigger\n * - Uses z-index of 50 for the dropdown and 40 for the backdrop\n * - Supports both light and dark themes\n *\n *\n * @example\n * // Basic dropdown with button trigger\n * <DropdownMenu\n *   trigger={<Button variant=\"secondary\">Options</Button>}\n *   items={[\n *     { id: 'edit', label: 'Edit', onClick: () => handleEdit() },\n *     { id: 'delete', label: 'Delete', onClick: () => handleDelete() },\n *   ]}\n * />\n *\n * @example\n * // Dropdown with icon trigger and icons in items\n * <DropdownMenu\n *   trigger={<Icon name=\"more\" />}\n *   align=\"left\"\n *   items={[\n *     {\n *       id: 'share',\n *       label: 'Share',\n *       icon: '🔗',\n *       onClick: () => handleShare()\n *     },\n *     {\n *       id: 'copy',\n *       label: 'Copy Link',\n *       icon: '📋',\n *       onClick: () => handleCopy()\n *     },\n *     {\n *       id: 'report',\n *       label: 'Report',\n *       icon: '⚠️',\n *       onClick: () => handleReport()\n *     },\n *   ]}\n * />\n *\n * @example\n * // User profile dropdown\n * <DropdownMenu\n *   trigger={\n *     <div className=\"flex items-center gap-2 cursor-pointer\">\n *       <Avatar src={user.avatar} />\n *       <span>{user.name}</span>\n *     </div>\n *   }\n *   align=\"right\"\n *   items={[\n *     { id: 'profile', label: 'Profile', onClick: () => navigate('/profile') },\n *     { id: 'settings', label: 'Settings', onClick: () => navigate('/settings') },\n *     { id: 'logout', label: 'Sign Out', onClick: () => handleLogout() },\n *   ]}\n * />\n */\nexport const DropdownMenu = ({ trigger, items, align = 'right' }: DropdownMenuProps) => {\n  const [isOpen, setIsOpen] = useState(false);\n  const dropdownRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {\n        setIsOpen(false);\n      }\n    };\n\n    const handleEscapeKey = (event: KeyboardEvent) => {\n      if (event.key === 'Escape' && isOpen) {\n        setIsOpen(false);\n      }\n    };\n\n    document.addEventListener('mousedown', handleClickOutside);\n    document.addEventListener('keydown', handleEscapeKey);\n\n    return () => {\n      document.removeEventListener('mousedown', handleClickOutside);\n      document.removeEventListener('keydown', handleEscapeKey);\n    };\n  }, [isOpen]);\n\n  const handleItemClick = (item: DropdownMenuItem) => {\n    item.onClick();\n    setIsOpen(false);\n  };\n\n  return (\n    <div className=\"relative\" ref={dropdownRef}>\n      <div\n        onClick={() => {\n          setIsOpen(!isOpen);\n        }}\n        role=\"button\"\n        aria-haspopup=\"true\"\n        aria-expanded={isOpen}\n        aria-controls=\"dropdown-menu\"\n        tabIndex={0}\n        onKeyDown={(e) => {\n          if (e.key === 'Enter' || e.key === ' ') {\n            e.preventDefault();\n            setIsOpen(!isOpen);\n          }\n        }}\n      >\n        {trigger}\n      </div>\n\n      {isOpen && (\n        <>\n          {/* Backdrop */}\n          <div\n            className=\"fixed inset-0 z-40\"\n            onClick={() => {\n              setIsOpen(false);\n            }}\n            aria-hidden=\"true\"\n          />\n\n          {/* Dropdown Menu */}\n          <div\n            id=\"dropdown-menu\"\n            role=\"menu\"\n            aria-orientation=\"vertical\"\n            className={`absolute top-full mt-2 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg z-50 ${\n              align === 'right' ? 'right-0' : 'left-0'\n            }`}\n          >\n            <div className=\"py-1\">\n              {items.map((item) => (\n                <button\n                  key={item.id}\n                  className=\"w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors\"\n                  onClick={() => {\n                    handleItemClick(item);\n                  }}\n                  role=\"menuitem\"\n                  tabIndex={-1}\n                >\n                  {item.icon && (\n                    <span className=\"text-gray-500 dark:text-gray-400\" aria-hidden=\"true\">\n                      {item.icon}\n                    </span>\n                  )}\n                  {item.label}\n                </button>\n              ))}\n            </div>\n          </div>\n        </>\n      )}\n    </div>\n  );\n};\n","import { useOccupancy } from '@ably/chat/react';\nimport React from 'react';\n\nimport { useRoomAvatar } from '../../hooks/use-room-avatar.tsx';\nimport { Avatar, AvatarData } from '../atoms/avatar.tsx';\nimport { Button } from '../atoms/button.tsx';\nimport { Icon } from '../atoms/icon.tsx';\nimport { TypingIndicators } from './typing-indicators.tsx';\n\n/**\n * Props for the RoomListItem component\n */\nexport interface RoomListItemProps {\n  /**\n   * Unique identifier for the room.\n   * Used for room identification and avatar generation when no custom avatar is provided.\n   */\n  roomName: string;\n\n  /**\n   * Whether this room is currently selected/active in the UI.\n   * Controls visual highlighting and selection indicators.\n   * When true, shows selection styling and active indicators.\n   */\n  isSelected: boolean;\n\n  /**\n   * Callback function triggered when the room item is clicked.\n   * Should handle room navigation and selection logic.\n   * Called for clicks on the main room area (not action buttons).\n   */\n  onClick: () => void;\n\n  /**\n   * Callback function triggered when the leave button is clicked.\n   * Should handle room departure logic and UI updates.\n   * Called only when the leave button is explicitly clicked.\n   */\n  onLeave: () => void;\n\n  /**\n   * Optional custom avatar data for the room.\n   * If not provided, uses the useRoomAvatar hook to generate/retrieve avatar data.\n   * Allows for custom room branding and visual identity.\n   */\n  avatar?: AvatarData;\n\n  /**\n   * Whether the component should render in collapsed mode (avatar only).\n   * When true, displays only the room avatar with a selection indicator.\n   * When false, shows full room information including name, counts, and actions.\n   * @default false\n   */\n  isCollapsed?: boolean;\n\n  /**\n   * Whether typing indicators should be displayed for this room.\n   * Controls the visibility of real-time typing status below the room name.\n   * @default true\n   */\n  typingIndicatorsEnabled?: boolean;\n}\n\n/**\n * RoomListItem component displays a room entry in the sidebar with activity indicators and controls\n *\n * Core Features:\n * - Room avatar with automatic fallback to generated avatars via useRoomAvatar hook\n * - Activity indicators (presence count, activity status)\n * - Room selection with visual feedback and hover states\n * - Typing indicators showing who is currently typing (when enabled)\n * - Leave room functionality with hover-revealed action button\n * - Collapsed mode for compact sidebar display (avatar-only)\n * - Connection count display for total room occupancy (connections)\n * - Accessible design with proper ARIA attributes and keyboard navigation\n * - Theme-aware styling supporting both light and dark modes\n *\n * @example\n * // Basic usage in sidebar room list\n * <RoomListItem\n *   roomName=\"general\"\n *   isSelected={currentRoom === \"general\"}\n *   onClick={() => setCurrentRoom(\"general\")}\n *   onLeave={() => leaveRoom(\"general\")}\n * />\n *\n * @example\n * // With custom avatar and collapsed mode\n * <RoomListItem\n *   roomName=\"design-team\"\n *   isSelected={false}\n *   onClick={handleRoomSelect}\n *   onLeave={handleRoomLeave}\n *   avatar={{\n *     displayName: \"Design Team\",\n *     src: \"/team-avatars/design.jpg\",\n *     color: \"bg-purple-500\"\n *   }}\n *   isCollapsed={sidebarCollapsed}\n *   typingIndicatorsEnabled={true}\n * />\n *\n *\n */\nexport const RoomListItem = React.memo(function RoomListItem({\n  roomName,\n  isSelected,\n  onClick,\n  onLeave,\n  avatar: propAvatar,\n  isCollapsed = false,\n  typingIndicatorsEnabled = true,\n}: RoomListItemProps) {\n  // Get occupancy data\n  const { connections, presenceMembers } = useOccupancy();\n\n  const { roomAvatar } = useRoomAvatar({ roomName });\n  const roomAvatarData = propAvatar || roomAvatar;\n  /**\n   * Checks if the room has any active users\n   *\n   * @returns True if at least one user is present in the room\n   */\n  const isRoomActive = () => {\n    // Check if anyone is present in the room\n    return presenceMembers > 0;\n  };\n\n  const isActive = isRoomActive();\n\n  // If collapsed, render just the avatar with selection indicator\n  if (isCollapsed) {\n    return (\n      <div className=\"flex justify-center p-2\">\n        <div\n          className={`relative cursor-pointer transition-transform hover:scale-110 ${\n            isSelected ? 'ring-2 ring-blue-500 rounded-full' : ''\n          }`}\n          onClick={onClick}\n          title={roomAvatarData?.displayName || roomName}\n          role=\"button\"\n          aria-label={`${roomAvatarData?.displayName || roomName} room${isSelected ? ' (selected)' : ''}`}\n          aria-pressed={isSelected}\n          tabIndex={0}\n          onKeyDown={(e) => {\n            if (e.key === 'Enter' || e.key === ' ') {\n              e.preventDefault();\n              onClick();\n            }\n          }}\n        >\n          <Avatar\n            alt={roomAvatarData?.displayName}\n            src={roomAvatarData?.src}\n            color={roomAvatarData?.color}\n            size=\"md\"\n            initials={roomAvatarData?.initials}\n          />\n          {isSelected && (\n            <div className=\"absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-green-500 rounded-full border-2 border-white dark:border-gray-900\" />\n          )}\n        </div>\n      </div>\n    );\n  }\n\n  // Otherwise render the full room list item\n  return (\n    <div\n      className={`group flex items-center gap-3 p-3 hover:bg-gray-50 dark:hover:bg-gray-800\n                    cursor-pointer transition-colors\n                    ${isSelected ? 'bg-gray-100 dark:bg-gray-800 border-r-2 border-blue-500' : ''}`}\n      onClick={onClick}\n      role=\"button\"\n      aria-label={`${roomAvatarData?.displayName || roomName} room${isSelected ? ' (selected)' : ''}${isActive ? `, ${String(presenceMembers)} online` : ''}`}\n      aria-pressed={isSelected}\n      tabIndex={0}\n      onKeyDown={(e) => {\n        if (e.key === 'Enter' || e.key === ' ') {\n          e.preventDefault();\n          onClick();\n        }\n      }}\n    >\n      <div className=\"relative\">\n        <Avatar\n          alt={roomAvatarData?.displayName}\n          src={roomAvatarData?.src}\n          color={roomAvatarData?.color}\n          size=\"md\"\n          initials={roomAvatarData?.initials}\n        />\n\n        {/* Present indicator */}\n        {isActive && (\n          <div\n            className=\"absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-green-500 rounded-full border-2 border-white dark:border-gray-900\"\n            aria-hidden=\"true\"\n            title=\"Room is active\"\n          />\n        )}\n\n        {/* Present count badge */}\n        {presenceMembers > 0 && (\n          <div\n            className=\"absolute -top-1 -right-1 bg-green-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center font-medium\"\n            aria-hidden=\"true\"\n            title={`${String(presenceMembers)} ${presenceMembers === 1 ? 'person' : 'people'} online`}\n          >\n            {presenceMembers > 9 ? '9+' : presenceMembers}\n          </div>\n        )}\n      </div>\n\n      <div className=\"flex-1 min-w-0\">\n        <div className=\"flex items-center justify-between\">\n          <h3 className=\"font-medium text-gray-900 dark:text-gray-100 truncate\">\n            {roomAvatarData?.displayName}\n          </h3>\n          <div className=\"flex items-center gap-2 flex-shrink-0\">\n            {/* Leave button - only visible on hover */}\n            <Button\n              variant=\"ghost\"\n              size=\"sm\"\n              onClick={(e) => {\n                e.stopPropagation();\n                onLeave();\n              }}\n              className=\"opacity-0 group-hover:opacity-100 transition-opacity text-gray-400 hover:text-red-500 p-1\"\n              aria-label={`Leave ${roomAvatarData?.displayName || 'room'}`}\n              title={`Leave ${roomAvatarData?.displayName || 'room'}`}\n            >\n              <Icon name=\"close\" size=\"sm\" />\n            </Button>\n            {/* Room participant count */}\n            <span\n              className=\"text-xs text-gray-400\"\n              title={`${String(connections)} total ${connections === 1 ? 'connection' : 'connections'}`}\n            >\n              {connections}\n            </span>\n          </div>\n        </div>\n        <div aria-live=\"polite\">\n          {typingIndicatorsEnabled && <TypingIndicators maxClients={1} />}\n        </div>\n      </div>\n    </div>\n  );\n});\n","import { RoomOptions } from '@ably/chat';\nimport { ChatRoomProvider } from '@ably/chat/react';\nimport React from 'react';\n\nimport { RoomListItem } from './room-list-item.tsx';\n\n/**\n * Props for the RoomList component\n */\nexport interface RoomListProps {\n  /** Array of room names to render. */\n  roomNames: string[];\n  /** Currently selected room. */\n  activeRoomName?: string;\n  /** Ably chat options applied to each `ChatRoomProvider`. */\n  defaultRoomOptions?: RoomOptions;\n\n  /** Fires when the user clicks a room. */\n  onSelect: (roomName: string) => void;\n  /** Fires when the user clicks the “leave” action. */\n  onLeave: (roomName: string) => void;\n\n  /** Collapsed (avatar-only) rendering mode. */\n  isCollapsed?: boolean;\n}\n\n/**\n * RoomList component\n *\n * Component that renders a list of chat rooms. It displays each room as a clickable item\n * with an avatar, name, and action buttons for selecting or leaving the room. It also\n * allows for collapsed rendering mode where only the avatar is shown.\n * Each room is wrapped in a `ChatRoomProvider`, rooms will automatically attach/(detach & release) on mount/unmount.\n *\n * @example\n * <RoomList\n *   roomNames={['room1', 'room2', 'room3']}\n *   onSelect={(roomName) => console.log('Selected:', roomName)}\n *   onLeave={(roomName) => console.log('Left:', roomName)}\n * />\n *\n * @example\n * // Collapsed mode for narrow sidebars\n * <RoomList\n *   roomNames={['general', 'random']}\n *   activeRoomName=\"general\"\n *   isCollapsed={true}\n *   onSelect={setActiveRoom}\n *   onLeave={handleLeaveRoom}\n * />\n *\n */\nexport const RoomList = ({\n  roomNames,\n  activeRoomName,\n  defaultRoomOptions,\n  onSelect,\n  onLeave,\n  isCollapsed = false,\n}: RoomListProps) => (\n  <>\n    {roomNames.map((roomName) => (\n      <ChatRoomProvider key={roomName} name={roomName} options={defaultRoomOptions}>\n        <RoomListItem\n          roomName={roomName}\n          isSelected={roomName === activeRoomName}\n          isCollapsed={isCollapsed}\n          onClick={() => {\n            onSelect(roomName);\n          }}\n          onLeave={() => {\n            onLeave(roomName);\n          }}\n        />\n      </ChatRoomProvider>\n    ))}\n  </>\n);\n\nRoomList.displayName = 'RoomList';\n","import { RoomOptions } from '@ably/chat';\nimport { clsx } from 'clsx';\nimport React, { useState } from 'react';\n\nimport { useTheme } from '../../hooks/use-theme.tsx';\nimport { Button } from '../atoms/button.tsx';\nimport { Icon } from '../atoms/icon.tsx';\nimport { CreateRoomModal } from './create-room-modal.tsx';\nimport { DropdownMenu } from './dropdown-menu.tsx';\nimport { RoomList } from './room-list.tsx';\n\n/**\n * Props for the Sidebar component\n */\nexport interface SidebarProps {\n  /** Rooms to display. */\n  roomNames: string[];\n  /** Currently-active room (optional). */\n  activeRoomName?: string;\n  /** Ably options passed to each `ChatRoomProvider`. */\n  defaultRoomOptions?: RoomOptions;\n  /** Adds (or joins) a room. Should also set it active, if desired. */\n  addRoom: (name: string) => void;\n  /** Sets the active room. Pass `undefined` to clear. */\n  setActiveRoom: (name?: string) => void;\n  /** Leaves a room (and handles provider release if needed). */\n  leaveRoom: (name: string) => void;\n  /** Optional CSS class names for additional styling. */\n  className?: string;\n  /** Whether the sidebar is in collapsed mode (avatar-only). */\n  isCollapsed?: boolean;\n  /** Callback to toggle the collapsed state. */\n  onToggleCollapse?: () => void;\n}\n\n/**\n * Sidebar component provides room navigation and management\n *\n * Features:\n * - Collapsible interface with avatar-only mode\n * - Room creation and management\n * - Theme toggle integration\n * - Active room highlighting\n * - Room count display\n *\n * @example\n * const [rooms, setRooms]   = useState<string[]>([]);\n * const [active, setActive] = useState<string>();\n *\n * const addRoom   = (name: string) => setRooms(r => r.includes(name) ? r : [...r, name]);\n * const leaveRoom = (name: string) => setRooms(r => r.filter(n => n !== name));\n *\n * <Sidebar\n *   roomNames={rooms}\n *   activeRoomName={active}\n *   defaultRoomOptions={{ rewind: 50 }}\n *   addRoom={addRoom}\n *   setActiveRoom={setActive}\n *   leaveRoom={leaveRoom}\n * />\n */\nexport const Sidebar = ({\n  roomNames,\n  activeRoomName,\n  defaultRoomOptions,\n  addRoom,\n  setActiveRoom,\n  leaveRoom,\n  className = '',\n  isCollapsed = false,\n  onToggleCollapse,\n}: SidebarProps) => {\n  const [showCreateModal, setShowCreateModal] = useState(false);\n  const { theme, toggleTheme } = useTheme();\n\n  return (\n    <aside\n      className={clsx(\n        'bg-white dark:bg-gray-900',\n        'border-r border-gray-200 dark:border-gray-800',\n        'flex flex-col h-full',\n        'w-full',\n        className\n      )}\n    >\n      {/* Header */}\n      <div\n        className={clsx(\n          'flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-800',\n          'min-h-[4rem]'\n        )}\n      >\n        {isCollapsed ? (\n          <div className=\"flex flex-col items-center gap-2\">\n            <Button variant=\"ghost\" size=\"sm\" onClick={toggleTheme}>\n              <Icon name={theme === 'dark' ? 'sun' : 'moon'} size=\"md\" />\n            </Button>\n            {onToggleCollapse && (\n              <Button variant=\"ghost\" size=\"sm\" onClick={onToggleCollapse}>\n                <Icon name=\"chevronright\" size=\"md\" />\n              </Button>\n            )}\n          </div>\n        ) : (\n          <>\n            <h1 className=\"text-xl font-bold text-gray-900 dark:text-gray-100 truncate\">\n              Chats <span className=\"text-sm font-normal text-gray-500\">({roomNames.length})</span>\n            </h1>\n\n            <div className=\"flex items-center gap-2 flex-shrink-0\">\n              <Button variant=\"ghost\" size=\"sm\" onClick={toggleTheme}>\n                <Icon name={theme === 'dark' ? 'sun' : 'moon'} size=\"md\" />\n              </Button>\n\n              <DropdownMenu\n                trigger={\n                  <Button variant=\"ghost\" size=\"sm\">\n                    <Icon name=\"more\" size=\"md\" />\n                  </Button>\n                }\n                items={[\n                  {\n                    id: 'create-room',\n                    label: 'Create Room',\n                    icon: '➕',\n                    onClick: () => {\n                      setShowCreateModal(true);\n                    },\n                  },\n                ]}\n              />\n\n              {onToggleCollapse && (\n                <Button variant=\"ghost\" size=\"sm\" onClick={onToggleCollapse}>\n                  <Icon name=\"chevronleft\" size=\"md\" />\n                </Button>\n              )}\n            </div>\n          </>\n        )}\n      </div>\n\n      <div className=\"flex-1 overflow-y-auto min-w-0\">\n        <RoomList\n          roomNames={roomNames}\n          activeRoomName={activeRoomName}\n          defaultRoomOptions={defaultRoomOptions}\n          onSelect={setActiveRoom}\n          onLeave={leaveRoom}\n          isCollapsed={isCollapsed}\n        />\n      </div>\n      <CreateRoomModal\n        isOpen={showCreateModal}\n        onClose={() => {\n          setShowCreateModal(false);\n        }}\n        onCreateRoom={(name) => {\n          addRoom(name);\n          setShowCreateModal(false);\n        }}\n      />\n    </aside>\n  );\n};\n\nSidebar.displayName = 'Sidebar';\n","import { ConnectionStatus, RoomOptions } from '@ably/chat';\nimport { ChatRoomProvider, useChatConnection } from '@ably/chat/react';\nimport { clsx } from 'clsx';\nimport React, { useCallback, useMemo, useState } from 'react';\n\nimport { AppLoading } from '../components/atoms/app-loading.tsx';\nimport { ChatWindow } from '../components/molecules/chat-window.tsx';\nimport { EmptyState } from '../components/molecules/empty-state.tsx';\nimport { RoomInfo } from '../components/molecules/room-info.tsx';\nimport { RoomReaction } from '../components/molecules/room-reaction.tsx';\nimport { Sidebar } from '../components/molecules/sidebar.tsx';\n\n/**\n * Props for the App component.\n *\n * @property initialRoomNames - An optional array of room names to populate the sidebar initially.\n * @property width - Width of the app container (default: '70vw')\n * @property height - Height of the app container (default: '70vh')\n */\ninterface AppProps {\n  initialRoomNames?: string[];\n  width?: string | number;\n  height?: string | number;\n}\n\nconst DEFAULT_ROOM_OPTIONS: RoomOptions = {\n  occupancy: { enableEvents: true },\n};\n\n/**\n * Main chat application component.\n *\n * @param props - The props for the App component.\n */\nexport const App = ({ initialRoomNames, width = '70vw', height = '70vh' }: AppProps) => {\n  const { currentStatus } = useChatConnection();\n  const [roomNames, setRoomNames] = useState<string[]>(initialRoomNames || []);\n  const [activeRoom, setActiveRoom] = useState<string | undefined>();\n  const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);\n\n  // Function to handle room selection change\n  const handleChangeSelectedRoom = useCallback((roomName?: string) => {\n    setActiveRoom(roomName);\n  }, []);\n\n  const addRoom = useCallback((name: string) => {\n    setRoomNames((prev) => (prev.includes(name) ? prev : [...prev, name]));\n    setActiveRoom(name);\n  }, []);\n\n  const leaveRoom = useCallback(\n    (name: string) => {\n      setRoomNames((prev) => {\n        const next = prev.filter((n) => n !== name);\n        if (next.length === 0) {\n          setActiveRoom(undefined);\n        } else {\n          if (name === activeRoom) setActiveRoom(next[0]);\n        }\n        return next;\n      });\n    },\n    [activeRoom]\n  );\n\n  const handleToggleSidebar = useCallback(() => {\n    setIsSidebarCollapsed((prev) => !prev);\n  }, []);\n\n  // Memoize the container style\n  const containerStyle = useMemo(\n    () => ({\n      width: typeof width === 'number' ? `${String(width)}px` : width,\n      height: typeof height === 'number' ? `${String(height)}px` : height,\n    }),\n    [width, height]\n  );\n\n  // Show loading state if not connected (cannot make REST or WS Calls)\n  // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison\n  if (currentStatus !== ConnectionStatus.Connected) {\n    return <AppLoading />;\n  }\n\n  return (\n    <div\n      className={clsx(\n        // Layout fundamentals\n        'flex',\n        // Theme and colors\n        'bg-gray-50 dark:bg-gray-950',\n        'text-gray-900 dark:text-gray-100',\n        // Positioning and overflow\n        'overflow-hidden',\n        // Visual styling\n        'border border-gray-200 dark:border-gray-700',\n        'rounded-lg shadow-lg',\n        // Centering\n        'mx-auto my-8'\n      )}\n      style={containerStyle}\n      role=\"main\"\n      aria-label=\"Main chat application\"\n    >\n      {/* Sidebar */}\n      <div\n        className={clsx(\n          'flex-shrink-0',\n          'transition-all duration-300 ease-in-out',\n          isSidebarCollapsed ? 'w-16' : 'w-64 md:w-72 lg:w-80'\n        )}\n      >\n        <Sidebar\n          roomNames={roomNames}\n          addRoom={addRoom}\n          defaultRoomOptions={DEFAULT_ROOM_OPTIONS}\n          setActiveRoom={handleChangeSelectedRoom}\n          leaveRoom={leaveRoom}\n          activeRoomName={activeRoom}\n          isCollapsed={isSidebarCollapsed}\n          onToggleCollapse={handleToggleSidebar}\n        />\n      </div>\n\n      {/* Main Content */}\n      <main className=\"flex-1 overflow-hidden\">\n        {/* Render the active chat window if a room is selected, otherwise show empty state */}\n        {activeRoom ? (\n          <ChatRoomProvider key={activeRoom} name={activeRoom} options={DEFAULT_ROOM_OPTIONS}>\n            <ChatWindow\n              key={activeRoom}\n              roomName={activeRoom}\n              customHeaderContent={<RoomInfo />}\n              customFooterContent={<RoomReaction />}\n            />\n          </ChatRoomProvider>\n        ) : (\n          <div className=\"flex flex-col h-full\">\n            <EmptyState\n              icon={\n                <svg\n                  className=\"mx-auto h-12 w-12 text-gray-400\"\n                  fill=\"none\"\n                  viewBox=\"0 0 24 24\"\n                  stroke=\"currentColor\"\n                  aria-hidden=\"true\"\n                >\n                  <path\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                    strokeWidth={2}\n                    d=\"M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z\"\n                  />\n                </svg>\n              }\n              title={'Select a room to start chatting'}\n              message={'Choose a room or create a new one to begin your conversation'}\n              ariaLabel=\"No chat room selected\"\n            />\n          </div>\n        )}\n      </main>\n    </div>\n  );\n};\n","import React, { useState } from 'react';\n\nimport { Avatar, AvatarData } from '../atoms/avatar.tsx';\nimport { Button } from '../atoms/button.tsx';\nimport { Icon } from '../atoms/icon.tsx';\n\n/**\n * Preset avatar options for users to choose from\n */\nconst PRESET_AVATARS = [\n  { src: 'https://api.dicebear.com/9.x/thumbs/svg?seed=Felix', label: 'Style 1' },\n  { src: 'https://api.dicebear.com/9.x/thumbs/svg?seed=Dusty', label: 'Style 2' },\n  { src: 'https://api.dicebear.com/9.x/thumbs/svg?seed=Mittens', label: 'Style 3' },\n  { src: 'https://api.dicebear.com/9.x/thumbs/svg?seed=Misty', label: 'Style 4' },\n  { src: 'https://api.dicebear.com/9.x/thumbs/svg?seed=Bailey', label: 'Style 5' },\n  { src: 'https://api.dicebear.com/9.x/thumbs/svg?seed=Milo', label: 'Style 6' },\n];\n\n/**\n * Predefined color options for avatars\n */\nconst COLOR_OPTIONS = [\n  { value: 'bg-blue-500', label: 'Blue' },\n  { value: 'bg-purple-500', label: 'Purple' },\n  { value: 'bg-green-500', label: 'Green' },\n  { value: 'bg-orange-500', label: 'Orange' },\n  { value: 'bg-red-500', label: 'Red' },\n  { value: 'bg-pink-500', label: 'Pink' },\n  { value: 'bg-indigo-500', label: 'Indigo' },\n  { value: 'bg-yellow-500', label: 'Yellow' },\n  { value: 'bg-teal-500', label: 'Teal' },\n  { value: 'bg-cyan-500', label: 'Cyan' },\n];\n\nexport interface AvatarEditorProps {\n  /**\n   * Current avatar URL\n   */\n  currentAvatar?: string;\n\n  /**\n   * Current avatar background color\n   */\n  currentColor?: string;\n\n  /**\n   * Display name for the avatar\n   */\n  displayName: string;\n\n  /**\n   * Callback when the editor is closed\n   */\n  onClose: () => void;\n\n  /**\n   * Callback when the avatar is saved\n   * @param avatar - The updated avatar data\n   */\n  onSave: (avatar: Partial<AvatarData>) => void;\n}\n\n/**\n * AvatarEditor component allows users to customize their avatar\n *\n * Features:\n * - Upload custom images\n * - Enter image URL\n * - Choose from preset avatars\n * - Select background colors\n * - Remove avatar\n *\n * TODO: Break up into smaller subcomponents:\n * - AvatarUploadTab\n * - AvatarPresetsTab\n * - AvatarCustomizeTab\n * - AvatarPreview\n */\nexport const AvatarEditor = ({\n  currentAvatar,\n  currentColor,\n  displayName,\n  onClose,\n  onSave,\n}: AvatarEditorProps) => {\n  const [avatarUrl, setAvatarUrl] = useState(currentAvatar || '');\n  const [selectedColor, setSelectedColor] = useState(currentColor || '');\n  const [customInitials, setCustomInitials] = useState('');\n  const [activeTab, setActiveTab] = useState<'presets' | 'color'>('presets');\n  const [error, setError] = useState('');\n\n  /**\n   * Handles changes to the custom initials input\n   * Limits input to 2 characters and converts to uppercase\n   *\n   * @param event - The input change event\n   */\n  const handleInitialsChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n    // Limit to 2 characters\n    setCustomInitials(event.target.value.slice(0, 2).toUpperCase());\n  };\n\n  /**\n   * Handles selection of a preset avatar\n   *\n   * @param presetUrl - The URL of the selected preset avatar\n   */\n  const handlePresetSelect = (presetUrl: string) => {\n    setAvatarUrl(presetUrl);\n    setError('');\n  };\n\n  /**\n   * Handles selection of a background color\n   *\n   * @param color - The selected color value (CSS class)\n   */\n  const handleColorSelect = (color: string) => {\n    setSelectedColor(color);\n  };\n\n  /**\n   * Handles saving the avatar changes\n   * Collects all avatar data and passes it to the onSave callback\n   */\n  const handleSave = () => {\n    const avatarData: Partial<AvatarData> = {\n      displayName,\n    };\n\n    if (avatarUrl) {\n      avatarData.src = avatarUrl;\n    }\n\n    if (selectedColor) {\n      avatarData.color = selectedColor;\n    }\n\n    if (customInitials) {\n      avatarData.initials = customInitials;\n    }\n\n    onSave(avatarData);\n    onClose();\n  };\n\n  /**\n   * Handles removing the avatar\n   * Clears the avatar URL and passes updated data to the onSave callback\n   */\n  const handleRemove = () => {\n    setAvatarUrl('');\n    onSave({ displayName, src: undefined });\n    onClose();\n  };\n\n  /**\n   * Generates initials from the display name or returns custom initials if set\n   *\n   * @returns The initials to display in the avatar (max 2 characters)\n   */\n  const getInitials = () => {\n    if (customInitials) return customInitials;\n\n    return displayName\n      .split(' ')\n      .filter((name) => name.length > 0)\n      .map((name) => name[0]?.toUpperCase() || '')\n      .join('')\n      .padEnd(2, '•')\n      .slice(0, 2);\n  };\n\n  return (\n    <div className=\"fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50\">\n      <div className=\"bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-md mx-4\">\n        <div className=\"flex items-center justify-between mb-4\">\n          <h2 className=\"text-lg font-semibold text-gray-900 dark:text-gray-100\">Edit Avatar</h2>\n          <Button variant=\"ghost\" size=\"sm\" onClick={onClose}>\n            <Icon name=\"close\" size=\"sm\" />\n          </Button>\n        </div>\n\n        {/* Avatar Preview */}\n        <div className=\"flex justify-center mb-6\">\n          <Avatar\n            alt={displayName}\n            src={avatarUrl}\n            color={selectedColor || currentColor || 'bg-gray-500'}\n            size=\"xl\"\n            initials={getInitials()}\n          />\n        </div>\n\n        {/* Tabs */}\n        <div className=\"flex border-b border-gray-200 dark:border-gray-700 mb-4\" role=\"tablist\">\n          <button\n            className={`px-4 py-2 font-medium text-sm ${\n              activeTab === 'presets'\n                ? 'text-blue-600 dark:text-blue-400 border-b-2 border-blue-600 dark:border-blue-400'\n                : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'\n            }`}\n            onClick={() => {\n              setActiveTab('presets');\n            }}\n            role=\"tab\"\n            aria-selected={activeTab === 'presets'}\n            aria-controls=\"presets-tab\"\n            id=\"presets-tab-button\"\n          >\n            Presets\n          </button>\n          <button\n            className={`px-4 py-2 font-medium text-sm ${\n              activeTab === 'color'\n                ? 'text-blue-600 dark:text-blue-400 border-b-2 border-blue-600 dark:border-blue-400'\n                : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'\n            }`}\n            onClick={() => {\n              setActiveTab('color');\n            }}\n            role=\"tab\"\n            aria-selected={activeTab === 'color'}\n            aria-controls=\"color-tab\"\n            id=\"color-tab-button\"\n          >\n            Customize\n          </button>\n        </div>\n\n        {/* Presets Tab Content */}\n        {activeTab === 'presets' && (\n          <div role=\"tabpanel\" id=\"presets-tab\" aria-labelledby=\"presets-tab-button\">\n            <label className=\"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2\">\n              Choose a Preset Avatar\n            </label>\n            <div className=\"grid grid-cols-3 gap-3\">\n              {PRESET_AVATARS.map((preset, index) => (\n                <div\n                  key={index}\n                  className={`cursor-pointer p-2 rounded-lg ${\n                    avatarUrl === preset.src\n                      ? 'bg-blue-100 dark:bg-blue-900 ring-2 ring-blue-500'\n                      : 'hover:bg-gray-100 dark:hover:bg-gray-700'\n                  }`}\n                  onClick={() => {\n                    handlePresetSelect(preset.src);\n                  }}\n                >\n                  <div className=\"flex flex-col items-center\">\n                    <Avatar alt={preset.label} src={preset.src} size=\"md\" />\n                    <span className=\"mt-1 text-xs text-gray-600 dark:text-gray-400\">\n                      {preset.label}\n                    </span>\n                  </div>\n                </div>\n              ))}\n            </div>\n          </div>\n        )}\n\n        {/* Color Tab Content */}\n        {activeTab === 'color' && (\n          <div\n            className=\"space-y-4\"\n            role=\"tabpanel\"\n            id=\"color-tab\"\n            aria-labelledby=\"color-tab-button\"\n          >\n            <div>\n              <label className=\"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2\">\n                Background Color\n              </label>\n              <div className=\"grid grid-cols-5 gap-2\">\n                {COLOR_OPTIONS.map((color) => (\n                  <div\n                    key={color.value}\n                    className={`w-8 h-8 rounded-full ${color.value} cursor-pointer ${\n                      selectedColor === color.value ? 'ring-2 ring-offset-2 ring-gray-400' : ''\n                    }`}\n                    onClick={() => {\n                      handleColorSelect(color.value);\n                    }}\n                    title={color.label}\n                  />\n                ))}\n              </div>\n            </div>\n\n            <div>\n              <label className=\"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2\">\n                Custom Initials (max 2 characters)\n              </label>\n              <input\n                type=\"text\"\n                value={customInitials}\n                onChange={handleInitialsChange}\n                maxLength={2}\n                placeholder=\"AB\"\n                className=\"w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n              />\n            </div>\n          </div>\n        )}\n\n        {/* Error Message */}\n        {error && <div className=\"text-red-600 dark:text-red-400 text-sm mt-2\">{error}</div>}\n\n        {/* Actions */}\n        <div className=\"flex gap-2 mt-6\">\n          <Button variant=\"secondary\" onClick={onClose} className=\"flex-1\">\n            Cancel\n          </Button>\n          {(currentAvatar || avatarUrl) && (\n            <Button variant=\"secondary\" onClick={handleRemove}>\n              Remove\n            </Button>\n          )}\n          <Button onClick={handleSave} className=\"flex-1\">\n            Save\n          </Button>\n        </div>\n      </div>\n    </div>\n  );\n};\n","import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\n\nimport { AvatarData } from '../components/atoms/avatar.tsx';\nimport { AvatarContext } from '../context/avatar-context.tsx';\n\n/**\n * Removes any undefined entries from a record.\n *\n * @param cache - A record whose values may be T or undefined\n * @returns A new record containing only the entries whose values are defined\n */\nexport function cleanCache<T>(cache: Record<string, T | undefined>): Record<string, T> {\n  const result: Record<string, T> = {};\n\n  // Iterate through each key in the cache\n  for (const key in cache) {\n    const value = cache[key];\n    if (value !== undefined) {\n      // Narrowed to T, safe to assign\n      result[key] = value;\n    }\n  }\n\n  return result;\n}\n\n/**\n * Type guard to validate persisted avatar data structure\n */\nconst isValidPersistedData = (data: unknown): data is PersistedAvatarData => {\n  if (!data || typeof data !== 'object') return false;\n\n  const obj = data as Record<string, unknown>;\n\n  return (\n    typeof obj.version === 'number' &&\n    typeof obj.userAvatars === 'object' &&\n    obj.userAvatars !== null &&\n    typeof obj.roomAvatars === 'object' &&\n    obj.roomAvatars !== null\n  );\n};\n/**\n * Storage key for persisting avatar data in localStorage\n */\nconst STORAGE_KEY = 'ably-chat-ui-avatars';\n/**\n * Callback function type for avatar change events\n */\nexport type AvatarChangeCallback = (\n  type: 'user' | 'room',\n  id: string,\n  avatar: AvatarData,\n  previousAvatar?: AvatarData\n) => void;\n\n/**\n * Options for avatar generation and management\n */\nexport interface AvatarOptions {\n  /**\n   * Whether to persist avatars to localStorage\n   * @default true\n   */\n  persist?: boolean;\n\n  /**\n   * Custom color palette for avatar generation\n   */\n  customColors?: string[];\n\n  /**\n   * Maximum number of cached avatars (0 = unlimited)\n   * @default 100\n   */\n  maxCacheSize?: number;\n\n  /**\n   * Error handler callback\n   * @param error - The error that occurred\n   */\n  onError?: (error: unknown) => void;\n}\n\n/**\n * Persisted avatar data structure for localStorage\n */\nexport interface PersistedAvatarData {\n  /** Cached avatars keyed by user `clientId` (values may be undefined, but are cleaned before saving) */\n  userAvatars: Record<string, AvatarData | undefined>;\n\n  /** Cached avatars keyed by room name (values may be undefined, but are cleaned before saving) */\n  roomAvatars: Record<string, AvatarData | undefined>;\n\n  /** Schema version of the persisted object */\n  version: number;\n}\n\n/**\n * Default color palette for avatar generation\n * Carefully selected for accessibility and visual appeal\n */\nconst DEFAULT_AVATAR_COLORS = [\n  'bg-blue-500',\n  'bg-purple-500',\n  'bg-green-500',\n  'bg-orange-500',\n  'bg-red-500',\n  'bg-pink-500',\n  'bg-indigo-500',\n  'bg-yellow-500',\n  'bg-teal-500',\n  'bg-cyan-500',\n  'bg-emerald-500',\n  'bg-violet-500',\n  'bg-amber-500',\n  'bg-rose-500',\n  'bg-fuchsia-500',\n  'bg-sky-500',\n];\n/**\n * Current version for avatar data persistence schema\n */\nconst AVATAR_DATA_VERSION = 1;\n/**\n * Hook for avatar generation logic\n * Handles color generation and initials extraction\n */\nconst useAvatarGeneration = (customColors?: string[]) => {\n  const avatarColors = customColors || DEFAULT_AVATAR_COLORS;\n\n  /**\n   * Generates a deterministic color based on a string using a hash function\n   */\n  const generateColor = useCallback(\n    (text: string): string => {\n      let hash = 0;\n      for (let i = 0; i < text.length; i++) {\n        hash = ((hash << 5) - hash + (text.codePointAt(i) ?? 0)) & 0xffffffff;\n      }\n      return avatarColors[Math.abs(hash) % avatarColors.length] || 'bg-gray-500';\n    },\n    [avatarColors]\n  );\n\n  /**\n   * Generates initials from a display name with intelligent word parsing\n   */\n  const generateInitials = useCallback((displayName: string): string => {\n    if (!displayName.trim()) return '??';\n\n    // Remove common prefixes and clean the name\n    const cleanName = displayName\n      .trim()\n      .replace(/^(mr|mrs|ms|dr|prof)\\.?\\s+/i, '')\n      .replaceAll(/[^\\w\\s]/g, ' ')\n      .replaceAll(/\\s+/g, ' ');\n\n    const words = cleanName.split(' ').filter((word: string) => word.length > 0);\n\n    if (words.length >= 2) {\n      const firstInitial = words[0]?.[0] || '';\n      const secondInitial = words[1]?.[0] || '';\n      return (firstInitial + secondInitial).toUpperCase();\n    }\n    return cleanName.slice(0, 2).toUpperCase();\n  }, []);\n  return { generateColor, generateInitials };\n};\n/**\n * Hook for avatar change notifications\n * Manages callback registration and notification dispatching\n */\nconst useAvatarNotifications = (onError?: (error: unknown) => void) => {\n  const [changeCallbacks, setChangeCallbacks] = useState<Set<AvatarChangeCallback>>(new Set());\n\n  /**\n   * Error handling helper\n   */\n  const handleError = useCallback(\n    (error: unknown, context?: string) => {\n      if (onError) {\n        onError(error);\n      } else if (process.env.NODE_ENV === 'development') {\n        console.warn(`Avatar error${context ? ` (${context})` : ''}:`, error);\n      }\n    },\n    [onError]\n  );\n\n  /**\n   * Notifies all registered callbacks about avatar changes\n   */\n  const notifyAvatarChange = useCallback(\n    (type: 'user' | 'room', id: string, avatar: AvatarData, previousAvatar?: AvatarData) => {\n      for (const callback of changeCallbacks) {\n        try {\n          callback(type, id, avatar, previousAvatar);\n        } catch (error) {\n          handleError(error, 'Avatar change callback');\n        }\n      }\n    },\n    [changeCallbacks, handleError]\n  );\n\n  /**\n   * Registers a callback for avatar change events\n   */\n  const onAvatarChange = useCallback((callback: AvatarChangeCallback) => {\n    setChangeCallbacks((prev) => new Set(prev).add(callback));\n\n    return () => {\n      setChangeCallbacks((prev) => {\n        const newSet = new Set(prev);\n        newSet.delete(callback);\n        return newSet;\n      });\n    };\n  }, []);\n\n  return { notifyAvatarChange, onAvatarChange, handleError };\n};\n/**\n * Hook for avatar caching and persistence\n * Manages localStorage operations, cache size limits, and state management\n */\nconst useAvatarCache = (\n  persist: boolean,\n  maxCacheSize: number,\n  handleError: (error: unknown, context?: string) => void\n) => {\n  // allow undefined values in the cache\n  const [userAvatars, setUserAvatars] = useState<Record<string, AvatarData | undefined>>({});\n  const [roomAvatars, setRoomAvatars] = useState<Record<string, AvatarData | undefined>>({});\n  const isInitialized = useRef(false);\n  const isImporting = useRef(false);\n\n  // helper to strip undefined entries\n  // Load persisted data on mount\n  useEffect(() => {\n    if (!persist || isInitialized.current) return;\n\n    try {\n      const saved = localStorage.getItem(STORAGE_KEY);\n      if (saved) {\n        const parsed: unknown = JSON.parse(saved);\n\n        if (isValidPersistedData(parsed)) {\n          if (parsed.version === AVATAR_DATA_VERSION) {\n            setUserAvatars(parsed.userAvatars);\n            setRoomAvatars(parsed.roomAvatars);\n          } else {\n            handleError(\n              new Error(`Mismatched avatar data version: ${String(parsed.version)}`),\n              'Loading persisted avatars'\n            );\n          }\n        } else {\n          handleError(\n            new Error('Invalid avatar data format in localStorage'),\n            'Loading persisted avatars'\n          );\n        }\n      }\n    } catch (error) {\n      handleError(error, 'Loading persisted avatars');\n    }\n\n    isInitialized.current = true;\n  }, [persist, handleError]);\n\n  // Persist data when avatars change (clean out undefined first)\n  useEffect(() => {\n    if (!persist || !isInitialized.current || isImporting.current) return;\n\n    try {\n      const data: PersistedAvatarData = {\n        userAvatars: cleanCache(userAvatars),\n        roomAvatars: cleanCache(roomAvatars),\n        version: AVATAR_DATA_VERSION,\n      };\n      localStorage.setItem(STORAGE_KEY, JSON.stringify(data));\n    } catch (error) {\n      handleError(error, 'Saving persisted avatars');\n    }\n  }, [userAvatars, roomAvatars, persist, handleError]);\n\n  // Reset the importing flag after state changes are committed\n  useLayoutEffect(() => {\n    if (isImporting.current) {\n      isImporting.current = false;\n    }\n  }, [userAvatars, roomAvatars]);\n\n  /**\n   * Manages cache size to prevent memory issues\n   */\n  const manageCacheSize = useCallback(\n    (\n      currentCache: Record<string, AvatarData | undefined>\n    ): Record<string, AvatarData | undefined> => {\n      if (maxCacheSize === 0 || Object.keys(currentCache).length < maxCacheSize) {\n        return currentCache;\n      }\n\n      // Remove oldest entries (simple LRU-like behavior)\n      const entries = Object.entries(currentCache);\n      const toKeep = entries.slice(1); // Remove first entry\n      return Object.fromEntries(toKeep);\n    },\n    [maxCacheSize]\n  );\n\n  // Cache management methods\n  const clearUserAvatars = useCallback(() => {\n    setUserAvatars({});\n  }, []);\n  const clearRoomAvatars = useCallback(() => {\n    setRoomAvatars({});\n  }, []);\n  const clearAllAvatars = useCallback(() => {\n    setUserAvatars({});\n    setRoomAvatars({});\n  }, []);\n\n  // Getter methods\n  const getUserAvatars = useCallback(() => userAvatars, [userAvatars]);\n  const getRoomAvatars = useCallback(() => roomAvatars, [roomAvatars]);\n\n  // Import/export functionality\n  const exportAvatars = useCallback(\n    (): PersistedAvatarData => ({\n      userAvatars: cleanCache(userAvatars),\n      roomAvatars: cleanCache(roomAvatars),\n      version: AVATAR_DATA_VERSION,\n    }),\n    [userAvatars, roomAvatars]\n  );\n\n  const importAvatars = useCallback(\n    (data: PersistedAvatarData) => {\n      try {\n        if (data.version === AVATAR_DATA_VERSION) {\n          // Set the importing flag to prevent the persistence effect from running\n          isImporting.current = true;\n          setUserAvatars(data.userAvatars);\n          setRoomAvatars(data.roomAvatars);\n        } else {\n          handleError(\n            new Error(`Unsupported avatar data version: ${String(data.version)}`),\n            'Importing avatars'\n          );\n        }\n      } catch (error) {\n        handleError(error, 'Importing avatars');\n      }\n    },\n    [handleError]\n  );\n\n  return {\n    userAvatars,\n    roomAvatars,\n    setUserAvatars,\n    setRoomAvatars,\n    manageCacheSize,\n    clearUserAvatars,\n    clearRoomAvatars,\n    clearAllAvatars,\n    getUserAvatars,\n    getRoomAvatars,\n    exportAvatars,\n    importAvatars,\n  };\n};\n\n/**\n * Props for the AvatarProvider component\n */\nexport interface AvatarProviderProps {\n  children: React.ReactNode;\n  options?: AvatarOptions;\n}\n\n/**\n * AvatarProvider manages avatar state, caching, and persistence.\n *\n * Features:\n * - Automatic avatar generation with deterministic colors and initials\n * - Persistent caching in localStorage (configurable)\n * - Change notifications for avatar updates\n * - Import/export functionality to backup/restore avatars\n * - Memory management with configurable cache limits\n *\n * This provider uses the following hooks:\n * - useAvatarCache: Handles caching and persistence\n * - useAvatarGeneration: Handles color and initial generation\n * - useAvatarNotifications: Handles change callbacks\n *\n * @example\n * // Basic usage\n * <AvatarProvider>\n *   <ChatApplication />\n * </AvatarProvider>\n *\n * @example\n * // With custom configuration\n * <AvatarProvider\n *   options={{\n *     persist: true,\n *     maxCacheSize: 50,\n *     customColors: ['bg-brand-500', 'bg-brand-600']\n *   }}\n * >\n *   <ChatApplication />\n * </AvatarProvider>\n */\nexport const AvatarProvider = ({ children, options = {} }: AvatarProviderProps) => {\n  const { persist = true, customColors, maxCacheSize = 100, onError } = options;\n  const { generateColor, generateInitials } = useAvatarGeneration(customColors);\n  const { notifyAvatarChange, onAvatarChange, handleError } = useAvatarNotifications(onError);\n\n  const {\n    userAvatars,\n    roomAvatars,\n    setUserAvatars,\n    setRoomAvatars,\n    manageCacheSize,\n    clearUserAvatars,\n    clearRoomAvatars,\n    clearAllAvatars,\n    getUserAvatars,\n    getRoomAvatars,\n    exportAvatars,\n    importAvatars,\n  } = useAvatarCache(persist, maxCacheSize, handleError);\n\n  /**\n   * Gets an avatar for a user if it exists in the cache\n   * @param clientId - The unique identifier for the user\n   * @returns The avatar data if it exists, undefined otherwise\n   */\n  const getAvatarForUser = useCallback(\n    (clientId: string): AvatarData | undefined => {\n      return userAvatars[clientId];\n    },\n    [userAvatars]\n  );\n\n  /**\n   * Creates an avatar for a user and adds it to the cache\n   * @param clientId - The unique identifier for the user\n   * @param displayName - Optional display name (defaults to clientId if not provided)\n   * @returns The created avatar data\n   */\n  const createAvatarForUser = useCallback(\n    (clientId: string, displayName?: string): AvatarData => {\n      const name = displayName || clientId;\n      const newAvatar: AvatarData = {\n        displayName: name,\n        color: generateColor(clientId),\n        initials: generateInitials(name),\n      };\n\n      // Update cache with size management\n      setUserAvatars((prev) => {\n        const managed = manageCacheSize(prev);\n        const updated = { ...managed, [clientId]: newAvatar };\n\n        notifyAvatarChange('user', clientId, newAvatar);\n\n        return updated;\n      });\n\n      return newAvatar;\n    },\n    [generateColor, generateInitials, manageCacheSize, notifyAvatarChange, setUserAvatars]\n  );\n\n  /**\n   * Gets an avatar for a room if it exists in the cache\n   * @param roomName - The unique identifier for the room\n   * @returns The avatar data if it exists, undefined otherwise\n   */\n  const getAvatarForRoom = useCallback(\n    (roomName: string): AvatarData | undefined => {\n      return roomAvatars[roomName];\n    },\n    [roomAvatars]\n  );\n\n  /**\n   * Creates an avatar for a room and adds it to the cache\n   * @param roomName - The unique identifier for the room\n   * @param displayName - Optional display name (defaults to roomName if not provided)\n   * @returns The created avatar data\n   */\n  const createAvatarForRoom = useCallback(\n    (roomName: string, displayName?: string): AvatarData => {\n      const name = displayName || roomName;\n      const newAvatar: AvatarData = {\n        displayName: name,\n        color: generateColor(roomName),\n        initials: generateInitials(name),\n      };\n\n      // Update cache with size management\n      setRoomAvatars((prev) => {\n        const managed = manageCacheSize(prev);\n        const updated = { ...managed, [roomName]: newAvatar };\n\n        notifyAvatarChange('room', roomName, newAvatar);\n\n        return updated;\n      });\n\n      return newAvatar;\n    },\n    [generateColor, generateInitials, manageCacheSize, notifyAvatarChange, setRoomAvatars]\n  );\n\n  /**\n   * Updates or creates a user avatar with change notifications\n   */\n  const setUserAvatar = useCallback(\n    (clientId: string, avatar: Partial<AvatarData>) => {\n      setUserAvatars((prev) => {\n        const existing = prev[clientId];\n        const name = avatar.displayName || existing?.displayName || clientId;\n\n        const updatedAvatar: AvatarData = {\n          displayName: name,\n          color: avatar.color || existing?.color || generateColor(clientId),\n          initials: avatar.initials || existing?.initials || generateInitials(name),\n          src: avatar.src || existing?.src,\n        };\n\n        notifyAvatarChange('user', clientId, updatedAvatar, existing);\n\n        return { ...prev, [clientId]: updatedAvatar };\n      });\n    },\n    [generateColor, generateInitials, notifyAvatarChange, setUserAvatars]\n  );\n\n  /**\n   * Updates or creates a room avatar with change notifications\n   */\n  const setRoomAvatar = useCallback(\n    (roomName: string, avatar: Partial<AvatarData>) => {\n      setRoomAvatars((prev) => {\n        const existing = prev[roomName];\n        const name = avatar.displayName || existing?.displayName || roomName;\n\n        const updatedAvatar: AvatarData = {\n          displayName: name,\n          color: avatar.color || existing?.color || generateColor(roomName),\n          initials: avatar.initials || existing?.initials || generateInitials(name),\n          src: avatar.src || existing?.src,\n        };\n\n        notifyAvatarChange('room', roomName, updatedAvatar, existing);\n\n        return { ...prev, [roomName]: updatedAvatar };\n      });\n    },\n    [generateColor, generateInitials, notifyAvatarChange, setRoomAvatars]\n  );\n\n  // Memoize context value to prevent unnecessary re-renders\n  const contextValue = useMemo(\n    () => ({\n      getAvatarForUser,\n      createAvatarForUser,\n      getAvatarForRoom,\n      createAvatarForRoom,\n      setUserAvatar,\n      setRoomAvatar,\n      getUserAvatars,\n      getRoomAvatars,\n      clearUserAvatars,\n      clearRoomAvatars,\n      clearAllAvatars,\n      onAvatarChange,\n      exportAvatars,\n      importAvatars,\n    }),\n    [\n      getAvatarForUser,\n      createAvatarForUser,\n      getAvatarForRoom,\n      createAvatarForRoom,\n      setUserAvatar,\n      setRoomAvatar,\n      getUserAvatars,\n      getRoomAvatars,\n      clearUserAvatars,\n      clearRoomAvatars,\n      clearAllAvatars,\n      onAvatarChange,\n      exportAvatars,\n      importAvatars,\n    ]\n  );\n\n  return <AvatarContext.Provider value={contextValue}>{children}</AvatarContext.Provider>;\n};\n","import React, { ReactNode } from 'react';\n\nimport {\n  ChatSettings,\n  ChatSettingsContext,\n  ChatSettingsContextType,\n} from '../context/chat-settings-context.tsx';\n\nexport const DEFAULT_SETTINGS: ChatSettings = {\n  allowMessageUpdatesOwn: true,\n  allowMessageUpdatesAny: false,\n  allowMessageDeletesOwn: true,\n  allowMessageDeletesAny: false,\n  allowMessageReactions: true,\n};\n\n/**\n * Props for the ChatSettingsProvider component.\n *\n */\nexport interface ChatSettingsProviderProps {\n  /** Child components that will have access to chat settings */\n  children?: ReactNode;\n  /**\n   * Initial global settings. Will be merged with defaults.\n   * @defaultValue `{}`\n   */\n  initialGlobalSettings?: Partial<ChatSettings>;\n  /**\n   * Initial room-specific settings mapping.\n   * @defaultValue `{}`\n   */\n  initialRoomSettings?: Record<string, Partial<ChatSettings>>;\n}\n\n/**\n * Provider component that manages global and room-level chat settings.\n *\n * This component provides a context for managing chat functionality settings\n * across an application. It supports both global default settings and\n * room-specific overrides. The settings control whether certain UI features are enabled/disabled,\n * but do not affect the underlying Ably Chat functionality. If you wish to ensure no user can edit or delete messages,\n * you must also configure the Ably client capabilities accordingly.\n *\n * @example\n * ```tsx\n * const globalSettings = {\n *   allowMessageUpdatesOwn: true,\n *   allowMessageUpdatesAny: false,\n *   allowMessageDeletesOwn: true,\n *   allowMessageDeletesAny: false,\n *   allowMessageReactions: true\n * };\n *\n * const roomSettings = {\n *   'general': {\n *     allowMessageUpdatesOwn: true,\n *     allowMessageUpdatesAny: true // Allow user to update any message in general room\n *   },\n *   'announcements': {\n *     allowMessageUpdatesOwn: false,\n *     allowMessageUpdatesAny: false,\n *     allowMessageDeletesOwn: false,\n *     allowMessageDeletesAny: true // Allow user to delete any messages in announcements\n *   }\n * };\n *\n * <ChatSettingsProvider\n *   initialGlobalSettings={globalSettings}\n *   initialRoomSettings={roomSettings}\n * >\n *   <ChatApp />\n * </ChatSettingsProvider>\n * ```\n *\n * @param ChatSettingsProviderProps - Props for the provider component\n * @returns {@link ChatSettingsProvider} component that wraps children components.\n *\n * @public\n */\nexport const ChatSettingsProvider = ({\n  initialGlobalSettings = {},\n  initialRoomSettings = {},\n  children,\n}: ChatSettingsProviderProps) => {\n  // Merge initial global settings with defaults\n  const globalSettings: ChatSettings = {\n    ...DEFAULT_SETTINGS,\n    ...initialGlobalSettings,\n  };\n\n  /**\n   * Get effective settings for a room by merging global and room-specific settings.\n   * Returns a frozen copy to prevent accidental mutations.\n   */\n  const getEffectiveSettings = (roomName?: string): ChatSettings => {\n    if (!roomName) {\n      return Object.freeze({ ...globalSettings });\n    }\n\n    const roomSpecificSettings = initialRoomSettings[roomName];\n\n    return Object.freeze({\n      ...globalSettings,\n      ...roomSpecificSettings,\n    });\n  };\n\n  const contextValue: ChatSettingsContextType = {\n    globalSettings: Object.freeze({ ...globalSettings }),\n    roomSettings: initialRoomSettings,\n    getEffectiveSettings,\n  };\n\n  return (\n    <ChatSettingsContext.Provider value={contextValue}>{children}</ChatSettingsContext.Provider>\n  );\n};\n","import React, { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport { ThemeContext } from '../context/theme-context.tsx';\n\n/**\n * Storage key for persisting theme preference in localStorage\n */\nconst THEME_STORAGE_KEY = 'ably-chat-ui-theme';\n/**\n * Supported theme types\n */\nexport type ThemeType = 'light' | 'dark';\n/**\n * Callback function type for theme change events\n */\nexport type ThemeChangeCallback = (theme: ThemeType, previousTheme: ThemeType) => void;\n\n/**\n * Configuration options for theme management\n */\nexport interface ThemeOptions {\n  /**\n   * Whether to persist theme preference to localStorage\n   * @default true\n   */\n  persist?: boolean;\n\n  /**\n   * Whether to detect and use system theme preference\n   * @default true\n   */\n  detectSystemTheme?: boolean;\n\n  /**\n   * Initial theme to use if no preference is found\n   * @default 'light'\n   */\n  defaultTheme?: ThemeType;\n}\n\n/**\n * Props for the ThemeProvider component\n */\nexport interface ThemeProviderProps {\n  /**\n   * Child components that will have access to the theme context\n   */\n  children: ReactNode;\n\n  /**\n   * Configuration options for theme management\n   */\n  options?: ThemeOptions;\n\n  /**\n   * Callback fired when the theme changes\n   */\n  onThemeChange?: ThemeChangeCallback;\n}\n\n/**\n * ThemeProvider manages theme state, persistence, and system integration across the application\n *\n * Features:\n * - Light/dark theme management with type safety\n * - Persistent theme preference in localStorage\n * - System theme preference detection and integration\n * - Change notifications for theme updates\n * - Performance optimizations with memoization\n * - Accessibility support with proper DOM updates\n *\n * TODO: Adding more themes for:\n * - High contrast mode for accessibility\n * - Custom user/brand themes\n *\n * @example\n * // Basic usage\n * <ThemeProvider>\n *   <ChatApplication />\n * </ThemeProvider>\n *\n * @example\n * // With custom configuration\n * <ThemeProvider\n *   options={{\n *     persist: true,\n *     detectSystemTheme: true,\n *     defaultTheme: 'dark'\n *   }}\n *   onThemeChange={(theme, prev) => {\n *     console.log(`Theme changed from ${prev} to ${theme}`);\n *   }}\n * >\n *   <ChatApplication />\n * </ThemeProvider>\n */\nexport const ThemeProvider = ({\n  children,\n  options = {},\n  onThemeChange: externalOnThemeChange,\n}: ThemeProviderProps) => {\n  const { persist = true, detectSystemTheme = true, defaultTheme = 'light' } = options;\n\n  const [theme, setThemeState] = useState<ThemeType>(defaultTheme);\n  const [changeCallbacks, setChangeCallbacks] = useState<Set<ThemeChangeCallback>>(new Set());\n  const isInitialized = useRef(false);\n\n  /**\n   * Detects the system theme preference\n   */\n  const supportsSystemTheme =\n    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n    globalThis.window !== undefined && typeof globalThis.window.matchMedia === 'function';\n\n  const getSystemTheme = useCallback<() => ThemeType | undefined>(() => {\n    if (!supportsSystemTheme) {\n      return; // SSR / old browser → not available\n    }\n\n    return globalThis.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n  }, [supportsSystemTheme]);\n\n  /**\n   * Notifies all registered callbacks about theme changes\n   */\n  const notifyThemeChange = useCallback(\n    (newTheme: ThemeType, previousTheme: ThemeType) => {\n      if (newTheme === previousTheme) return;\n\n      for (const callback of changeCallbacks) {\n        try {\n          callback(newTheme, previousTheme);\n        } catch (error) {\n          if (process.env.NODE_ENV === 'development') {\n            console.error('Error in theme change callback:', error);\n          }\n        }\n      }\n\n      // Notify external callback if provided\n      if (externalOnThemeChange) {\n        try {\n          externalOnThemeChange(newTheme, previousTheme);\n        } catch (error) {\n          if (process.env.NODE_ENV === 'development') {\n            console.error('Error in external theme change callback:', error);\n          }\n        }\n      }\n    },\n    [changeCallbacks, externalOnThemeChange]\n  );\n\n  /**\n   * Sets the theme with change notifications and persistence\n   */\n  const setTheme = useCallback(\n    (newTheme: ThemeType) => {\n      setThemeState((prevTheme) => {\n        if (prevTheme !== newTheme) {\n          // Persist theme preference\n          if (persist) {\n            try {\n              localStorage.setItem(THEME_STORAGE_KEY, newTheme);\n            } catch (error) {\n              if (process.env.NODE_ENV === 'development') {\n                console.warn('Failed to persist theme preference:', error);\n              }\n            }\n          }\n\n          // Notify change\n          notifyThemeChange(newTheme, prevTheme);\n        }\n        return newTheme;\n      });\n    },\n    [persist, notifyThemeChange]\n  );\n\n  /**\n   * Toggles between light and dark themes\n   */\n  const toggleTheme = useCallback(() => {\n    setTheme(theme === 'light' ? 'dark' : 'light');\n  }, [theme, setTheme]);\n\n  /**\n   * Resets theme to system preference or default\n   */\n  const resetToSystemTheme = useCallback(() => {\n    const systemTheme = getSystemTheme();\n    setTheme(systemTheme || defaultTheme);\n  }, [getSystemTheme, setTheme, defaultTheme]);\n\n  /**\n   * Registers a callback for theme change events\n   */\n  const onThemeChange = useCallback((callback: ThemeChangeCallback) => {\n    setChangeCallbacks((prev) => new Set(prev).add(callback));\n\n    return () => {\n      setChangeCallbacks((prev) => {\n        const newSet = new Set(prev);\n        newSet.delete(callback);\n        return newSet;\n      });\n    };\n  }, []);\n\n  // Initialize theme from localStorage or system preference\n  useEffect(() => {\n    if (isInitialized.current) return;\n\n    let initialTheme = defaultTheme;\n\n    // Try to get saved theme preference\n    if (persist) {\n      try {\n        const savedTheme = localStorage.getItem(THEME_STORAGE_KEY);\n        if (savedTheme === 'light' || savedTheme === 'dark') {\n          initialTheme = savedTheme;\n        }\n      } catch (error) {\n        if (process.env.NODE_ENV === 'development') {\n          console.warn('Failed to load saved theme preference:', error);\n        }\n      }\n    }\n\n    // Fall back to system theme if no saved preference and detection is enabled\n    if ((detectSystemTheme && !persist) || (persist && !localStorage.getItem(THEME_STORAGE_KEY))) {\n      const systemTheme = getSystemTheme();\n      if (systemTheme) {\n        initialTheme = systemTheme;\n      }\n    }\n\n    setThemeState(initialTheme);\n    isInitialized.current = true;\n  }, [persist, detectSystemTheme, defaultTheme, getSystemTheme]);\n\n  // Listen for system theme changes\n  useEffect(() => {\n    if (!supportsSystemTheme) return;\n\n    const mql = globalThis.matchMedia('(prefers-color-scheme: dark)');\n    const handle = (e: MediaQueryListEvent) => {\n      setTheme(e.matches ? 'dark' : 'light');\n    };\n\n    if (typeof mql.addEventListener === 'function') {\n      mql.addEventListener('change', handle);\n      return () => {\n        mql.removeEventListener('change', handle);\n      };\n    }\n\n    // eslint-disable-next-line @typescript-eslint/no-deprecated\n    mql.addListener(handle);\n    return () => {\n      // eslint-disable-next-line @typescript-eslint/no-deprecated\n      mql.removeListener(handle);\n    };\n  }, [setTheme, supportsSystemTheme]);\n\n  // Apply theme to document element\n  useEffect(() => {\n    if (!isInitialized.current) return;\n\n    const root = document.documentElement;\n\n    // Update data attribute for CSS targeting\n    root.dataset.theme = theme;\n\n    // Update class for Tailwind dark mode\n    root.classList.toggle('dark', theme === 'dark');\n\n    // Update meta theme-color for mobile browsers\n    const themeColorMeta = document.querySelector('meta[name=\"theme-color\"]');\n    if (themeColorMeta) {\n      themeColorMeta.setAttribute('content', theme === 'dark' ? '#1f2937' : '#ffffff');\n    }\n  }, [theme]);\n\n  // Derived state\n  const isDark = useMemo(() => theme === 'dark', [theme]);\n  const isLight = useMemo(() => theme === 'light', [theme]);\n\n  // Memoize context value to prevent unnecessary re-renders\n  const contextValue = useMemo(\n    () => ({\n      theme,\n      toggleTheme,\n      setTheme,\n      isDark,\n      isLight,\n      supportsSystemTheme,\n      getSystemTheme,\n      resetToSystemTheme,\n      onThemeChange,\n    }),\n    [\n      theme,\n      toggleTheme,\n      setTheme,\n      isDark,\n      isLight,\n      supportsSystemTheme,\n      getSystemTheme,\n      resetToSystemTheme,\n      onThemeChange,\n    ]\n  );\n\n  return <ThemeContext.Provider value={contextValue}>{children}</ThemeContext.Provider>;\n};\n","declare global {\n  var __ABLY_CHAT_REACT_UI_COMPONENTS_VERSION__: string;\n}\n\nglobalThis.__ABLY_CHAT_REACT_UI_COMPONENTS_VERSION__ = '0.1.2';\nexport * from './app/index.js';\nexport * from './components/molecules/index.js';\nexport * from './context/index.js';\nexport * from './hooks/index.js';\nexport * from './providers/index.js';\nimport './style.css';\n"],"names":["r","e","t","f","n","o","clsx","AppLoading","width","height","containerStyle","useMemo","jsx","jsxs","useMessageWindow","windowSize","overscan","historyBatchSize","nextPageRef","useRef","serialSetRef","initialHistoryLoadedRef","recoveringRef","allMessagesRef","version","setVersion","useState","activeMessages","setActiveMessages","anchorIdx","setAnchorIdx","loading","setLoading","roomName","useRoom","useEffect","historyBeforeSubscribe","useMessages","event","message","type","ChatMessageEventType","updateMessages","prevVersion","messageSerial","changed","idx","findMessageIndex","currentMessage","merged","messages","lastReceivedMessage","handleDiscontinuity","hasMoreHistory","setHasMoreHistory","useCallback","targetSerial","reverse","left","right","mid","midMessage","midSerial","findInsertionIndex","newMessage","msgs","prepend","insertedBeforeAnchor","allMessages","m","existingMessage","firstMessage","lastMessage","insIdx","a","recoverFromSerial","page","nextPage","error","cancelled","loadMoreHistory","computeWindow","arr","anchor","latest","half","start","end","showLatestMessages","scrollBy","delta","prev","next","showMessagesAroundSerial","serial","AvatarContext","createContext","useAvatar","context","useContext","useUserAvatar","clientId","displayName","getAvatarForUser","createAvatarForUser","updateUserAvatar","avatar","setAvatar","existingAvatar","newAvatar","setUserAvatar","avatarData","getRandomColor","text","colors","hash","i","colorIndex","Avatar","src","alt","color","size","initials","onClick","imgError","setImgError","sizeClasses","avatarColor","getDisplayText","words","word","firstChar","secondChar","handleImageError","showingImage","DefaultSpinner","spinnerSizes","Button","variant","children","className","leftIcon","rightIcon","fullWidth","loadingSpinner","disabled","props","baseClasses","variantClasses","isDisabled","Icon","name","ariaLabel","ariaHidden","svgProps","colorClasses","iconPaths","iconPath","isInteractive","shouldHideFromScreenReader","TextInput","React","success","prefix","suffix","multiline","maxHeight","value","onChange","ariaInvalid","ref","textareaRef","multilineClasses","computedAriaInvalid","autoResizeTextarea","textarea","newHeight","renderTextarea","element","renderInput","Tooltip","position","maxWidth","wrap","showArrow","zIndex","spacing","role","rest","positionClasses","wrapClasses","spacingClasses","ConfirmDialog","isOpen","onClose","onConfirm","title","confirmText","cancelText","confirmVariant","icon","handleEscapeKey","handleConfirm","Fragment","DEFAULT_EMOJI_STORAGE_KEY","emojis","EmojiPicker","onEmojiSelect","columns","emojiList","recentEmojis","setRecentEmojis","stored","parsed","handleEmojiSelect","emoji","filtered","updated","handleKeyDown","displayEmojis","emojiButtons","recentEmojiButtons","ChatSettingsContext","useChatSettings","MessageActions","onReactionButtonClicked","onEditButtonClicked","onDeleteButtonClicked","isOwn","getEffectiveSettings","settings","allowMessageUpdatesOwn","allowMessageUpdatesAny","allowMessageDeletesOwn","allowMessageDeletesAny","allowMessageReactions","hasReactionAction","hasEditAction","hasDeleteAction","MessageReactions","onReactionClick","currentClientId","distinct","emojiNames","reaction","hasUserReacted","count","formatTime","timestamp","messageDate","today","ChatMessage","onEdit","onDelete","onReactionAdd","onReactionRemove","isHovered","setIsHovered","isEditing","setIsEditing","editText","setEditText","showEmojiPicker","setShowEmojiPicker","emojiPickerPosition","setEmojiPickerPosition","showAvatarTooltip","setShowAvatarTooltip","tooltipPosition","setTooltipPosition","showDeleteConfirm","setShowDeleteConfirm","messageRef","messageBubbleRef","avatarRef","useChatClient","userAvatar","handleEdit","handleSaveEdit","handleCancelEdit","handleDelete","handleConfirmDelete","calculateEmojiPickerPosition","bubbleRect","_a","pickerWidth","pickerHeight","top","spaceAbove","requiredSpaceAbove","maxLeft","minLeft","minGap","maxTop","minTop","handleAddReaction","handleResize","handleReactionClick","handleKeyPress","handleAvatarMouseEnter","rect","tooltipHeight","spaceBelow","handleAvatarMouseLeave","calculateTooltipPosition","avatarRect","tooltipY","avatarCenter","coords","createPortal","ANIMATION_DELAYS","TypingDots","dotSizeClassName","dotClassName","animationDuration","dotColor","delay","TypingIndicators","maxClients","textClassName","onTypingChange","currentlyTyping","useTyping","activeTypingUsers","id","buildTypingSentence","clientIds","safeMax","displayNames","remaining","ChatMessageList","forwardRef","onLoadMoreHistory","isLoading","onMessageInView","onViewLatest","autoScroll","loadMoreThreshold","enableTypingIndicators","containerRef","lastScrollCheck","shouldStickAfterPrepend","prevScrollHeight","messagesMapRef","isAtBottom","setIsAtBottom","centerSerial","setCenterSerial","isUserAtBottom","scrollTop","scrollHeight","clientHeight","updateIsAtBottom","atBottom","maybeLoadHistory","reportMessageInView","viewportCenter","best","el","bottom","d","handleScroll","now","scrollToBottom","handleTypingChange","useLayoutEffect","node","resizeObs","setRefs","msg","setEl","ChatWindowFooter","ChatWindowHeader","MessageInput","onSent","placeholder","onSendError","enableTyping","setMessage","inputRef","keystroke","stop","sendMessage","handleSend","trimmedMessage","sentMessage","handleInputChange","newValue","handleEmojiButtonClick","button","input","newCursorPosition","ChatWindow","customHeaderContent","customFooterContent","autoEnterPresence","onError","usePresence","deleteMessage","updateMessage","sendReaction","deleteReaction","handleRESTMessageUpdate","handleMessageUpdate","newText","handleMessageDelete","handleReactionAdd","MessageReactionType","handleReactionRemove","EmptyState","action","verticalAlign","textAlign","maxWidthClasses","verticalAlignClasses","textAlignClasses","useRoomAvatar","getAvatarForRoom","createAvatarForRoom","updateRoomAvatar","setRoomAvatar","Participant","isPresent","isSelf","isTyping","propAvatar","statusText","getParticipantStatus","ParticipantList","presenceData","onToggle","presentCount","sortedParticipants","b","member","PresenceCount","PresenceIndicators","usePresenceListener","presenceText","setPresenceText","p","isAnyonePresent","buildPresenceSentence","names","PresenceList","tooltipClassName","RoomInfo","propRoomAvatar","showTooltip","setShowTooltip","tooltipCoords","setTooltipCoords","setIsOpen","roomAvatar","roomAvatarData","handleMouseEnter","finalTooltipPosition","horizontalCenter","verticalPos","useThrottle","fn","timeoutRef","lastArgsRef","isThrottledRef","cleanup","args","EmojiBurst","isActive","duration","onComplete","setEmojis","newEmojis","emojiVariants","angle","speed","animationFrame","startTime","animate","progress","currentEmojis","DEFAULT_EMOJIS","EmojiWheel","customEmojis","isAnimating","setIsAnimating","handleClickOutside","timer","setWindowSize","radius","buttonSize","wheelSize","minMargin","safePosition","index","x","y","RoomReaction","emojiBurstDuration","initialEmojiBurstPosition","showEmojiBurst","setShowEmojiBurst","emojiBurstPosition","setEmojiBurstPosition","burstEmoji","setBurstEmoji","showEmojiWheel","setShowEmojiWheel","emojiWheelPosition","setEmojiWheelPosition","defaultEmoji","setDefaultEmoji","reactionButtonRef","longPressTimerRef","isLongPressRef","handleIncomingReaction","throttledHandleIncomingReaction","sendRoomReaction","useRoomReactions","showLocalBurst","sendReactionToRoom","throttledSendReaction","handleMouseDown","handleMouseUp","handleTouchStart","handleTouchEnd","handleEmojiWheelClose","handleEmojiBurstComplete","ThemeContext","useTheme","CreateRoomModal","onCreateRoom","setRoomName","handleClose","handleSubmit","DropdownMenu","trigger","items","align","dropdownRef","handleItemClick","item","RoomListItem","isSelected","onLeave","isCollapsed","typingIndicatorsEnabled","connections","presenceMembers","useOccupancy","RoomList","roomNames","activeRoomName","defaultRoomOptions","onSelect","ChatRoomProvider","Sidebar","addRoom","setActiveRoom","leaveRoom","onToggleCollapse","showCreateModal","setShowCreateModal","theme","toggleTheme","DEFAULT_ROOM_OPTIONS","App","initialRoomNames","currentStatus","useChatConnection","setRoomNames","activeRoom","isSidebarCollapsed","setIsSidebarCollapsed","handleChangeSelectedRoom","handleToggleSidebar","ConnectionStatus","PRESET_AVATARS","COLOR_OPTIONS","AvatarEditor","currentAvatar","currentColor","onSave","avatarUrl","setAvatarUrl","selectedColor","setSelectedColor","customInitials","setCustomInitials","activeTab","setActiveTab","setError","handleInitialsChange","handlePresetSelect","presetUrl","handleColorSelect","handleSave","handleRemove","getInitials","preset","cleanCache","cache","result","key","isValidPersistedData","data","obj","STORAGE_KEY","DEFAULT_AVATAR_COLORS","AVATAR_DATA_VERSION","useAvatarGeneration","customColors","avatarColors","generateColor","generateInitials","cleanName","firstInitial","secondInitial","_b","useAvatarNotifications","changeCallbacks","setChangeCallbacks","handleError","notifyAvatarChange","previousAvatar","callback","onAvatarChange","newSet","useAvatarCache","persist","maxCacheSize","userAvatars","setUserAvatars","roomAvatars","setRoomAvatars","isInitialized","isImporting","saved","manageCacheSize","currentCache","toKeep","clearUserAvatars","clearRoomAvatars","clearAllAvatars","getUserAvatars","getRoomAvatars","exportAvatars","importAvatars","AvatarProvider","options","existing","updatedAvatar","contextValue","DEFAULT_SETTINGS","ChatSettingsProvider","initialGlobalSettings","initialRoomSettings","globalSettings","roomSpecificSettings","THEME_STORAGE_KEY","ThemeProvider","externalOnThemeChange","detectSystemTheme","defaultTheme","setThemeState","supportsSystemTheme","getSystemTheme","notifyThemeChange","newTheme","previousTheme","setTheme","prevTheme","resetToSystemTheme","systemTheme","onThemeChange","initialTheme","savedTheme","mql","handle","root","themeColorMeta","isDark","isLight"],"mappings":"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","x_google_ignoreList":[0]}