{"version":1,"id":"chat-conversation","framework":"next","kind":"block","files":[{"path":"src/blocks/gfa/chat-conversation.tsx","content":"\"use client\";\n\nimport Image from \"next/image\";\nimport * as React from \"react\";\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type ChatMessage = {\n  id: string;\n  side: \"left\" | \"right\";\n  text: string;\n  avatarSrc: string;\n};\n\nexport const CHAT_CONVERSATION_DEMO_MESSAGES: ChatMessage[] = [\n  {\n    id: \"1\",\n    side: \"left\",\n    text: \"GFA just shared a new component.\",\n    avatarSrc: \"https://picsum.photos/id/64/96/96\",\n  },\n  {\n    id: \"2\",\n    side: \"right\",\n    text: \"Whoa — checking it right now.\",\n    avatarSrc: \"https://picsum.photos/id/91/96/96\",\n  },\n  {\n    id: \"3\",\n    side: \"left\",\n    text: \"How does it look?\",\n    avatarSrc: \"https://picsum.photos/id/338/96/96\",\n  },\n  {\n    id: \"4\",\n    side: \"right\",\n    text: \"Bro, let him cook!\",\n    avatarSrc: \"https://picsum.photos/id/91/96/96\",\n  },\n];\n\nconst STAGGER_BETWEEN_MESSAGES_S = 0.64;\nconst BUBBLE_AFTER_AVATAR_S = 0.24;\n\nfunction Bubble({ children, className }: { children: React.ReactNode; className?: string }) {\n  return (\n    <div\n      className={cn(\n        \"max-w-[min(100%,18rem)] rounded-2xl bg-foreground px-5 py-3 text-[0.9375rem] leading-snug text-background sm:max-w-[min(100%,22rem)] sm:text-[0.9375rem]\",\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n}\n\nfunction MessageRow({\n  message: m,\n  index,\n  inView,\n  reduceMotion,\n}: {\n  message: ChatMessage;\n  index: number;\n  inView: boolean;\n  reduceMotion: boolean;\n}) {\n  const baseDelay = index * STAGGER_BETWEEN_MESSAGES_S;\n  const bubbleDelay = baseDelay + BUBBLE_AFTER_AVATAR_S;\n  const isLeft = m.side === \"left\";\n  const bubbleX = isLeft ? -22 : 22;\n\n  const rowClass = isLeft\n    ? \"flex w-full items-center justify-start gap-3 sm:gap-3.5\"\n    : \"flex w-full items-center justify-end gap-3 sm:gap-3.5\";\n\n  const avatarInner = (\n    <Image\n      src={m.avatarSrc}\n      alt=\"\"\n      width={96}\n      height={96}\n      className=\"size-full object-cover\"\n      sizes=\"48px\"\n    />\n  );\n\n  const bubbleInner = <Bubble>{m.text}</Bubble>;\n\n  const avatarShellClass =\n    \"relative size-11 shrink-0 overflow-hidden rounded-full sm:size-12\";\n\n  if (reduceMotion) {\n    return (\n      <div className={rowClass}>\n        {isLeft ? (\n          <>\n            <div className={avatarShellClass}>{avatarInner}</div>\n            {bubbleInner}\n          </>\n        ) : (\n          <>\n            {bubbleInner}\n            <div className={avatarShellClass}>{avatarInner}</div>\n          </>\n        )}\n      </div>\n    );\n  }\n\n  const avatarMotion = {\n    initial: { scale: 0.35, opacity: 0 },\n    animate: inView ? { scale: 1, opacity: 1 } : { scale: 0.35, opacity: 0 },\n    transition: {\n      type: \"spring\" as const,\n      stiffness: 300,\n      damping: 40,\n      mass: 0.95,\n      delay: baseDelay,\n    },\n  };\n\n  const bubbleMotion = {\n    initial: { opacity: 0, x: bubbleX },\n    animate: inView ? { opacity: 1, x: 0 } : { opacity: 0, x: bubbleX },\n    transition: {\n      type: \"spring\" as const,\n      stiffness: 240,\n      damping: 42,\n      mass: 1.05,\n      delay: bubbleDelay,\n    },\n  };\n\n  if (isLeft) {\n    return (\n      <div className={rowClass}>\n        <motion.div className={avatarShellClass} {...avatarMotion}>\n          {avatarInner}\n        </motion.div>\n        <motion.div {...bubbleMotion}>{bubbleInner}</motion.div>\n      </div>\n    );\n  }\n\n  return (\n    <div className={rowClass}>\n      <motion.div {...bubbleMotion}>{bubbleInner}</motion.div>\n      <motion.div className={avatarShellClass} {...avatarMotion}>\n        {avatarInner}\n      </motion.div>\n    </div>\n  );\n}\n\nexport type ChatConversationProps = {\n  className?: string;\n  messages?: ChatMessage[];\n};\n\nexport function ChatConversation({\n  className,\n  messages = CHAT_CONVERSATION_DEMO_MESSAGES,\n}: ChatConversationProps) {\n  const reduceMotion = useReducedMotion() ?? false;\n  const containerRef = React.useRef<HTMLDivElement>(null);\n  const inView = useInView(containerRef, {\n    once: true,\n    margin: \"-12% 0px -12% 0px\",\n    amount: 0.22,\n  });\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\"flex w-full flex-col gap-2.5 sm:gap-3\", className)}\n      role=\"log\"\n      aria-label=\"Animated chat conversation\"\n    >\n      {messages.map((m, index) => (\n        <MessageRow\n          key={m.id}\n          message={m}\n          index={index}\n          inView={inView}\n          reduceMotion={reduceMotion}\n        />\n      ))}\n    </div>\n  );\n}\n"}],"meta":{"studioName":"GFA Development & Design Studio","siteUrl":"https://ui.gkhn.dev"}}