{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "open-in-v0-chat", "title": "Chat Basic", "description": "A basic chat example block for bootstrapping the chat UI.", "dependencies": [ "@radix-ui/react-separator", "@radix-ui/react-slot", "class-variance-authority", "emoji-picker-react" ], "registryDependencies": [ "https://mesailor.github.io/r/chat.json", "separator", "input-group", "sidebar", "badge", "popover", "button", "dropdown-menu", "skeleton", "dialog", "avatar", "sheet" ], "files": [ { "path": "registry/new-york/blocks/chat-basic/page.tsx", "content": "\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\nimport { Fragment } from \"react/jsx-runtime\";\nimport {\n Event,\n EventFile,\n} from \"@/registry/new-york/blocks/chat-basic/data/messages\";\nimport { mockAPI } from \"@/registry/new-york/blocks/chat-basic/data/mock-api\";\nimport {\n CURRENT_USER,\n OTHER_USER,\n} from \"@/registry/new-york/blocks/chat-basic/data/users\";\nimport { useMessages } from \"@/registry/new-york/blocks/chat-basic/hooks/use-messages\";\nimport { useMessageReactions } from \"@/registry/new-york/blocks/chat-basic/hooks/use-message-reactions\";\nimport { useMessageSearch } from \"@/registry/new-york/blocks/chat-basic/hooks/use-message-search\";\nimport { useMessageActions } from \"@/registry/new-york/blocks/chat-basic/hooks/use-message-actions\";\nimport { useHighlightedMessageId } from \"@/registry/new-york/blocks/chat-basic/hooks/use-highlighted-message-id\";\nimport { useProfile } from \"@/registry/new-york/blocks/chat-basic/hooks/use-profile\";\nimport { useChatSidebar } from \"@/registry/new-york/blocks/chat-basic/hooks/use-chat-sidebar\";\nimport { useIsWider } from \"@/registry/new-york/blocks/chat-basic/hooks/use-is-wider\";\nimport {\n CheckIcon,\n PhoneIcon,\n PlusIcon,\n SearchIcon,\n SendIcon,\n SmileIcon,\n VideoIcon,\n XIcon,\n} from \"lucide-react\";\nimport { ChatHeaderActions } from \"@/registry/new-york/blocks/chat-basic/components/chat-header-actions\";\nimport {\n InputGroup,\n InputGroupAddon,\n InputGroupInput,\n} from \"@/components/ui/input-group\";\nimport { SidebarInset, SidebarProvider } from \"@/components/ui/sidebar\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Chat } from \"@/registry/new-york/chat/chat\";\nimport {\n ChatHeader,\n ChatHeaderAddon,\n ChatHeaderAvatar,\n ChatHeaderButton,\n ChatHeaderMain,\n} from \"@/registry/new-york/chat/chat-header\";\nimport {\n ChatToolbar,\n ChatToolbarAddon,\n ChatToolbarAttachment,\n ChatToolbarAttachmentButton,\n ChatToolbarButton,\n ChatToolbarTextarea,\n} from \"@/registry/new-york/chat/chat-toolbar\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport EmojiPicker, { EmojiClickData, Theme } from \"emoji-picker-react\";\nimport { ChatMessages } from \"@/registry/new-york/chat/chat-messages\";\nimport { PrimaryMessage } from \"@/registry/new-york/blocks/chat-basic/components/message-items/primary-message\";\nimport { DateItem } from \"@/registry/new-york/blocks/chat-basic/components/message-items/date-item\";\nimport { AdditionalMessage } from \"@/registry/new-york/blocks/chat-basic/components/message-items/additional-message\";\nimport { PrimaryMessageSkeleton } from \"@/registry/new-york/blocks/chat-basic/components/message-items/primary-message-skeleton\";\nimport { DateItemSkeleton } from \"@/registry/new-york/blocks/chat-basic/components/message-items/date-item-skeleton\";\nimport { SearchSidebarContent } from \"@/registry/new-york/blocks/chat-basic/components/message-search/search-sidebar-content\";\nimport { DeleteDialog } from \"@/registry/new-york/blocks/chat-basic/components/message-actions/delete-dialog\";\nimport { ProfileSidebarContent } from \"@/registry/new-york/blocks/chat-basic/components/profile/profile-sidebar-content\";\nimport { BlockDialog } from \"@/registry/new-york/blocks/chat-basic/components/profile/block-dialog\";\nimport { ChatSidebar } from \"@/registry/new-york/blocks/chat-basic/components/chat-sidebar/chat-sidebar\";\n\nexport default function ChatExampleComponent() {\n const chatContainerRef = useRef(null);\n const chatMessagesRef = useRef(null);\n const isChatWide = useIsWider(chatContainerRef, 672);\n\n const { loading, messages, setMessages } = useMessages({\n onFetch: mockAPI.getEvents,\n });\n\n const { sidebarOpen, setSidebarOpen, sidebarView, setSidebarView } =\n useChatSidebar();\n\n const { handleReaction } = useMessageReactions({\n setMessages,\n onReact: mockAPI.reactToEvent,\n });\n\n const {\n searchQuery,\n setSearchQuery,\n activeSearchQuery,\n searchResults,\n handleSearch,\n handleClearSearch,\n openSearch,\n } = useMessageSearch({\n setSidebarOpen,\n setSidebarView,\n onSearch: mockAPI.searchEvents,\n });\n\n const { highlightedMessageId, setHighlightedMessageId } =\n useHighlightedMessageId();\n\n const {\n openBlockDialog,\n setOpenBlockDialog,\n isBlocked,\n openProfile,\n handleBlock,\n handleUnblock,\n } = useProfile({\n setSidebarOpen,\n setSidebarView,\n onBlock: mockAPI.blockUser,\n onUnblock: mockAPI.unblockUser,\n });\n\n const {\n messageToDelete,\n openDeleteDialog,\n setOpenDeleteDialog,\n messageToEdit,\n handleOpenDeleteDialog,\n handleDelete,\n handleStartEdit,\n handleSubmitEdit,\n handleCancelEdit,\n } = useMessageActions({\n setMessages,\n onDelete: mockAPI.deleteEvent,\n onUpdate: mockAPI.updateEvent,\n });\n\n const handleSubmit = useCallback(\n async (submitData: { text: string; files: File[] }) => {\n // Optimistically add the new message to the UI with a temporary ID and \"sending\" status\n const tempId = Date.now();\n const newMessage: Event = {\n id: tempId,\n status: \"sending\",\n tempId: tempId,\n sender: CURRENT_USER,\n timestamp: Date.now(),\n content: {\n type: \"message\",\n ...(submitData.text && { text: submitData.text }),\n ...(submitData.files.length > 0 && {\n files: submitData.files.map((file) => ({\n url: URL.createObjectURL(file),\n fileName: file.name,\n mimeType: file.type,\n })),\n }),\n },\n };\n setMessages((prev) => [newMessage, ...prev]);\n\n // Replace the temporary message with the posted message\n const postedMessage = await mockAPI.postEvent({\n text: submitData.text,\n files: submitData.files,\n });\n setMessages((prev) =>\n prev.map((msg) => (msg.tempId === tempId ? postedMessage : msg)),\n );\n },\n [setMessages],\n );\n\n const scrollToBottom = useCallback(() => {\n chatMessagesRef.current?.scrollTo({ top: 0, behavior: \"smooth\" });\n }, []);\n\n const scrollToMessage = useCallback(\n (id: number) => {\n const container = chatMessagesRef.current;\n const element = document.getElementById(`message-${id}`);\n if (!container || !element) return;\n\n const containerRect = container.getBoundingClientRect();\n const elementRect = element.getBoundingClientRect();\n container.scrollTo({\n top:\n container.scrollTop +\n elementRect.top -\n containerRect.top -\n containerRect.height / 2 +\n elementRect.height / 2,\n behavior: \"smooth\",\n });\n\n setHighlightedMessageId(id);\n },\n [setHighlightedMessageId],\n );\n\n const handleSidebarClose = useCallback(() => {\n if (isChatWide && sidebarView === \"search\") {\n handleClearSearch();\n }\n setSidebarOpen(false);\n }, [isChatWide, sidebarView, handleClearSearch, setSidebarOpen]);\n\n return (\n <>\n \n \n \n \n \n \n Ann Smith\n {isBlocked && Blocked}\n AKA\n \n \n Front-end developer\n \n \n \n \n \n setSearchQuery(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n handleSearch(searchQuery);\n }\n }}\n />\n \n \n \n \n \n \n \n \n \n \n handleUnblock(OTHER_USER.id)}\n onBlock={() => setOpenBlockDialog(true)}\n />\n \n \n\n {\n if (!open) handleSidebarClose();\n }}\n className=\"flex-1 min-h-0\"\n >\n \n \n {loading &&\n Array.from({ length: 20 }).map((_, i) => {\n if (i % 6 === 0) {\n return (\n \n \n \n \n );\n }\n return (\n \n );\n })}\n\n {!loading &&\n messages.map((msg, i, msgs) => {\n const isOwnMessage = msg.sender.id === CURRENT_USER.id;\n // If date changed, show date item\n if (\n new Date(msg.timestamp).toDateString() !==\n new Date(msgs[i + 1]?.timestamp).toDateString()\n ) {\n return (\n \n handleReaction(msg.id, emoji)}\n onDelete={\n isOwnMessage\n ? () => handleOpenDeleteDialog(msg)\n : undefined\n }\n onEdit={\n isOwnMessage\n ? () => handleStartEdit(msg)\n : undefined\n }\n />\n \n \n );\n }\n\n // If next item is same user, show additional\n if (msg.sender.id === msgs[i + 1]?.sender.id) {\n return (\n handleReaction(msg.id, emoji)}\n onDelete={\n isOwnMessage\n ? () => handleOpenDeleteDialog(msg)\n : undefined\n }\n onEdit={\n isOwnMessage ? () => handleStartEdit(msg) : undefined\n }\n />\n );\n }\n // Else, show primary\n else {\n return (\n handleReaction(msg.id, emoji)}\n onDelete={\n isOwnMessage\n ? () => handleOpenDeleteDialog(msg)\n : undefined\n }\n onEdit={\n isOwnMessage ? () => handleStartEdit(msg) : undefined\n }\n />\n );\n }\n })}\n \n\n \n \n\n \n {sidebarView === \"search\" && (\n \n )}\n {sidebarView === \"profile\" && }\n \n \n \n \n handleBlock(OTHER_USER.id)}\n />\n \n );\n}\n\ninterface ToolbarProps {\n messageToEdit: Event | null;\n onSubmit: (data: { text: string; files: File[] }) => Promise | void;\n onSubmitEdit: (data: {\n text: string;\n uploadFiles: File[];\n editedFiles: EventFile[];\n }) => Promise | void;\n onCancelEdit: () => void;\n onScrollToBottom?: () => void;\n}\n\nfunction Toolbar({\n messageToEdit,\n onSubmit,\n onSubmitEdit,\n onCancelEdit,\n onScrollToBottom,\n}: ToolbarProps) {\n const [input, setInput] = useState(messageToEdit?.content.text ?? \"\");\n const [files, setFiles] = useState([]);\n\n const [filesToEdit, setFilesToEdit] = useState(\n messageToEdit?.content.files ?? [],\n );\n\n const [emojiOpen, setEmojiOpen] = useState(false);\n\n const handleSubmit = useCallback(() => {\n const trimmedContent = input.trim();\n if (!trimmedContent && files.length === 0) return; // Don't submit empty messages\n\n onSubmit?.({\n text: trimmedContent,\n files,\n });\n\n setInput(\"\");\n setFiles([]);\n // Scroll to top (newest message) when a new message is sent\n setTimeout(() => {\n onScrollToBottom?.();\n });\n }, [input, files, onSubmit, onScrollToBottom]);\n\n const handleSubmitEdit = useCallback(() => {\n const trimmedContent = input.trim();\n if (!trimmedContent && files.length === 0 && filesToEdit.length === 0) {\n return; // Don't submit empty messages\n }\n\n onSubmitEdit?.({\n text: trimmedContent,\n uploadFiles: files,\n editedFiles: filesToEdit,\n });\n\n setInput(\"\");\n setFiles([]);\n }, [input, files, filesToEdit, onSubmitEdit]);\n\n return (\n \n {(files.length > 0 || filesToEdit.length > 0) && (\n \n {files.map((file, i) => (\n \n setFiles((prev) => prev.filter((_, idx) => idx !== i))\n }\n />\n ))}\n {filesToEdit.map((file, i) => (\n \n setFilesToEdit((prev) => prev.filter((_, idx) => idx !== i))\n }\n />\n ))}\n \n )}\n\n \n {\n setFiles((prev) => [...prev, ...files]);\n }}\n >\n \n \n \n \n \n \n \n \n \n {\n setInput((prev) => prev + emojiData.emoji);\n setEmojiOpen(false);\n }}\n />\n \n \n \n\n
\n setInput(e.target.value)}\n onSubmit={() => (messageToEdit ? handleSubmitEdit() : handleSubmit())}\n />\n
\n\n \n {messageToEdit && (\n <>\n \n \n \n handleSubmitEdit()}\n >\n \n \n \n )}\n {!messageToEdit && (\n handleSubmit()}\n onMouseDown={(e) => {\n e.preventDefault();\n }}\n >\n \n \n )}\n \n
\n );\n}\n", "type": "registry:page", "target": "app/page.tsx" }, { "path": "registry/new-york/blocks/chat-basic/data/messages.ts", "content": "import { User } from \"@/registry/new-york/blocks/chat-basic/data/users\";\n\nexport type EventType = \"message\" | \"system\";\n\nexport interface EventFile {\n url: string;\n fileName: string;\n mimeType?: string;\n}\n\nexport interface EventContent {\n type: EventType;\n text?: string;\n files?: EventFile[];\n}\n\nexport interface Event {\n id: number;\n tempId?: number; // Temporary ID for optimistic UI updates\n status: \"sending\" | \"sent\" | \"failed\";\n sender: User;\n timestamp: number;\n content: EventContent;\n reactions?: string[];\n isEdited?: boolean;\n}\n\nexport const EVENTS: Event[] = [\n {\n id: 17,\n status: \"sent\",\n sender: {\n id: \"annsmith-user-id\",\n name: \"Ann Smith\",\n avatarUrl:\n \"https://cdn.jsdelivr.net/gh/alohe/avatars/png/upstream_20.png\",\n username: \"@annsmith\",\n },\n timestamp: 1234979120123,\n content: {\n type: \"message\",\n text: \"Hey John, just wanted to say - the new dashboard design looks fantastic! The way you organized the metrics is super intuitive. I can already tell our users are going to love it. Great work on this! 🙌\",\n },\n },\n {\n id: 16,\n status: \"sent\",\n sender: {\n id: \"johndoe-user-id\",\n name: \"John Doe\",\n avatarUrl:\n \"https://cdn.jsdelivr.net/gh/alohe/avatars/png/upstream_13.png\",\n username: \"@johndoe\",\n },\n timestamp: 1234878110123,\n content: {\n type: \"message\",\n text: \"Oh, and could you also share those user feedback notes? I want to make sure we're really nailing what they need before we ship this.\",\n },\n },\n {\n id: 15,\n status: \"sent\",\n sender: {\n id: \"johndoe-user-id\",\n name: \"John Doe\",\n avatarUrl:\n \"https://cdn.jsdelivr.net/gh/alohe/avatars/png/upstream_13.png\",\n username: \"@johndoe\",\n },\n timestamp: 1234878100123,\n content: {\n type: \"message\",\n text: \"Just tested the new checkout flow - it's so smooth! The loading states you added make such a difference. Customers are gonna love this! 🚀\",\n },\n },\n {\n id: 14,\n status: \"sent\",\n sender: {\n id: \"johndoe-user-id\",\n name: \"John Doe\",\n avatarUrl:\n \"https://cdn.jsdelivr.net/gh/alohe/avatars/png/upstream_13.png\",\n username: \"@johndoe\",\n },\n timestamp: 1234678090123,\n content: {\n type: \"message\",\n text: \"Sweet! I'll check it out on mobile too and let you know. Thanks for keeping accessibility in mind - that's real value right there!\",\n },\n },\n {\n id: 13,\n status: \"sent\",\n sender: {\n id: \"annsmith-user-id\",\n name: \"Ann Smith\",\n avatarUrl:\n \"https://cdn.jsdelivr.net/gh/alohe/avatars/png/upstream_20.png\",\n username: \"@annsmith\",\n },\n timestamp: 1234678080123,\n content: {\n type: \"message\",\n text: \"But first, could you give the responsive design a quick look? I want to make sure it feels great on all devices before we show it to users.\",\n },\n },\n {\n id: 12,\n status: \"sent\",\n sender: {\n id: \"annsmith-user-id\",\n name: \"Ann Smith\",\n avatarUrl:\n \"https://cdn.jsdelivr.net/gh/alohe/avatars/png/upstream_20.png\",\n username: \"@annsmith\",\n },\n timestamp: 1234678070123,\n content: {\n type: \"message\",\n text: \"Hey! Just wrapped up the dashboard redesign. Focused on making the key metrics super easy to find - think our users will really appreciate it!\",\n },\n },\n {\n id: 11,\n status: \"sent\",\n sender: {\n id: \"johndoe-user-id\",\n name: \"John Doe\",\n avatarUrl:\n \"https://cdn.jsdelivr.net/gh/alohe/avatars/png/upstream_13.png\",\n username: \"@johndoe\",\n },\n timestamp: 1234677970123,\n content: {\n type: \"message\",\n text: \"Also, how's the performance optimization going? Any wins on those load times we discussed?\",\n },\n },\n {\n id: 10,\n status: \"sent\",\n sender: {\n id: \"johndoe-user-id\",\n name: \"John Doe\",\n avatarUrl:\n \"https://cdn.jsdelivr.net/gh/alohe/avatars/png/upstream_13.png\",\n username: \"@johndoe\",\n },\n timestamp: 1234677910123,\n content: {\n type: \"message\",\n text: \"Hey! How's it going? Did you get a chance to look at those user journey mockups? Want to make sure we're solving the right problems for them.\",\n },\n },\n {\n id: 9,\n status: \"sent\",\n sender: {\n id: \"annsmith-user-id\",\n name: \"Ann Smith\",\n avatarUrl:\n \"https://cdn.jsdelivr.net/gh/alohe/avatars/png/upstream_20.png\",\n username: \"@annsmith\",\n },\n timestamp: 1234567919123,\n content: {\n type: \"message\",\n text: \"I'll ping you here once it's ready for a demo!\",\n },\n },\n {\n id: 8,\n status: \"sent\",\n sender: {\n id: \"annsmith-user-id\",\n name: \"Ann Smith\",\n avatarUrl:\n \"https://cdn.jsdelivr.net/gh/alohe/avatars/png/upstream_20.png\",\n username: \"@annsmith\",\n },\n timestamp: 1234567917123,\n content: {\n type: \"message\",\n text: \"Perfect! Time to build something awesome ✨\",\n },\n },\n {\n id: 7,\n status: \"sent\",\n sender: {\n id: \"annsmith-user-id\",\n name: \"Ann Smith\",\n avatarUrl:\n \"https://cdn.jsdelivr.net/gh/alohe/avatars/png/upstream_20.png\",\n username: \"@annsmith\",\n },\n timestamp: 1234567913123,\n content: {\n type: \"message\",\n text: \"Awesome, thanks! That totally makes sense now. I love how we're thinking about the end user experience here. Can't wait to see their reaction when this goes live!\",\n },\n },\n {\n id: 6,\n status: \"sent\",\n sender: {\n id: \"johndoe-user-id\",\n name: \"John Doe\",\n avatarUrl:\n \"https://cdn.jsdelivr.net/gh/alohe/avatars/png/upstream_13.png\",\n username: \"@johndoe\",\n },\n timestamp: 1234567910123,\n content: {\n type: \"message\",\n text: \"So basically - keep the interactions snappy, add subtle animations for feedback, and make sure error states are super clear. When users feel confident using the interface, they stick around. That's the value we're delivering! 😊\",\n },\n },\n {\n id: 5,\n status: \"sent\",\n sender: {\n id: \"johndoe-user-id\",\n name: \"John Doe\",\n avatarUrl:\n \"https://cdn.jsdelivr.net/gh/alohe/avatars/png/upstream_13.png\",\n username: \"@johndoe\",\n },\n timestamp: 1234567910123,\n content: { type: \"message\", text: \"Here's what I'm thinking:\" },\n },\n {\n id: 4,\n status: \"sent\",\n sender: {\n id: \"johndoe-user-id\",\n name: \"John Doe\",\n avatarUrl:\n \"https://cdn.jsdelivr.net/gh/alohe/avatars/png/upstream_13.png\",\n username: \"@johndoe\",\n },\n timestamp: 1234567899123,\n content: {\n type: \"message\",\n text: \"Absolutely! Let me pull up my notes from the customer interviews\",\n },\n },\n {\n id: 3,\n status: \"sent\",\n sender: {\n id: \"johndoe-user-id\",\n name: \"John Doe\",\n avatarUrl:\n \"https://cdn.jsdelivr.net/gh/alohe/avatars/png/upstream_13.png\",\n username: \"@johndoe\",\n },\n timestamp: 1234567895123,\n content: { type: \"message\", text: \"Hey! Doing great, thanks for asking!\" },\n },\n {\n id: 2,\n status: \"sent\",\n sender: {\n id: \"annsmith-user-id\",\n name: \"Ann Smith\",\n avatarUrl:\n \"https://cdn.jsdelivr.net/gh/alohe/avatars/png/upstream_20.png\",\n username: \"@annsmith\",\n },\n timestamp: 1234567892123,\n content: {\n type: \"message\",\n text: \"Could you share your thoughts on the UX flow? Want to make sure we're creating real value for our users.\",\n },\n },\n {\n id: 1,\n status: \"sent\",\n sender: {\n id: \"annsmith-user-id\",\n name: \"Ann Smith\",\n avatarUrl:\n \"https://cdn.jsdelivr.net/gh/alohe/avatars/png/upstream_20.png\",\n username: \"@annsmith\",\n },\n timestamp: 1234567890123,\n content: { type: \"message\", text: \"Hey there! How's your day going?\" },\n },\n];\n", "type": "registry:file", "target": "data/mock/messages.ts" }, { "path": "registry/new-york/blocks/chat-basic/data/users.ts", "content": "export interface User {\n id: string;\n name: string;\n avatarUrl: string;\n username: string;\n}\n\nexport const CURRENT_USER: User = {\n id: \"johndoe-user-id\",\n name: \"John Doe\",\n avatarUrl: \"https://cdn.jsdelivr.net/gh/alohe/avatars/png/upstream_13.png\",\n username: \"@johndoe\",\n};\n\nexport const OTHER_USER: User = {\n id: \"annsmith-user-id\",\n name: \"Ann Smith\",\n avatarUrl: \"https://cdn.jsdelivr.net/gh/alohe/avatars/png/upstream_20.png\",\n username: \"@annsmith\",\n};\n", "type": "registry:file", "target": "data/mock/users.ts" }, { "path": "registry/new-york/blocks/chat-basic/data/mock-api.ts", "content": "import {\n Event,\n EventContent,\n EventFile,\n EVENTS,\n} from \"@/registry/new-york/blocks/chat-basic/data/messages\";\nimport { CURRENT_USER } from \"@/registry/new-york/blocks/chat-basic/data/users\";\n\nexport const searchEvents = (query: string): Promise => {\n const q = query.trim().toLowerCase();\n if (!q) return Promise.resolve([]);\n return new Promise((resolve) => {\n setTimeout(() => {\n resolve(EVENTS.filter((e) => e.content.text?.toLowerCase().includes(q)));\n }, 150);\n });\n};\n\nexport const getEvents = () => {\n // Simulate fetching events from an API with a delay\n return new Promise((resolve) => {\n resolve(EVENTS);\n });\n};\n\nexport const postEvent = ({\n text,\n files,\n}: {\n text?: string;\n files?: File[];\n}): Promise => {\n if (!text && (!files || files.length === 0)) {\n return Promise.reject(new Error(\"Either text or files must be provided\"));\n }\n\n const content: EventContent = {\n type: \"message\",\n ...(text && { text }),\n ...(files &&\n files.length > 0 && {\n files: files.map((file) => ({\n url: URL.createObjectURL(file),\n fileName: file.name,\n mimeType: file.type || undefined,\n })),\n }),\n };\n\n const newEvent: Event = {\n id: Date.now(),\n status: \"sent\",\n sender: CURRENT_USER,\n timestamp: Date.now(),\n content,\n };\n // Simulate posting an event to an API with a delay\n return new Promise((resolve) => {\n setTimeout(() => {\n EVENTS.unshift(newEvent); // Add to the beginning since events are in reverse order\n resolve(newEvent);\n }, 1000);\n });\n};\n\nexport const deleteEvent = (id: number) => {\n return new Promise((resolve, reject) => {\n setTimeout(() => {\n const index = EVENTS.findIndex((e) => e.id === id);\n if (index !== -1) {\n EVENTS.splice(index, 1);\n resolve(id);\n } else {\n reject(new Error(\"Event not found\"));\n }\n }, 500);\n });\n};\n\nexport const updateEvent = (\n id: number,\n data: { text?: string; uploadFiles?: File[]; editedFiles?: EventFile[] },\n) => {\n return new Promise((resolve, reject) => {\n setTimeout(() => {\n const event = EVENTS.find((e) => e.id === id);\n if (!event) {\n reject(new Error(\"Event not found\"));\n return;\n }\n const newFiles: EventFile[] = (data.uploadFiles ?? []).map((file) => ({\n url: URL.createObjectURL(file),\n fileName: file.name,\n mimeType: file.type || undefined,\n }));\n const allFiles = [...(data.editedFiles ?? []), ...newFiles];\n event.content = {\n ...event.content,\n ...(data.text !== undefined && { text: data.text }),\n ...(allFiles.length > 0 ? { files: allFiles } : { files: undefined }),\n };\n event.isEdited = true;\n resolve(structuredClone(event));\n }, 500);\n });\n};\n\nconst blockUser = (userId: string): Promise => {\n console.log(`Blocking user with ID: ${userId}`);\n return new Promise((resolve) => setTimeout(resolve, 200));\n};\n\nconst unblockUser = (userId: string): Promise => {\n console.log(`Unblocking user with ID: ${userId}`);\n return new Promise((resolve) => setTimeout(resolve, 200));\n};\n\nexport const reactToEvent = (\n eventId: number,\n emoji: string,\n): Promise => {\n const event = EVENTS.find((e) => e.id === eventId);\n if (!event) return Promise.reject(new Error(\"Event not found\"));\n\n if (event.reactions?.includes(emoji)) {\n event.reactions = event.reactions.filter((r) => r !== emoji);\n } else {\n event.reactions = [emoji];\n }\n\n return new Promise((resolve) => {\n setTimeout(() => resolve(event), 200);\n });\n};\n\nexport const mockAPI = {\n blockUser,\n unblockUser,\n reactToEvent,\n searchEvents,\n deleteEvent,\n updateEvent,\n getEvents,\n postEvent,\n};\n", "type": "registry:file", "target": "data/mock/mock-api.ts" }, { "path": "registry/new-york/blocks/chat-basic/hooks/use-messages.ts", "content": "import { Event } from \"@/registry/new-york/blocks/chat-basic/data/messages\";\nimport { useEffect, useState } from \"react\";\n\nexport const useMessages = ({\n onFetch,\n}: {\n onFetch: () => Promise;\n}) => {\n const [loading, setLoading] = useState(true);\n const [messages, setMessages] = useState([]);\n\n useEffect(() => {\n const fetchMessages = async () => {\n setLoading(true);\n try {\n const fetchedMessages = await onFetch();\n setMessages(fetchedMessages);\n } finally {\n setLoading(false);\n }\n };\n fetchMessages();\n }, [onFetch]);\n\n return { loading, messages, setMessages };\n};\n", "type": "registry:hook", "target": "hooks/chat/use-messages.ts" }, { "path": "registry/new-york/blocks/chat-basic/hooks/use-message-reactions.ts", "content": "import { Event } from \"@/registry/new-york/blocks/chat-basic/data/messages\";\nimport { useCallback } from \"react\";\n\nexport const useMessageReactions = ({\n setMessages,\n onReact,\n}: {\n setMessages: React.Dispatch>;\n onReact: (eventId: number, emoji: string) => Promise;\n}) => {\n const handleReaction = useCallback(\n async (eventId: number, emoji: string) => {\n try {\n const updated = await onReact(eventId, emoji);\n setMessages((prev) =>\n prev.map((msg) => (msg.id === eventId ? updated : msg)),\n );\n } catch (error) {\n console.error(\"Failed to add reaction:\", error);\n // Optionally show a toast or other user feedback\n }\n },\n [setMessages, onReact],\n );\n\n return { handleReaction };\n};\n", "type": "registry:hook", "target": "hooks/chat/use-message-reactions.ts" }, { "path": "registry/new-york/blocks/chat-basic/hooks/use-message-search.ts", "content": "import { Event } from \"@/registry/new-york/blocks/chat-basic/data/messages\";\nimport { useCallback, useRef, useState } from \"react\";\n\nexport const useMessageSearch = ({\n setSidebarOpen,\n setSidebarView,\n onSearch,\n}: {\n setSidebarOpen: React.Dispatch>;\n setSidebarView: React.Dispatch>;\n onSearch: (query: string) => Promise;\n}) => {\n const searchIdRef = useRef(0);\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [activeSearchQuery, setActiveSearchQuery] = useState(\"\");\n const [searchResults, setSearchResults] = useState([]);\n\n const handleSearch = useCallback(\n async (query: string) => {\n const trimmed = query.trim();\n if (!trimmed) return;\n const currentSearchId = ++searchIdRef.current;\n setActiveSearchQuery(trimmed);\n setSidebarView(\"search\");\n setSidebarOpen(true);\n try {\n const results = await onSearch(trimmed);\n // Only update if this is still the most recent search\n if (currentSearchId === searchIdRef.current) {\n setSearchResults(results);\n }\n } catch (error) {\n console.error(\"Search failed:\", error);\n if (currentSearchId === searchIdRef.current) {\n setSearchResults([]);\n }\n }\n },\n [setSidebarOpen, setSidebarView, onSearch],\n );\n\n const handleClearSearch = useCallback(() => {\n setSearchQuery(\"\");\n setActiveSearchQuery(\"\");\n setSearchResults([]);\n }, []);\n\n const openSearch = useCallback(() => {\n setSidebarView(\"search\");\n setSidebarOpen(true);\n }, [setSidebarView, setSidebarOpen]);\n\n return {\n searchQuery,\n setSearchQuery,\n activeSearchQuery,\n searchResults,\n handleSearch,\n handleClearSearch,\n openSearch,\n };\n};\n", "type": "registry:hook", "target": "hooks/chat/use-message-search.ts" }, { "path": "registry/new-york/blocks/chat-basic/hooks/use-message-actions.ts", "content": "import { useCallback, useState } from \"react\";\nimport {\n Event,\n EventContent,\n EventFile,\n} from \"@/registry/new-york/blocks/chat-basic/data/messages\";\n\nexport const useMessageActions = ({\n setMessages,\n onDelete,\n onUpdate,\n}: {\n setMessages: React.Dispatch>;\n onDelete: (id: number) => Promise;\n onUpdate: (\n id: number,\n data: { text?: string; uploadFiles?: File[]; editedFiles?: EventFile[] },\n ) => Promise;\n}) => {\n const [messageToDelete, setMessageToDelete] = useState(null);\n const [openDeleteDialog, setOpenDeleteDialog] = useState(false);\n const [messageToEdit, setMessageToEdit] = useState(null);\n\n const handleOpenDeleteDialog = useCallback((event: Event) => {\n setMessageToDelete(event);\n setOpenDeleteDialog(true);\n }, []);\n\n const handleDelete = useCallback(async () => {\n setOpenDeleteDialog(false);\n if (!messageToDelete) return;\n\n try {\n const deletedMessageId = await onDelete(messageToDelete.id);\n setMessages((prev) => prev.filter((msg) => msg.id !== deletedMessageId));\n setMessageToDelete(null);\n } catch (error) {\n console.error(\"Failed to delete message:\", error);\n }\n }, [messageToDelete, onDelete, setMessages]);\n\n const handleStartEdit = useCallback((msg: Event) => {\n setMessageToEdit(msg);\n }, []);\n\n const handleSubmitEdit = useCallback(\n async (data: {\n text: string;\n uploadFiles: File[];\n editedFiles: EventFile[];\n }) => {\n if (!messageToEdit) return;\n\n // Client-side mapping only for the optimistic update\n const optimisticNewFiles: EventFile[] = data.uploadFiles.map((file) => ({\n url: URL.createObjectURL(file),\n fileName: file.name,\n mimeType: file.type,\n }));\n const optimisticAllFiles = [...data.editedFiles, ...optimisticNewFiles];\n const optimisticContent: EventContent = {\n type: \"message\",\n ...(data.text && { text: data.text }),\n ...(optimisticAllFiles.length > 0 && { files: optimisticAllFiles }),\n };\n\n // Optimistic update\n setMessages((prev) =>\n prev.map((msg) =>\n msg.id === messageToEdit.id\n ? { ...msg, content: optimisticContent, isEdited: true }\n : msg,\n ),\n );\n setMessageToEdit(null);\n\n try {\n const updated = await onUpdate(messageToEdit.id, {\n text: data.text,\n uploadFiles: data.uploadFiles,\n editedFiles: data.editedFiles,\n });\n setMessages((prev) =>\n prev.map((msg) => (msg.id === updated.id ? updated : msg)),\n );\n } catch (error) {\n console.error(\"Failed to update message:\", error);\n setMessages((prev) =>\n prev.map((msg) =>\n msg.id === messageToEdit.id ? messageToEdit : msg,\n ),\n );\n }\n },\n [messageToEdit, onUpdate, setMessages],\n );\n\n const handleCancelEdit = useCallback(() => {\n setMessageToEdit(null);\n }, []);\n\n return {\n messageToDelete,\n openDeleteDialog,\n setOpenDeleteDialog,\n messageToEdit,\n handleOpenDeleteDialog,\n handleDelete,\n handleStartEdit,\n handleSubmitEdit,\n handleCancelEdit,\n };\n};\n", "type": "registry:hook", "target": "hooks/chat/use-message-actions.ts" }, { "path": "registry/new-york/blocks/chat-basic/hooks/use-profile.ts", "content": "import { useCallback, useState } from \"react\";\n\nexport const useProfile = ({\n setSidebarOpen,\n setSidebarView,\n onBlock,\n onUnblock,\n}: {\n setSidebarOpen: React.Dispatch>;\n setSidebarView: React.Dispatch>;\n onBlock: (userId: string) => Promise;\n onUnblock: (userId: string) => Promise;\n}) => {\n const [openBlockDialog, setOpenBlockDialog] = useState(false);\n const [isBlocked, setIsBlocked] = useState(false);\n\n const openProfile = useCallback(() => {\n setSidebarView(\"profile\");\n setSidebarOpen(true);\n }, [setSidebarView, setSidebarOpen]);\n\n const handleBlock = useCallback(\n async (userId: string) => {\n try {\n await onBlock(userId);\n setIsBlocked(true);\n setOpenBlockDialog(false);\n } catch (error) {\n console.error(\"Failed to block user:\", error);\n }\n },\n [onBlock],\n );\n\n const handleUnblock = useCallback(\n async (userId: string) => {\n try {\n await onUnblock(userId);\n setIsBlocked(false);\n } catch (error) {\n console.error(\"Failed to unblock user:\", error);\n }\n },\n [onUnblock],\n );\n\n return {\n openBlockDialog,\n setOpenBlockDialog,\n isBlocked,\n openProfile,\n handleBlock,\n handleUnblock,\n };\n};\n", "type": "registry:hook", "target": "hooks/chat/use-profile.ts" }, { "path": "registry/new-york/blocks/chat-basic/hooks/use-chat-sidebar.ts", "content": "import { useState } from \"react\";\n\nexport const useChatSidebar = () => {\n const [sidebarOpen, setSidebarOpen] = useState(false);\n const [sidebarView, setSidebarView] = useState<\"search\" | \"profile\">(\n \"search\",\n );\n\n return {\n sidebarOpen,\n setSidebarOpen,\n sidebarView,\n setSidebarView,\n };\n};\n", "type": "registry:hook", "target": "hooks/chat/use-chat-sidebar.ts" }, { "path": "registry/new-york/blocks/chat-basic/hooks/use-highlighted-message-id.ts", "content": "import { useEffect, useState } from \"react\";\n\nexport function useHighlightedMessageId() {\n const [highlightedMessageId, setHighlightedMessageId] = useState<\n number | null\n >(null);\n\n useEffect(() => {\n if (highlightedMessageId === null) return;\n const timer = setTimeout(() => setHighlightedMessageId(null), 3000);\n return () => clearTimeout(timer);\n }, [highlightedMessageId]);\n\n return {\n highlightedMessageId,\n setHighlightedMessageId,\n };\n}\n", "type": "registry:hook", "target": "hooks/chat/use-highlighted-message-id.ts" }, { "path": "registry/new-york/blocks/chat-basic/hooks/use-is-wider.ts", "content": "import { useState, useEffect, RefObject } from \"react\";\n\nexport function useIsWider(ref: RefObject, pixels: number) {\n const [isWider, setIsWider] = useState(false);\n\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n const ro = new ResizeObserver(([entry]) => {\n setIsWider(entry.contentRect.width >= pixels);\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, [ref, pixels]);\n\n return isWider;\n}\n", "type": "registry:hook", "target": "hooks/use-is-wider.ts" }, { "path": "registry/new-york/blocks/chat-basic/hooks/use-is-viewport-wider.ts", "content": "import { useState, useEffect } from \"react\";\n\nexport function useIsViewportWider(pixels: number) {\n const [isWider, setIsWider] = useState(false);\n\n useEffect(() => {\n const mql = window.matchMedia(`(min-width: ${pixels}px)`);\n setIsWider(mql.matches);\n const handler = (e: MediaQueryListEvent) => setIsWider(e.matches);\n mql.addEventListener(\"change\", handler);\n return () => mql.removeEventListener(\"change\", handler);\n }, [pixels]);\n\n return isWider;\n}\n", "type": "registry:hook", "target": "hooks/use-is-viewport-wider.ts" }, { "path": "registry/new-york/blocks/chat-basic/hooks/use-long-press.ts", "content": "import { useCallback, useRef } from \"react\";\n\ninterface UseLongPressOptions {\n delay?: number;\n}\n\nexport function useLongPress(\n callback: () => void,\n { delay = 500 }: UseLongPressOptions = {},\n) {\n const timerRef = useRef | null>(null);\n const startPosRef = useRef<{ x: number; y: number } | null>(null);\n\n const cancel = useCallback(() => {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n startPosRef.current = null;\n }, []);\n\n const onPointerDown = useCallback(\n (e: React.PointerEvent) => {\n if (e.pointerType !== \"touch\") return;\n startPosRef.current = { x: e.clientX, y: e.clientY };\n timerRef.current = setTimeout(() => {\n timerRef.current = null;\n callback();\n }, delay);\n },\n [callback, delay],\n );\n\n const onPointerMove = useCallback(\n (e: React.PointerEvent) => {\n if (!startPosRef.current) return;\n const dx = e.clientX - startPosRef.current.x;\n const dy = e.clientY - startPosRef.current.y;\n if (Math.sqrt(dx * dx + dy * dy) > 10) cancel();\n },\n [cancel],\n );\n\n const onContextMenu = useCallback((e: React.MouseEvent) => {\n e.preventDefault();\n }, []);\n\n return {\n onPointerDown,\n onPointerUp: cancel,\n onPointerLeave: cancel,\n onPointerCancel: cancel,\n onPointerMove,\n onContextMenu,\n };\n}\n", "type": "registry:hook", "target": "hooks/use-long-press.ts" }, { "path": "registry/new-york/blocks/chat-basic/components/chat-header-actions.tsx", "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport {\n BanIcon,\n MoreHorizontalIcon,\n PhoneIcon,\n SearchIcon,\n UserIcon,\n VideoIcon,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { ChatHeaderButton } from \"@/registry/new-york/chat/chat-header\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport { useIsViewportWider } from \"@/registry/new-york/blocks/chat-basic/hooks/use-is-viewport-wider\";\n\ninterface ChatHeaderActionsProps {\n isChatWide: boolean;\n openSearch: () => void;\n openProfile: () => void;\n isBlocked: boolean;\n onUnblock: () => void;\n onBlock: () => void;\n}\n\nexport function ChatHeaderActions({\n isChatWide,\n openSearch,\n openProfile,\n isBlocked,\n onUnblock,\n onBlock,\n}: ChatHeaderActionsProps) {\n const isTabletOrWider = useIsViewportWider(768);\n const [dialogOpen, setDialogOpen] = useState(false);\n\n const trigger = (\n \n \n \n );\n\n if (isTabletOrWider) {\n return (\n \n {trigger}\n \n {!isChatWide && (\n <>\n \n \n Search\n \n \n \n Start call\n \n \n \n Start video\n \n \n \n )}\n \n \n Show profile\n \n {isBlocked ? (\n \n \n Unblock\n \n ) : (\n \n \n Block\n \n )}\n \n \n );\n }\n\n return (\n \n {trigger}\n \n \n Options\n \n Chat actions\n \n \n
\n }\n onClick={() => {\n setDialogOpen(false);\n openSearch();\n }}\n >\n Search\n \n }>Start call\n }>Start video\n
\n }\n onClick={() => {\n setDialogOpen(false);\n openProfile();\n }}\n >\n Show profile\n \n {isBlocked ? (\n }\n onClick={() => {\n setDialogOpen(false);\n onUnblock();\n }}\n >\n Unblock\n \n ) : (\n }\n destructive\n onClick={() => {\n setDialogOpen(false);\n onBlock();\n }}\n >\n Block\n \n )}\n
\n
\n
\n );\n}\n\ninterface ActionItemProps {\n icon: React.ReactNode;\n children: React.ReactNode;\n onClick?: () => void;\n destructive?: boolean;\n}\n\nfunction ActionItem({ icon, children, onClick, destructive }: ActionItemProps) {\n return (\n \n {icon}\n {children}\n \n );\n}\n", "type": "registry:component", "target": "components/chat/chat-header-actions.tsx" }, { "path": "registry/new-york/blocks/chat-basic/components/message-items/primary-message.tsx", "content": "import { useState } from \"react\";\nimport { EventContent } from \"@/registry/new-york/blocks/chat-basic/data/messages\";\nimport { cn } from \"@/lib/utils\";\nimport {\n ChatEvent,\n ChatEventAddon,\n ChatEventAvatar,\n ChatEventBody,\n ChatEventContent,\n ChatEventHoverActions,\n ChatEventHoverActionsButton,\n ChatEventTime,\n ChatEventTitle,\n} from \"@/registry/new-york/chat/chat-event\";\nimport { MessageContent } from \"@/registry/new-york/blocks/chat-basic/components/message-items/message-content\";\nimport { MessageActionsDropdown } from \"@/registry/new-york/blocks/chat-basic/components/message-actions/message-actions-dropdown\";\nimport { MessageActionsDialog } from \"@/registry/new-york/blocks/chat-basic/components/message-actions/message-actions-dialog\";\nimport { ReactionsPopover } from \"@/registry/new-york/blocks/chat-basic/components/message-reactions/reactions-popover\";\nimport { useLongPress } from \"@/registry/new-york/blocks/chat-basic/hooks/use-long-press\";\nimport { MoreHorizontalIcon, SmilePlusIcon } from \"lucide-react\";\n\ninterface PrimaryMessageProps {\n avatarSrc?: string;\n avatarAlt?: string;\n avatarFallback?: string;\n senderName: string;\n content: EventContent;\n timestamp: number;\n status?: \"sent\" | \"sending\" | \"failed\";\n reactions?: string[];\n isEdited?: boolean;\n onReaction?: (emoji: string) => void;\n onDelete?: () => void;\n onEdit?: () => void;\n className?: string;\n id?: string;\n highlighted?: boolean;\n}\n\nexport function PrimaryMessage({\n avatarSrc,\n avatarAlt,\n avatarFallback,\n senderName,\n content,\n timestamp,\n status,\n reactions,\n isEdited,\n onReaction,\n onDelete,\n onEdit,\n className,\n id,\n highlighted,\n}: PrimaryMessageProps) {\n const [actionsDialogOpen, setActionsDialogOpen] = useState(false);\n const longPressHandlers = useLongPress(() => setActionsDialogOpen(true));\n\n return (\n <>\n \n \n \n \n \n \n {senderName}\n \n \n \n \n \n {isEdited && (\n (edited)\n )}\n {reactions && reactions.length > 0 && (\n
\n {reactions.map((emoji, i) => (\n onReaction?.(emoji)}\n className=\"text-sm bg-accent border rounded-full px-2 py-0.5 select-none hover:bg-destructive/10 hover:border-destructive/40 transition-colors\"\n aria-label={`Toggle ${emoji} reaction`}\n >\n {emoji}\n \n ))}\n
\n )}\n
\n \n \n \n \n \n \n navigator.clipboard.writeText(content.text!)\n : undefined\n }\n onEdit={onEdit}\n onDelete={onDelete}\n >\n \n \n \n \n \n \n navigator.clipboard.writeText(content.text!)\n : undefined\n }\n onEdit={onEdit}\n onDelete={onDelete}\n />\n \n );\n}\n", "type": "registry:component", "target": "components/chat/message-items/primary-message.tsx" }, { "path": "registry/new-york/blocks/chat-basic/components/message-items/additional-message.tsx", "content": "import { useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { EventContent } from \"@/registry/new-york/blocks/chat-basic/data/messages\";\nimport {\n ChatEvent,\n ChatEventAddon,\n ChatEventBody,\n ChatEventContent,\n ChatEventHoverActions,\n ChatEventHoverActionsButton,\n ChatEventTime,\n} from \"@/registry/new-york/chat/chat-event\";\nimport { MessageContent } from \"@/registry/new-york/blocks/chat-basic/components/message-items/message-content\";\nimport { MessageActionsDropdown } from \"@/registry/new-york/blocks/chat-basic/components/message-actions/message-actions-dropdown\";\nimport { MessageActionsDialog } from \"@/registry/new-york/blocks/chat-basic/components/message-actions/message-actions-dialog\";\nimport { ReactionsPopover } from \"@/registry/new-york/blocks/chat-basic/components/message-reactions/reactions-popover\";\nimport { useLongPress } from \"@/registry/new-york/blocks/chat-basic/hooks/use-long-press\";\nimport { MoreHorizontalIcon, SmilePlusIcon } from \"lucide-react\";\n\ninterface AdditionalMessageProps {\n className?: string;\n content: EventContent;\n timestamp: number;\n status?: \"sent\" | \"sending\" | \"failed\";\n reactions?: string[];\n isEdited?: boolean;\n onReaction?: (emoji: string) => void;\n onDelete?: () => void;\n onEdit?: () => void;\n id?: string;\n highlighted?: boolean;\n}\n\nexport function AdditionalMessage({\n className,\n content,\n timestamp,\n status,\n reactions,\n isEdited,\n onReaction,\n onDelete,\n onEdit,\n id,\n highlighted,\n}: AdditionalMessageProps) {\n const [actionsDialogOpen, setActionsDialogOpen] = useState(false);\n const longPressHandlers = useLongPress(() => setActionsDialogOpen(true));\n\n return (\n <>\n \n \n \n \n \n \n \n \n {isEdited && (\n (edited)\n )}\n {reactions && reactions.length > 0 && (\n
\n {reactions.map((emoji, i) => (\n onReaction?.(emoji)}\n className=\"text-sm bg-accent border rounded-full px-2 py-0.5 select-none hover:bg-destructive/10 hover:border-destructive/40 transition-colors\"\n aria-label={`Toggle ${emoji} reaction`}\n >\n {emoji}\n \n ))}\n
\n )}\n
\n \n \n \n \n \n \n navigator.clipboard.writeText(content.text!)\n : undefined\n }\n onEdit={onEdit}\n onDelete={onDelete}\n >\n \n \n \n \n \n \n navigator.clipboard.writeText(content.text!)\n : undefined\n }\n onEdit={onEdit}\n onDelete={onDelete}\n />\n \n );\n}\n", "type": "registry:component", "target": "components/chat/message-items/additional-message.tsx" }, { "path": "registry/new-york/blocks/chat-basic/components/message-items/date-item.tsx", "content": "import { Separator } from \"@/components/ui/separator\";\nimport { cn } from \"@/lib/utils\";\nimport { ChatEvent, ChatEventTime } from \"@/registry/new-york/chat/chat-event\";\n\nexport function DateItem({\n timestamp,\n className,\n}: {\n timestamp: number;\n className?: string;\n}) {\n return (\n \n \n \n \n \n );\n}\n", "type": "registry:component", "target": "components/chat/message-items/date-item.tsx" }, { "path": "registry/new-york/blocks/chat-basic/components/message-items/message-content.tsx", "content": "import {\n EventContent,\n EventFile,\n} from \"@/registry/new-york/blocks/chat-basic/data/messages\";\nimport { FileTextIcon } from \"lucide-react\";\n\nexport function MessageContent({ content }: { content: EventContent }) {\n const hasText = !!content.text;\n const hasFiles = !!(content.files && content.files.length > 0);\n\n return (\n <>\n {hasText && {content.text}}\n {hasFiles && (\n
\n {content.files!.map((file, i) => (\n \n ))}\n
\n )}\n \n );\n}\n\nfunction FileCard({ file }: { file: EventFile }) {\n return (\n
\n \n
\n \n {file.fileName}\n \n {file.mimeType && (\n
\n {file.mimeType}\n
\n )}\n
\n
\n );\n}\n", "type": "registry:component", "target": "components/chat/message-items/message-content.tsx" }, { "path": "registry/new-york/blocks/chat-basic/components/message-items/primary-message-skeleton.tsx", "content": "import { cn } from \"@/lib/utils\";\nimport {\n ChatEvent,\n ChatEventAddon,\n ChatEventBody,\n ChatEventContent,\n ChatEventTitle,\n} from \"@/registry/new-york/chat/chat-event\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\n\nexport function PrimaryMessageSkeleton({ className }: { className?: string }) {\n return (\n \n \n \n \n \n \n \n \n \n
\n \n \n
\n
\n
\n
\n );\n}\n", "type": "registry:component", "target": "components/chat/message-items/primary-message-skeleton.tsx" }, { "path": "registry/new-york/blocks/chat-basic/components/message-items/date-item-skeleton.tsx", "content": "import { Separator } from \"@/components/ui/separator\";\nimport { cn } from \"@/lib/utils\";\nimport { ChatEvent } from \"@/registry/new-york/chat/chat-event\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\n\nexport function DateItemSkeleton({ className }: { className?: string }) {\n return (\n \n \n \n \n \n );\n}\n", "type": "registry:component", "target": "components/chat/message-items/date-item-skeleton.tsx" }, { "path": "registry/new-york/blocks/chat-basic/components/message-items/message-preview.tsx", "content": "import {\n ChatEvent,\n ChatEventAddon,\n ChatEventAvatar,\n ChatEventBody,\n ChatEventContent,\n ChatEventTime,\n ChatEventTitle,\n} from \"@/registry/new-york/chat/chat-event\";\nimport { EventContent } from \"@/registry/new-york/blocks/chat-basic/data/messages\";\nimport { MessageContent } from \"@/registry/new-york/blocks/chat-basic/components/message-items/message-content\";\n\nexport function MessagePreview({\n avatarSrc,\n avatarAlt,\n avatarFallback,\n senderName,\n content,\n timestamp,\n onClick,\n className,\n}: {\n avatarSrc?: string;\n avatarAlt?: string;\n avatarFallback?: string;\n senderName: string;\n content: EventContent;\n timestamp: number;\n onClick?: () => void;\n className?: string;\n}) {\n return (\n \n \n \n \n \n \n {senderName}\n \n \n \n \n \n \n \n );\n}\n", "type": "registry:component", "target": "components/chat/message-items/message-preview.tsx" }, { "path": "registry/new-york/blocks/chat-basic/components/message-reactions/reactions-popover.tsx", "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { PlusIcon } from \"lucide-react\";\nimport EmojiPicker, { EmojiClickData, Theme } from \"emoji-picker-react\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Button } from \"@/components/ui/button\";\n\nexport const DEFAULT_REACTIONS = [\"👍\", \"❤️\", \"😂\", \"😮\", \"😢\", \"🙏\", \"🎉\"];\n\nexport function ReactionsPopover({\n children,\n onReaction,\n}: {\n children: React.ReactNode;\n onReaction?: (emoji: string) => void;\n}) {\n const [open, setOpen] = useState(false);\n const [fullPickerOpen, setFullPickerOpen] = useState(false);\n\n return (\n {\n setOpen(o);\n if (!o) setFullPickerOpen(false);\n }}\n >\n {children}\n \n
\n {DEFAULT_REACTIONS.map((emoji) => (\n {\n onReaction?.(emoji);\n setOpen(false);\n }}\n >\n {emoji}\n \n ))}\n \n \n \n \n \n \n \n {\n onReaction?.(emojiData.emoji);\n setFullPickerOpen(false);\n setOpen(false);\n }}\n />\n \n \n
\n
\n \n );\n}\n", "type": "registry:component", "target": "components/chat/message-reactions/reactions-popover.tsx" }, { "path": "registry/new-york/blocks/chat-basic/components/message-actions/message-actions-dropdown.tsx", "content": "import {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { CopyIcon, PencilIcon, Trash2Icon } from \"lucide-react\";\n\ninterface MessageActionsDropdownProps {\n children: React.ReactNode;\n onCopy?: () => void;\n onDelete?: () => void;\n onEdit?: () => void;\n}\n\nexport function MessageActionsDropdown({\n children,\n onCopy,\n onDelete,\n onEdit,\n}: MessageActionsDropdownProps) {\n const hasAdditionalActions = !!onCopy || !!onEdit || !!onDelete;\n\n if (!hasAdditionalActions) return null;\n\n return (\n \n {children}\n e.preventDefault()}\n >\n {onCopy && (\n \n \n Copy\n \n )}\n {onEdit && (\n \n \n Edit\n \n )}\n {onDelete && (\n \n \n Delete\n \n )}\n \n \n );\n}\n", "type": "registry:component", "target": "components/chat/message-actions/message-actions-dropdown.tsx" }, { "path": "registry/new-york/blocks/chat-basic/components/message-actions/message-actions-dialog.tsx", "content": "import { Button } from \"@/components/ui/button\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { CopyIcon, PencilIcon, Trash2Icon } from \"lucide-react\";\nimport { DEFAULT_REACTIONS } from \"@/registry/new-york/blocks/chat-basic/components/message-reactions/reactions-popover\";\n\ninterface MessageActionsDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onReaction?: (emoji: string) => void;\n onCopy?: () => void;\n onEdit?: () => void;\n onDelete?: () => void;\n}\n\nexport function MessageActionsDialog({\n open,\n onOpenChange,\n onReaction,\n onCopy,\n onEdit,\n onDelete,\n}: MessageActionsDialogProps) {\n const hasActions = !!onCopy || !!onEdit || !!onDelete;\n\n return (\n \n \n \n Message actions\n \n React to this message or choose an action\n \n \n {onReaction && (\n
\n {DEFAULT_REACTIONS.map((emoji) => (\n {\n onReaction(emoji);\n onOpenChange(false);\n }}\n >\n {emoji}\n \n ))}\n
\n )}\n {hasActions && (\n <>\n {onReaction &&
}\n
\n {onCopy && (\n {\n onCopy();\n onOpenChange(false);\n }}\n >\n \n Copy\n \n )}\n {onEdit && (\n {\n onOpenChange(false);\n onEdit();\n }}\n >\n \n Edit\n \n )}\n {onDelete && (\n {\n onOpenChange(false);\n onDelete();\n }}\n >\n \n Delete\n \n )}\n
\n \n )}\n \n
\n );\n}\n", "type": "registry:component", "target": "components/chat/message-actions/message-actions-dialog.tsx" }, { "path": "registry/new-york/blocks/chat-basic/components/message-actions/delete-dialog.tsx", "content": "import { Button } from \"@/components/ui/button\";\nimport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { MessagePreview } from \"@/registry/new-york/blocks/chat-basic/components/message-items/message-preview\";\nimport { Event } from \"@/registry/new-york/blocks/chat-basic/data/messages\";\n\nexport function DeleteDialog({\n open,\n onOpenChange,\n message,\n onConfirm,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n message: Event | null;\n onConfirm: () => void;\n}) {\n return (\n \n \n \n Delete message\n \n Are you sure you want to delete this message?\n \n \n {message && (\n \n )}\n \n \n \n \n \n \n \n \n );\n}\n", "type": "registry:component", "target": "components/chat/message-actions/delete-dialog.tsx" }, { "path": "registry/new-york/blocks/chat-basic/components/message-search/search-sidebar-content.tsx", "content": "\"use client\";\n\nimport { MessagePreview } from \"@/registry/new-york/blocks/chat-basic/components/message-items/message-preview\";\nimport {\n InputGroup,\n InputGroupAddon,\n InputGroupInput,\n} from \"@/components/ui/input-group\";\nimport { Event } from \"@/registry/new-york/blocks/chat-basic/data/messages\";\nimport { SearchIcon } from \"lucide-react\";\n\nexport interface SearchSidebarContentProps {\n query: string;\n results: Event[];\n searchQuery: string;\n onSearchQueryChange: (value: string) => void;\n onSearch: (query: string) => void;\n onResultClick: (id: number) => void;\n onClose: () => void;\n isMobile: boolean;\n}\n\nexport function SearchSidebarContent({\n query,\n results,\n searchQuery,\n onSearchQueryChange,\n onSearch,\n onResultClick,\n onClose,\n isMobile,\n}: SearchSidebarContentProps) {\n const label = `${results.length} result${results.length !== 1 ? \"s\" : \"\"} for \"${query}\"`;\n\n const resultItems = results.map((msg) => (\n {\n onResultClick(msg.id);\n if (isMobile) onClose();\n }}\n />\n ));\n\n const emptyState = (\n

\n No messages found.\n

\n );\n\n if (isMobile) {\n return (\n <>\n
\n \n onSearchQueryChange(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n onSearch(searchQuery);\n }\n }}\n autoFocus\n />\n \n \n \n \n
\n {query && (\n
\n {label}\n
\n )}\n
\n {query && (results.length === 0 ? emptyState : resultItems)}\n
\n \n );\n }\n\n return (\n <>\n {query && (\n
\n {label}\n
\n )}\n
\n {query && (results.length === 0 ? emptyState : resultItems)}\n
\n \n );\n}\n", "type": "registry:component", "target": "components/chat/message-search/search-sidebar-content.tsx" }, { "path": "registry/new-york/blocks/chat-basic/components/profile/profile-sidebar-content.tsx", "content": "import { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport { OTHER_USER } from \"@/registry/new-york/blocks/chat-basic/data/users\";\n\nexport function ProfileSidebarContent() {\n return (\n
\n \n \n {OTHER_USER.name.slice(0, 2)}\n \n
\n

{OTHER_USER.name}

\n

{OTHER_USER.username}

\n
\n
\n );\n}\n", "type": "registry:component", "target": "components/chat/profile/profile-sidebar-content.tsx" }, { "path": "registry/new-york/blocks/chat-basic/components/profile/block-dialog.tsx", "content": "import { Button } from \"@/components/ui/button\";\nimport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"@/components/ui/dialog\";\n\nexport function BlockDialog({\n open,\n onOpenChange,\n onConfirm,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onConfirm: () => void;\n}) {\n return (\n \n \n \n Block user\n \n Are you sure you want to block this user? They will no longer be\n able to send you messages.\n \n \n \n \n \n \n \n \n \n \n );\n}\n", "type": "registry:component", "target": "components/chat/profile/block-dialog.tsx" }, { "path": "registry/new-york/blocks/chat-basic/components/chat-sidebar/chat-sidebar.tsx", "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Sheet,\n SheetContent,\n SheetHeader,\n SheetTitle,\n} from \"@/components/ui/sheet\";\nimport { SidebarContent, SidebarHeader } from \"@/components/ui/sidebar\";\nimport { XIcon } from \"lucide-react\";\n\ninterface ChatSidebarProps {\n open: boolean;\n onClose: () => void;\n title: string;\n isMobile: boolean;\n children: React.ReactNode;\n}\n\nexport function ChatSidebar({\n open,\n onClose,\n title,\n isMobile,\n children,\n}: ChatSidebarProps) {\n if (isMobile) {\n return (\n {\n if (!o) onClose();\n }}\n >\n \n \n {title}\n \n {children}\n \n \n );\n }\n\n return (\n \n \n {title}\n \n \n \n \n {children}\n \n );\n}\n", "type": "registry:component", "target": "components/chat/chat-sidebar/chat-sidebar.tsx" } ], "css": { "@keyframes message-highlight": { "0%, 60%": { "background-color": "var(--accent)" }, "100%": { "background-color": "transparent" } }, "@utility animate-message-highlight": { "animation": "message-highlight 3s ease-out forwards" }, "@utility scrollbar-hidden": { "-ms-overflow-style": "none", "scrollbar-width": "none", "&::-webkit-scrollbar": { "display": "none" } } }, "type": "registry:block" }