{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "realtime-chat-nuxtjs",
  "type": "registry:block",
  "title": "Realtime Chat",
  "description": "Component which provides a realtime chat interface.",
  "dependencies": [
    "@supabase/supabase-js@latest",
    "lucide-vue-next@latest"
  ],
  "registryDependencies": [
    "button",
    "input"
  ],
  "files": [
    {
      "path": "registry/default/realtime-chat/nuxtjs/app/components/realtime-chat.vue",
      "content": "<script setup lang=\"ts\">\nimport { ref, computed, watch } from 'vue'\nimport { Send } from 'lucide-vue-next'\n\nimport ChatMessageItem from './chat-message-item.vue'\nimport { useChatScroll } from '../composables/useChatScroll'\nimport {\n  useRealtimeChat,\n  type ChatMessage,\n} from '../composables/useRealtimeChat'\n  // @ts-ignore\nimport Button from '@/components/ui/Button.vue'\n  // @ts-ignore\nimport Input from '@/components/ui/Input.vue'\n\ninterface RealtimeChatProps {\n  roomName: string\n  username: string\n  onMessage?: (messages: ChatMessage[]) => void\n  messages?: ChatMessage[]\n}\n\nconst props = defineProps<RealtimeChatProps>()\n\nconst initialMessages = computed(() => props.messages ?? [])\n\nconst { containerRef, scrollToBottom } = useChatScroll()\n\nconst { messages: realtimeMessages, sendMessage, isConnected } =\n  useRealtimeChat({\n    roomName: props.roomName,\n    username: props.username,\n  })\n\nconst newMessage = ref('')\n\n/**\n * Merge + dedupe + sort\n */\nconst allMessages = computed<ChatMessage[]>(() => {\n  const merged = [...initialMessages.value, ...realtimeMessages.value]\n\n  const unique = merged.filter(\n    (message, index, self) =>\n      index === self.findIndex((m) => m.id === message.id)\n  )\n\n  return unique.sort((a, b) =>\n    a.createdAt.localeCompare(b.createdAt)\n  )\n})\n\n/**\n * Emit messages to parent if callback provided\n */\nwatch(allMessages, (messages) => {\n  if (props.onMessage) {\n    props.onMessage(messages)\n  }\n\n  scrollToBottom()\n}, { flush: 'post' })\n\nfunction handleSendMessage() {\n  if (!newMessage.value.trim() || !isConnected.value) return\n\n  sendMessage(newMessage.value)\n  newMessage.value = ''\n}\n</script>\n\n<template>\n  <div class=\"flex flex-col h-full w-full bg-background text-foreground antialiased\">\n    <!-- Messages -->\n    <div\n      ref=\"containerRef\"\n      class=\"flex-1 overflow-y-auto p-4 space-y-4\"\n    >\n      <div\n        v-if=\"allMessages.length === 0\"\n        class=\"text-center text-sm text-muted-foreground\"\n      >\n        No messages yet. Start the conversation!\n      </div>\n\n      <div class=\"space-y-1\">\n        <div\n          v-for=\"(message, index) in allMessages\"\n          :key=\"message.id\"\n          class=\"animate-in fade-in slide-in-from-bottom-4 duration-300\"\n        >\n          <ChatMessageItem\n            :message=\"message\"\n            :isOwnMessage=\"message.user.name === props.username\"\n            :showHeader=\"\n              !allMessages[index - 1] ||\n              allMessages[index - 1].user.name !== message.user.name\n            \"\n          />\n        </div>\n      </div>\n    </div>\n\n    <!-- Input -->\n    <form\n      @submit.prevent=\"handleSendMessage\"\n      class=\"flex w-full gap-2 border-t border-border p-4\"\n    >\n      <Input\n        v-model=\"newMessage\"\n        :disabled=\"!isConnected\"\n        type=\"text\"\n        placeholder=\"Type a message...\"\n        :class=\"[\n          'rounded-full bg-background text-sm transition-all duration-300',\n          isConnected && newMessage.trim()\n            ? 'w-[calc(100%-36px)]'\n            : 'w-full',\n        ]\"\n      />\n\n      <Button\n        v-if=\"isConnected && newMessage.trim()\"\n        type=\"submit\"\n        :disabled=\"!isConnected\"\n        class=\"aspect-square rounded-full animate-in fade-in slide-in-from-right-4 duration-300\"\n      >\n        <Send class=\"size-4\" />\n      </Button>\n    </form>\n  </div>\n</template>\n",
      "type": "registry:component",
      "target": "app/components/realtime-chat.vue"
    },
    {
      "path": "registry/default/realtime-chat/nuxtjs/app/components/chat-message-item.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport type { ChatMessage } from '../composables/useRealtimeChat'\n\nconst props = defineProps<{\n  message: ChatMessage\n  isOwnMessage: boolean\n  showHeader: boolean\n}>()\n\nconst containerClasses = computed(() => [\n  'flex mt-2',\n  props.isOwnMessage ? 'justify-end' : 'justify-start',\n])\n\nconst wrapperClasses = computed(() => [\n  'max-w-[75%] w-fit flex flex-col gap-1',\n  props.isOwnMessage ? 'items-end' : '',\n])\n\nconst headerClasses = computed(() => [\n  'flex items-center gap-2 text-xs px-3',\n  props.isOwnMessage ? 'justify-end flex-row-reverse' : '',\n])\n\nconst bubbleClasses = computed(() => [\n  'py-2 px-3 rounded-xl text-sm w-fit',\n  props.isOwnMessage\n    ? 'bg-primary text-primary-foreground'\n    : 'bg-muted text-foreground',\n])\n\nconst formattedTime = computed(() =>\n  new Date(props.message.createdAt).toLocaleTimeString('en-US', {\n    hour: '2-digit',\n    minute: '2-digit',\n    hour12: true,\n  })\n)\n</script>\n\n<template>\n  <div :class=\"containerClasses\">\n    <div :class=\"wrapperClasses\">\n      <!-- Header -->\n      <div v-if=\"showHeader\" :class=\"headerClasses\">\n        <span class=\"font-medium\">\n          {{ message.user.name }}\n        </span>\n\n        <span class=\"text-foreground/50 text-xs\">\n          {{ formattedTime }}\n        </span>\n      </div>\n\n      <!-- Message Bubble -->\n      <div :class=\"bubbleClasses\">\n        {{ message.content }}\n      </div>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:component",
      "target": "app/components/chat-message-item.vue"
    },
    {
      "path": "registry/default/realtime-chat/nuxtjs/app/composables/useChatScroll.ts",
      "content": "import { ref } from 'vue'\n\nexport function useChatScroll() {\n  const containerRef = ref<HTMLDivElement | null>(null)\n\n  function scrollToBottom() {\n    if (!containerRef.value) return\n\n    const container = containerRef.value\n\n    container.scrollTo({\n      top: container.scrollHeight,\n      behavior: 'smooth',\n    })\n  }\n\n  return {\n    containerRef,\n    scrollToBottom,\n  }\n}\n",
      "type": "registry:component",
      "target": "app/composables/useChatScroll.ts"
    },
    {
      "path": "registry/default/realtime-chat/nuxtjs/app/composables/useRealtimeChat.ts",
      "content": "import { onUnmounted, ref, watch } from 'vue'\n\n// @ts-ignore\nimport { createClient } from '@/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(props: UseRealtimeChatProps) {\n  const supabase = createClient()\n\n  const messages = ref<ChatMessage[]>([])\n  const channel = ref<ReturnType<typeof supabase.channel> | null>(null)\n  const isConnected = ref(false)\n\n  function cleanup() {\n    if (channel.value) {\n      supabase.removeChannel(channel.value)\n      channel.value = null\n    }\n  }\n\n  function setupChannel() {\n    if (!props.roomName) return\n\n    const newChannel = supabase.channel(props.roomName)\n\n    newChannel\n      .on('broadcast', { event: EVENT_MESSAGE_TYPE }, (payload: { payload: ChatMessage }) => {\n        messages.value.push(payload.payload as ChatMessage)\n      })\n      .subscribe((status: string) => {\n        isConnected.value = status === 'SUBSCRIBED'\n      })\n\n    channel.value = newChannel\n  }\n\n  watch(\n    () => props.roomName,\n    () => {\n      cleanup()\n      setupChannel()\n    },\n    { immediate: true }\n  )\n\n  onUnmounted(() => {\n    cleanup()\n  })\n\n  async function sendMessage(content: string) {\n    if (!channel.value || !isConnected.value) return\n\n    const message: ChatMessage = {\n      id: crypto.randomUUID(),\n      content,\n      user: {\n        name: props.username,\n      },\n      createdAt: new Date().toISOString(),\n    }\n\n    // Optimistic update\n    messages.value.push(message)\n\n    await channel.value.send({\n      type: 'broadcast',\n      event: EVENT_MESSAGE_TYPE,\n      payload: message,\n    })\n  }\n\n  return {\n    messages,\n    sendMessage,\n    isConnected,\n  }\n}\n",
      "type": "registry:component",
      "target": "app/composables/useRealtimeChat.ts"
    }
  ]
}