{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chat-thread",
  "title": "Chat Thread",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-react/blocks/chat-thread/ChatThread.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { ArrowRight, AtSign, Paperclip, Phone, Smile, Video, MoreVertical, Check, CheckCheck } from 'lucide-react'\nimport { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'\nimport { Button } from '@/components/ui/button'\nimport { Textarea } from '@/components/ui/textarea'\n\ntype MessageStatus = 'sent' | 'delivered' | 'read'\n\ninterface Message {\n  id: string\n  author: 'me' | 'them'\n  body: string\n  sentAt: Date\n  status?: MessageStatus\n}\n\nconst now = new Date()\nconst yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000)\n\nconst peer = {\n  name: 'Marcus Rivera',\n  handle: '@marcus',\n  avatarUrl: '',\n  initials: 'MR',\n  online: true,\n  lastSeen: 'Active now',\n}\n\nconst initialMessages: Message[] = [\n  {\n    id: 'm1',\n    author: 'them',\n    body: 'Hey — did you get a chance to look at the onboarding doc?',\n    sentAt: new Date(yesterday.getTime() + 9 * 60 * 60 * 1000 + 14 * 60 * 1000),\n  },\n  {\n    id: 'm2',\n    author: 'me',\n    body: 'Yeah, just finished. Two small notes on the time-off flow but otherwise looks solid.',\n    sentAt: new Date(yesterday.getTime() + 9 * 60 * 60 * 1000 + 21 * 60 * 1000),\n    status: 'read',\n  },\n  {\n    id: 'm3',\n    author: 'them',\n    body: 'Perfect. Drop them in the doc and I will action this afternoon.',\n    sentAt: new Date(yesterday.getTime() + 9 * 60 * 60 * 1000 + 23 * 60 * 1000),\n  },\n  {\n    id: 'm4',\n    author: 'me',\n    body: 'Done. Also — are we still on for the dashboard review tomorrow at 10?',\n    sentAt: new Date(now.getTime() - 42 * 60 * 1000),\n    status: 'read',\n  },\n  {\n    id: 'm5',\n    author: 'them',\n    body: 'Yes! I will bring the kanban deck. Want to push it to 10:30 so we can join from the standup?',\n    sentAt: new Date(now.getTime() - 8 * 60 * 1000),\n  },\n]\n\nfunction dayLabel(d: Date): string {\n  const sameDay = d.toDateString() === now.toDateString()\n  if (sameDay) return 'Today'\n  const sameYesterday = d.toDateString() === yesterday.toDateString()\n  if (sameYesterday) return 'Yesterday'\n  return d.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })\n}\n\nfunction formatTime(d: Date): string {\n  return d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })\n}\n\nexport function ChatThread() {\n  const [messages, setMessages] = React.useState<Message[]>(initialMessages)\n  const [draft, setDraft] = React.useState('')\n  const [peerTyping, setPeerTyping] = React.useState(false)\n  const scrollRoot = React.useRef<HTMLDivElement | null>(null)\n  const composerRef = React.useRef<HTMLTextAreaElement | null>(null)\n  // Keep `send` referenced by the native keydown listener fresh without\n  // re-binding on every keystroke.\n  const sendRef = React.useRef<() => void>(() => {})\n\n  const groupedMessages = React.useMemo(() => {\n    const groups: Array<{ label: string; items: Message[] }> = []\n    for (const m of messages) {\n      const label = dayLabel(m.sentAt)\n      const last = groups[groups.length - 1]\n      if (last && last.label === label) last.items.push(m)\n      else groups.push({ label, items: [m] })\n    }\n    return groups\n  }, [messages])\n\n  function scrollToBottom() {\n    if (!scrollRoot.current) return\n    scrollRoot.current.scrollTop = scrollRoot.current.scrollHeight\n  }\n\n  React.useEffect(() => {\n    scrollToBottom()\n  }, [messages])\n\n  function send() {\n    const body = draft.trim()\n    if (!body) return\n    setMessages((prev) => [\n      ...prev,\n      {\n        id: `m${Date.now()}`,\n        author: 'me',\n        body,\n        sentAt: new Date(),\n        status: 'sent',\n      },\n    ])\n    setDraft('')\n    setPeerTyping(true)\n    window.setTimeout(() => {\n      setPeerTyping(false)\n    }, 1800)\n  }\n\n  // The Textarea primitive's `onKeyDown` prop fires with no event, so attach a\n  // native keydown listener to the underlying <textarea> to implement\n  // Enter-to-send / Shift+Enter-for-newline.\n  React.useEffect(() => {\n    sendRef.current = send\n  })\n\n  React.useEffect(() => {\n    const el = composerRef.current\n    if (!el) return\n    function onComposerKey(e: KeyboardEvent) {\n      if (e.key === 'Enter' && !e.shiftKey) {\n        e.preventDefault()\n        sendRef.current()\n      }\n    }\n    el.addEventListener('keydown', onComposerKey)\n    return () => el.removeEventListener('keydown', onComposerKey)\n  }, [])\n\n  return (\n    <div className=\"bg-card text-card-foreground flex h-[680px] w-full flex-col overflow-hidden rounded-xl border shadow-sm\">\n      <header className=\"flex h-14 shrink-0 items-center justify-between gap-3 border-b px-4\">\n        <div className=\"flex min-w-0 items-center gap-3\">\n          <div className=\"relative\">\n            <Avatar className=\"size-9\">\n              {peer.avatarUrl && <AvatarImage src={peer.avatarUrl} alt={peer.name} />}\n              <AvatarFallback className=\"text-xs font-medium\">{peer.initials}</AvatarFallback>\n            </Avatar>\n            {peer.online && (\n              <span className=\"bg-success ring-card absolute -right-0.5 -bottom-0.5 size-2.5 rounded-full ring-2\" />\n            )}\n          </div>\n          <div className=\"min-w-0\">\n            <p className=\"truncate text-sm font-semibold tracking-tight\">{peer.name}</p>\n            <p className=\"text-muted-foreground truncate text-xs\">{peer.lastSeen}</p>\n          </div>\n        </div>\n        <div className=\"flex items-center gap-1\">\n          <Button variant=\"ghost\" size=\"icon\" className=\"size-8\" aria-label=\"Voice call\">\n            <Phone className=\"size-4\" />\n          </Button>\n          <Button variant=\"ghost\" size=\"icon\" className=\"size-8\" aria-label=\"Video call\">\n            <Video className=\"size-4\" />\n          </Button>\n          <Button variant=\"ghost\" size=\"icon\" className=\"size-8\" aria-label=\"More options\">\n            <MoreVertical className=\"size-4\" />\n          </Button>\n        </div>\n      </header>\n\n      <div ref={scrollRoot} className=\"chat-scroll flex-1 space-y-5 overflow-y-auto px-4 py-4\">\n        {groupedMessages.map((group) => (\n          <div key={group.label} className=\"space-y-3\">\n            <div className=\"flex items-center justify-center\">\n              <span className=\"text-muted-foreground bg-muted/60 rounded-full px-2.5 py-0.5 text-[10px] font-medium tracking-wide uppercase\">\n                {group.label}\n              </span>\n            </div>\n            {group.items.map((m) => (\n              <div\n                key={m.id}\n                className={`flex items-end gap-2 ${m.author === 'me' ? 'justify-end' : 'justify-start'}`}\n              >\n                {m.author === 'them' && (\n                  <Avatar className=\"size-7\">\n                    <AvatarFallback className=\"text-[10px]\">{peer.initials}</AvatarFallback>\n                  </Avatar>\n                )}\n                <div\n                  className={`flex max-w-[78%] flex-col gap-1 ${m.author === 'me' ? 'items-end' : 'items-start'}`}\n                >\n                  <div\n                    className={`rounded-2xl px-3.5 py-2 text-sm leading-relaxed shadow-sm ${\n                      m.author === 'me'\n                        ? 'bg-primary text-primary-foreground rounded-br-md'\n                        : 'bg-muted text-foreground rounded-bl-md'\n                    }`}\n                  >\n                    {m.body}\n                  </div>\n                  <div className=\"text-muted-foreground flex items-center gap-1 px-1 text-[10px] tabular-nums\">\n                    <span>{formatTime(m.sentAt)}</span>\n                    {m.author === 'me' && m.status === 'read' && <CheckCheck className=\"text-info size-3\" />}\n                    {m.author === 'me' && m.status === 'delivered' && <CheckCheck className=\"size-3\" />}\n                    {m.author === 'me' && m.status === 'sent' && <Check className=\"size-3\" />}\n                  </div>\n                </div>\n              </div>\n            ))}\n          </div>\n        ))}\n\n        {peerTyping && (\n          <div className=\"flex items-end gap-2\">\n            <Avatar className=\"size-7\">\n              <AvatarFallback className=\"text-[10px]\">{peer.initials}</AvatarFallback>\n            </Avatar>\n            <div className=\"bg-muted text-muted-foreground flex items-center gap-1 rounded-2xl rounded-bl-md px-3 py-2.5\">\n              <span className=\"typing-dot\" />\n              <span className=\"typing-dot\" style={{ animationDelay: '0.15s' }} />\n              <span className=\"typing-dot\" style={{ animationDelay: '0.3s' }} />\n            </div>\n          </div>\n        )}\n      </div>\n\n      <div className=\"bg-muted/20 border-t px-4 py-3\">\n        <div className=\"bg-background border-input/60 focus-within:border-primary/40 focus-within:ring-primary/10 group flex flex-col overflow-hidden rounded-2xl border shadow-sm transition-all duration-150 focus-within:shadow-md focus-within:ring-4\">\n          <Textarea\n            ref={composerRef}\n            value={draft}\n            onValueChange={setDraft}\n            placeholder={`Message ${peer.name.split(' ')[0]}`}\n            rows={1}\n            variant=\"plain\"\n            className=\"placeholder:text-muted-foreground/70 max-h-40 min-h-[44px] resize-none border-0 bg-transparent px-4 pt-3 pb-1 text-sm leading-relaxed shadow-none focus-visible:ring-0\"\n          />\n          <div className=\"flex items-center justify-between gap-2 px-2 pb-2\">\n            <div className=\"flex items-center gap-0.5\">\n              <Button\n                variant=\"ghost\"\n                size=\"icon\"\n                className=\"text-muted-foreground hover:text-foreground size-8\"\n                aria-label=\"Attach file\"\n              >\n                <Paperclip className=\"size-4\" />\n              </Button>\n              <Button\n                variant=\"ghost\"\n                size=\"icon\"\n                className=\"text-muted-foreground hover:text-foreground size-8\"\n                aria-label=\"Emoji\"\n              >\n                <Smile className=\"size-4\" />\n              </Button>\n              <Button\n                variant=\"ghost\"\n                size=\"icon\"\n                className=\"text-muted-foreground hover:text-foreground size-8\"\n                aria-label=\"Mention\"\n              >\n                <AtSign className=\"size-4\" />\n              </Button>\n            </div>\n            <div className=\"flex items-center gap-2\">\n              <kbd className=\"bg-muted text-muted-foreground hidden h-5 items-center gap-0.5 rounded border px-1.5 font-mono text-[10px] sm:inline-flex\">\n                <span>↵</span>\n              </kbd>\n              <Button\n                className=\"h-8 gap-1.5 rounded-lg px-3 text-xs font-medium\"\n                disabled={!draft.trim()}\n                aria-label=\"Send\"\n                onClick={send}\n              >\n                Send\n                <ArrowRight className=\"size-3.5\" />\n              </Button>\n            </div>\n          </div>\n        </div>\n        <p className=\"text-muted-foreground/70 mt-2 text-center text-[10px]\">\n          <kbd className=\"bg-muted/60 rounded px-1 py-0.5 font-mono text-[9px]\">Enter</kbd>\n          {' '}to send\n          <span className=\"mx-1.5 opacity-50\">·</span>\n          <kbd className=\"bg-muted/60 rounded px-1 py-0.5 font-mono text-[9px]\">Shift</kbd>\n          {' '}+{' '}\n          <kbd className=\"bg-muted/60 rounded px-1 py-0.5 font-mono text-[9px]\">↵</kbd>\n          {' '}for newline\n        </p>\n      </div>\n\n      <style>{`\n        .chat-scroll::-webkit-scrollbar {\n          width: 6px;\n        }\n        .chat-scroll::-webkit-scrollbar-thumb {\n          background: var(--border);\n          border-radius: 3px;\n        }\n        .typing-dot {\n          display: inline-block;\n          width: 6px;\n          height: 6px;\n          border-radius: 9999px;\n          background: currentColor;\n          opacity: 0.5;\n          animation: typing-bounce 1s infinite ease-in-out;\n        }\n        @keyframes typing-bounce {\n          0%,\n          80%,\n          100% {\n            transform: translateY(0);\n            opacity: 0.35;\n          }\n          40% {\n            transform: translateY(-3px);\n            opacity: 0.9;\n          }\n        }\n      `}</style>\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/ChatThread.tsx"
    }
  ],
  "dependencies": [
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/avatar.json",
    "https://uipkge.dev/r/react/button.json",
    "https://uipkge.dev/r/react/textarea.json"
  ],
  "description": "Single-pane conversation surface. Peer header (avatar + online dot + call/video/more), grouped messages with day separators (Today/Yesterday/date), own vs. peer bubbles, read/delivered/sent ticks, typing indicator, composer with attach/emoji and Enter-to-send. Stateful demo; swap the `messages` ref for your transport.",
  "categories": [
    "communication"
  ]
}