{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "realtime-chat-nextjs",
  "type": "registry:component",
  "title": "Realtime Chat",
  "description": "Component which renders realtime chat messages from other users in a room.",
  "dependencies": [
    "lucide-react",
    "@supabase/ssr@latest",
    "@supabase/supabase-js@latest"
  ],
  "registryDependencies": [
    "input",
    "button"
  ],
  "files": [
    {
      "path": "registry/default/blocks/realtime-chat/components/chat-message.tsx",
      "content": "import { cn } from '@/lib/utils'\nimport type { ChatMessage } from '@/registry/default/blocks/realtime-chat/hooks/use-realtime-chat'\n\ninterface ChatMessageItemProps {\n  message: ChatMessage\n  isOwnMessage: boolean\n  showHeader: boolean\n}\n\nexport const ChatMessageItem = ({ message, isOwnMessage, showHeader }: ChatMessageItemProps) => {\n  return (\n    <div className={`flex mt-2 ${isOwnMessage ? 'justify-end' : 'justify-start'}`}>\n      <div\n        className={cn('max-w-[75%] w-fit flex flex-col gap-1', {\n          'items-end': isOwnMessage,\n        })}\n      >\n        {showHeader && (\n          <div\n            className={cn('flex items-center gap-2 text-xs px-3', {\n              'justify-end flex-row-reverse': isOwnMessage,\n            })}\n          >\n            <span className={'font-medium'}>{message.user.name}</span>\n            <span className=\"text-foreground/50 text-xs\">\n              {new Date(message.createdAt).toLocaleTimeString('en-US', {\n                hour: '2-digit',\n                minute: '2-digit',\n                hour12: true,\n              })}\n            </span>\n          </div>\n        )}\n        <div\n          className={cn(\n            'py-2 px-3 rounded-xl text-sm w-fit',\n            isOwnMessage ? 'bg-primary text-primary-foreground' : 'bg-muted text-foreground'\n          )}\n        >\n          {message.content}\n        </div>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/default/blocks/realtime-chat/components/realtime-chat.tsx",
      "content": "'use client'\n\nimport { Send } from 'lucide-react'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\n\nimport { cn } from '@/lib/utils'\nimport { ChatMessageItem } from '@/registry/default/blocks/realtime-chat/components/chat-message'\nimport { useChatScroll } from '@/registry/default/blocks/realtime-chat/hooks/use-chat-scroll'\nimport {\n  useRealtimeChat,\n  type ChatMessage,\n} from '@/registry/default/blocks/realtime-chat/hooks/use-realtime-chat'\nimport { Button } from '@/registry/default/components/ui/button'\nimport { Input } from '@/registry/default/components/ui/input'\n\ninterface RealtimeChatProps {\n  roomName: string\n  username: string\n  onMessage?: (messages: ChatMessage[]) => void\n  messages?: ChatMessage[]\n}\n\n/**\n * Realtime chat component\n * @param roomName - The name of the room to join. Each room is a unique chat.\n * @param username - The username of the user\n * @param onMessage - The callback function to handle the messages. Useful if you want to store the messages in a database.\n * @param messages - The messages to display in the chat. Useful if you want to display messages from a database.\n * @returns The chat component\n */\nexport const RealtimeChat = ({\n  roomName,\n  username,\n  onMessage,\n  messages: initialMessages = [],\n}: RealtimeChatProps) => {\n  const { containerRef, scrollToBottom } = useChatScroll()\n\n  const {\n    messages: realtimeMessages,\n    sendMessage,\n    isConnected,\n  } = useRealtimeChat({\n    roomName,\n    username,\n  })\n  const [newMessage, setNewMessage] = useState('')\n\n  // Merge realtime messages with initial messages\n  const allMessages = useMemo(() => {\n    const mergedMessages = [...initialMessages, ...realtimeMessages]\n    // Remove duplicates based on message id\n    const uniqueMessages = mergedMessages.filter(\n      (message, index, self) => index === self.findIndex((m) => m.id === message.id)\n    )\n    // Sort by creation date\n    const sortedMessages = uniqueMessages.sort((a, b) => a.createdAt.localeCompare(b.createdAt))\n\n    return sortedMessages\n  }, [initialMessages, realtimeMessages])\n\n  useEffect(() => {\n    if (onMessage) {\n      onMessage(allMessages)\n    }\n  }, [allMessages, onMessage])\n\n  useEffect(() => {\n    // Scroll to bottom whenever messages change\n    scrollToBottom()\n  }, [allMessages, scrollToBottom])\n\n  const handleSendMessage = useCallback(\n    (e: React.FormEvent) => {\n      e.preventDefault()\n      if (!newMessage.trim() || !isConnected) return\n\n      sendMessage(newMessage)\n      setNewMessage('')\n    },\n    [newMessage, isConnected, sendMessage]\n  )\n\n  return (\n    <div className=\"flex flex-col h-full w-full bg-background text-foreground antialiased\">\n      {/* Messages */}\n      <div ref={containerRef} className=\"flex-1 overflow-y-auto p-4 space-y-4\">\n        {allMessages.length === 0 ? (\n          <div className=\"text-center text-sm text-muted-foreground\">\n            No messages yet. Start the conversation!\n          </div>\n        ) : null}\n        <div className=\"space-y-1\">\n          {allMessages.map((message, index) => {\n            const prevMessage = index > 0 ? allMessages[index - 1] : null\n            const showHeader = !prevMessage || prevMessage.user.name !== message.user.name\n\n            return (\n              <div\n                key={message.id}\n                className=\"animate-in fade-in slide-in-from-bottom-4 duration-300\"\n              >\n                <ChatMessageItem\n                  message={message}\n                  isOwnMessage={message.user.name === username}\n                  showHeader={showHeader}\n                />\n              </div>\n            )\n          })}\n        </div>\n      </div>\n\n      <form onSubmit={handleSendMessage} className=\"flex w-full gap-2 border-t border-border p-4\">\n        <Input\n          className={cn(\n            'rounded-full bg-background text-sm transition-all duration-300',\n            isConnected && newMessage.trim() ? 'w-[calc(100%-36px)]' : 'w-full'\n          )}\n          type=\"text\"\n          value={newMessage}\n          onChange={(e) => setNewMessage(e.target.value)}\n          placeholder=\"Type a message...\"\n          disabled={!isConnected}\n        />\n        {isConnected && newMessage.trim() && (\n          <Button\n            className=\"aspect-square rounded-full animate-in fade-in slide-in-from-right-4 duration-300\"\n            type=\"submit\"\n            disabled={!isConnected}\n          >\n            <Send className=\"size-4\" />\n          </Button>\n        )}\n      </form>\n    </div>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/default/blocks/realtime-chat/hooks/use-realtime-chat.tsx",
      "content": "'use client'\n\nimport { useCallback, useEffect, useState } from 'react'\n\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\n\ninterface UseRealtimeChatProps {\n  roomName: string\n  username: string\n}\n\nexport interface ChatMessage {\n  id: string\n  content: string\n  user: {\n    name: string\n  }\n  createdAt: string\n}\n\nconst EVENT_MESSAGE_TYPE = 'message'\n\nexport function useRealtimeChat({ roomName, username }: UseRealtimeChatProps) {\n  const supabase = createClient()\n  const [messages, setMessages] = useState<ChatMessage[]>([])\n  const [channel, setChannel] = useState<ReturnType<typeof supabase.channel> | null>(null)\n  const [isConnected, setIsConnected] = useState(false)\n\n  useEffect(() => {\n    const newChannel = supabase.channel(roomName)\n\n    newChannel\n      .on('broadcast', { event: EVENT_MESSAGE_TYPE }, (payload) => {\n        setMessages((current) => [...current, payload.payload as ChatMessage])\n      })\n      .subscribe(async (status) => {\n        if (status === 'SUBSCRIBED') {\n          setIsConnected(true)\n        } else {\n          setIsConnected(false)\n        }\n      })\n\n    setChannel(newChannel)\n\n    return () => {\n      supabase.removeChannel(newChannel)\n    }\n  }, [roomName, username, supabase])\n\n  const sendMessage = useCallback(\n    async (content: string) => {\n      if (!channel || !isConnected) return\n\n      const message: ChatMessage = {\n        id: crypto.randomUUID(),\n        content,\n        user: {\n          name: username,\n        },\n        createdAt: new Date().toISOString(),\n      }\n\n      // Update local state immediately for the sender\n      setMessages((current) => [...current, message])\n\n      await channel.send({\n        type: 'broadcast',\n        event: EVENT_MESSAGE_TYPE,\n        payload: message,\n      })\n    },\n    [channel, isConnected, username]\n  )\n\n  return { messages, sendMessage, isConnected }\n}\n",
      "type": "registry:hook"
    },
    {
      "path": "registry/default/blocks/realtime-chat/hooks/use-chat-scroll.tsx",
      "content": "import { useCallback, useRef } from 'react'\n\nexport function useChatScroll() {\n  const containerRef = useRef<HTMLDivElement>(null)\n\n  const scrollToBottom = useCallback(() => {\n    if (!containerRef.current) return\n\n    const container = containerRef.current\n    container.scrollTo({\n      top: container.scrollHeight,\n      behavior: 'smooth',\n    })\n  }, [])\n\n  return { containerRef, scrollToBottom }\n}\n",
      "type": "registry:hook"
    },
    {
      "path": "registry/default/clients/nextjs/lib/supabase/client.ts",
      "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n  return createBrowserClient(\n    process.env.NEXT_PUBLIC_SUPABASE_URL!,\n    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!\n  )\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts",
      "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n  let supabaseResponse = NextResponse.next({\n    request,\n  })\n\n  // With Fluid compute, don't put this client in a global environment\n  // variable. Always create a new one on each request.\n  const supabase = createServerClient(\n    process.env.NEXT_PUBLIC_SUPABASE_URL!,\n    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n    {\n      cookies: {\n        getAll() {\n          return request.cookies.getAll()\n        },\n        setAll(cookiesToSet) {\n          cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n          supabaseResponse = NextResponse.next({\n            request,\n          })\n          cookiesToSet.forEach(({ name, value, options }) =>\n            supabaseResponse.cookies.set(name, value, options)\n          )\n        },\n      },\n    }\n  )\n\n  // Do not run code between createServerClient and\n  // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n  // issues with users being randomly logged out.\n\n  // IMPORTANT: If you remove getClaims() and you use server-side rendering\n  // with the Supabase client, your users may be randomly logged out.\n  const { data } = await supabase.auth.getClaims()\n  const user = data?.claims\n\n  if (\n    !user &&\n    !request.nextUrl.pathname.startsWith('/login') &&\n    !request.nextUrl.pathname.startsWith('/auth') &&\n    // the OAuth consent route sends unauthenticated visitors to the login page\n    // itself, so that it can preserve the authorization in the `next` parameter\n    request.nextUrl.pathname !== '/oauth/consent'\n  ) {\n    // no user, potentially respond by redirecting the user to the login page\n    const url = request.nextUrl.clone()\n    url.pathname = '/auth/login'\n    return NextResponse.redirect(url)\n  }\n\n  // IMPORTANT: You *must* return the supabaseResponse object as it is.\n  // If you're creating a new response object with NextResponse.next() make sure to:\n  // 1. Pass the request in it, like so:\n  //    const myNewResponse = NextResponse.next({ request })\n  // 2. Copy over the cookies, like so:\n  //    myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n  // 3. Change the myNewResponse object to fit your needs, but avoid changing\n  //    the cookies!\n  // 4. Finally:\n  //    return myNewResponse\n  // If this is not done, you may be causing the browser and server to go out\n  // of sync and terminate the user's session prematurely!\n\n  return supabaseResponse\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/default/clients/nextjs/lib/supabase/server.ts",
      "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\n */\nexport async function createClient() {\n  const cookieStore = await cookies()\n\n  return createServerClient(\n    process.env.NEXT_PUBLIC_SUPABASE_URL!,\n    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n    {\n      cookies: {\n        getAll() {\n          return cookieStore.getAll()\n        },\n        setAll(cookiesToSet) {\n          try {\n            cookiesToSet.forEach(({ name, value, options }) =>\n              cookieStore.set(name, value, options)\n            )\n          } catch {\n            // The `setAll` method was called from a Server Component.\n            // This can be ignored if you have middleware refreshing\n            // user sessions.\n          }\n        },\n      },\n    }\n  )\n}\n",
      "type": "registry:lib"
    }
  ],
  "envVars": {
    "NEXT_PUBLIC_SUPABASE_URL": "",
    "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": ""
  },
  "docs": "You'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`."
}