{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "faq-chat-accordion",
  "type": "registry:ui",
  "title": "FAQ Chat Accordion",
  "description": "Conversational FAQ with right/left message bubbles, typing indicator dots, spring message entrance, and layout FLIP repositioning.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/ruixenui/faq-chat-accordion.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\n/* ═══════════════════════════════════════════════════════════\n   FAQ Chat Accordion — Conversational Message Exchange.\n\n   A FAQ that feels like texting a support bot. Questions are\n   right-aligned bubbles (you sent them), answers are left-\n   aligned bubbles (the system replied). Click a question and\n   a single bubble opens — first showing typing dots, then\n   morphing smoothly into the answer text.\n\n   ALL height transitions use CSS grid-template-rows — this\n   changes actual DOM height (not transforms), so siblings\n   follow the transition naturally without FLIP tricks or\n   layout springs. Zero jank. Zero double-shift.\n\n   Motion is used sparingly: spring y+opacity for the bubble\n   entrance feel, spring whileTap for tactile press. That's\n   it. Everything else is CSS — because CSS grid transitions\n   are inherently smoother than transform-based layout hacks.\n\n   Animation architecture:\n     ┌─ Outer grid (0fr ↔ 1fr) ─── bubble enter/exit\n     │   └─ motion.div (y + opacity) ─── spring entrance\n     │       └─ Bubble container (bg-muted, rounded)\n     │           ├─ Inner grid A (dots: 1fr ↔ 0fr)\n     │           └─ Inner grid B (text: 0fr ↔ 1fr)\n     └─ Siblings follow naturally (real height, not transforms)\n\n   Four FAQ paradigms in ruixen, zero overlap:\n     • faq-auto-accordion:   spring traveling accent, motion physics\n     • faq-chat-accordion:   conversational message exchange\n     • faq-scroll-accordion: self-contained scroll cascade\n     • staggered-faq:        BlurredStagger text reveal, split\n\n   Dependencies: motion (only for whileTap + spring entrance).\n   ═══════════════════════════════════════════════════════════ */\n\nexport interface FAQItem {\n  question: string;\n  answer: string;\n}\n\nexport interface FAQChatAccordionProps {\n  title?: string;\n  items?: FAQItem[];\n  /** Milliseconds to show typing dots before answer. 0 to skip. */\n  typingDelay?: number;\n  className?: string;\n}\n\nconst defaultItems: FAQItem[] = [\n  {\n    question: \"How late does the internet close?\",\n    answer: \"The internet never closes — it runs 24/7, 365 days a year.\",\n  },\n  {\n    question: \"Do I need a license to browse?\",\n    answer:\n      \"No license required. Just open your browser and you're good to go.\",\n  },\n  {\n    question: \"What flavour are the cookies?\",\n    answer:\n      \"Our cookies are digital. They help us remember your preferences, not satisfy cravings.\",\n  },\n  {\n    question: \"Can I get lost here?\",\n    answer:\n      \"You might explore deeper than planned, but there's always a way back.\",\n  },\n  {\n    question: \"What if I click the wrong button?\",\n    answer:\n      \"Nothing breaks. Hit back, refresh, or just try again. We're forgiving.\",\n  },\n];\n\nconst EXPO = \"cubic-bezier(0.16, 1, 0.3, 1)\";\nconst EXPO_STRONG = \"cubic-bezier(0.22, 1, 0.36, 1)\";\n\nexport function FAQChatAccordion({\n  title = \"Have questions?\",\n  items = defaultItems,\n  typingDelay = 400,\n  className,\n}: FAQChatAccordionProps) {\n  const [active, setActive] = React.useState<number | null>(null);\n  const [showAnswer, setShowAnswer] = React.useState<number | null>(null);\n  const timerRef = React.useRef<ReturnType<typeof setTimeout>>();\n\n  const handleClick = React.useCallback(\n    (i: number) => {\n      if (timerRef.current) clearTimeout(timerRef.current);\n\n      if (active === i) {\n        // Close — don't clear showAnswer so bubble\n        // exits with current content (no reverse morph)\n        setActive(null);\n        return;\n      }\n\n      // Open — clear showAnswer to start typing phase\n      setShowAnswer(null);\n      setActive(i);\n\n      if (typingDelay > 0) {\n        timerRef.current = setTimeout(() => setShowAnswer(i), typingDelay);\n      } else {\n        setShowAnswer(i);\n      }\n    },\n    [active, typingDelay],\n  );\n\n  React.useEffect(() => {\n    return () => {\n      if (timerRef.current) clearTimeout(timerRef.current);\n    };\n  }, []);\n\n  return (\n    <section className={cn(\"py-16 md:py-24\", className)}>\n      <div className=\"mx-auto max-w-lg px-6\">\n        {/* ── Header ────────────────────────────────────── */}\n        {title && (\n          <h2 className=\"mb-6 text-center text-2xl font-semibold tracking-tight text-foreground\">\n            {title}\n          </h2>\n        )}\n\n        {/* ── Date separator ────────────────────────────── */}\n        <div className=\"mb-6 flex items-center gap-3\">\n          <div className=\"h-px flex-1 bg-border\" />\n          <span className=\"text-[11px] font-medium text-muted-foreground\">\n            Today\n          </span>\n          <div className=\"h-px flex-1 bg-border\" />\n        </div>\n\n        {/* ── Message thread ────────────────────────────── */}\n        <div className=\"flex flex-col gap-3\">\n          {items.map((item, i) => {\n            const isActive = active === i;\n            const hasAnswer = showAnswer === i;\n\n            return (\n              <div key={i}>\n                {/* ── Question bubble (right-aligned) ──── */}\n                <div className=\"flex justify-end\">\n                  <motion.button\n                    type=\"button\"\n                    className=\"max-w-[85%] rounded-2xl rounded-br-[6px] bg-foreground px-4 py-2.5 text-left text-[14px] font-medium text-background\"\n                    whileTap={{ scale: 0.98 }}\n                    transition={{\n                      type: \"spring\",\n                      stiffness: 500,\n                      damping: 30,\n                    }}\n                    onClick={() => handleClick(i)}\n                    aria-expanded={isActive}\n                  >\n                    {item.question}\n                  </motion.button>\n                </div>\n\n                {/* ── Answer area ─────────────────────────\n                     Outer CSS grid handles enter/exit height.\n                     Siblings follow naturally — real DOM height,\n                     not transforms.\n                     ──────────────────────────────────────── */}\n                <div\n                  className=\"grid\"\n                  style={{\n                    gridTemplateRows: isActive ? \"1fr\" : \"0fr\",\n                    transition: isActive\n                      ? `grid-template-rows 0.4s ${EXPO_STRONG}`\n                      : `grid-template-rows 0.25s ${EXPO}`,\n                  }}\n                >\n                  <div className=\"overflow-hidden\">\n                    {/* Spring entrance feel (y + opacity only) */}\n                    <motion.div\n                      initial={false}\n                      animate={{\n                        y: isActive ? 0 : 4,\n                        opacity: isActive ? 1 : 0,\n                      }}\n                      transition={{\n                        y: isActive\n                          ? {\n                              type: \"spring\",\n                              stiffness: 380,\n                              damping: 26,\n                            }\n                          : { duration: 0.15 },\n                        opacity: {\n                          duration: isActive ? 0.25 : 0.12,\n                        },\n                      }}\n                    >\n                      <div className=\"flex justify-start pt-2\">\n                        <div className=\"max-w-[85%] overflow-hidden rounded-2xl rounded-bl-[6px] bg-muted\">\n                          {/* ── Dots section ───────────── */}\n                          {/* Collapses when answer ready   */}\n                          <div\n                            className=\"grid\"\n                            style={{\n                              gridTemplateRows: hasAnswer ? \"0fr\" : \"1fr\",\n                              transition: `grid-template-rows 0.3s ${EXPO}`,\n                            }}\n                          >\n                            <div className=\"overflow-hidden\">\n                              <div className=\"flex items-center gap-[5px] px-4 py-3\">\n                                <span\n                                  className=\"inline-block h-[5px] w-[5px] rounded-full bg-muted-foreground/50\"\n                                  style={{\n                                    animation:\n                                      \"faqChatDot 1.2s ease-in-out infinite\",\n                                  }}\n                                />\n                                <span\n                                  className=\"inline-block h-[5px] w-[5px] rounded-full bg-muted-foreground/50\"\n                                  style={{\n                                    animation:\n                                      \"faqChatDot 1.2s ease-in-out 0.15s infinite\",\n                                  }}\n                                />\n                                <span\n                                  className=\"inline-block h-[5px] w-[5px] rounded-full bg-muted-foreground/50\"\n                                  style={{\n                                    animation:\n                                      \"faqChatDot 1.2s ease-in-out 0.3s infinite\",\n                                  }}\n                                />\n                              </div>\n                            </div>\n                          </div>\n\n                          {/* ── Text section ───────────── */}\n                          {/* Expands when answer ready     */}\n                          <div\n                            className=\"grid\"\n                            style={{\n                              gridTemplateRows: hasAnswer ? \"1fr\" : \"0fr\",\n                              transition: `grid-template-rows 0.3s ${EXPO}`,\n                            }}\n                          >\n                            <div className=\"overflow-hidden\">\n                              <p\n                                className=\"px-4 py-2.5 text-[14px] leading-relaxed text-foreground\"\n                                style={{\n                                  opacity: hasAnswer ? 1 : 0,\n                                  transition: hasAnswer\n                                    ? \"opacity 0.2s ease 0.08s\"\n                                    : \"opacity 0.1s ease\",\n                                }}\n                              >\n                                {item.answer}\n                              </p>\n                            </div>\n                          </div>\n                        </div>\n                      </div>\n                    </motion.div>\n                  </div>\n                </div>\n              </div>\n            );\n          })}\n        </div>\n      </div>\n\n      <style\n        dangerouslySetInnerHTML={{\n          __html: `@keyframes faqChatDot{0%,60%,100%{opacity:.3;transform:translateY(0)}30%{opacity:1;transform:translateY(-3px)}}`,\n        }}\n      />\n    </section>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ruixen/faq-chat-accordion.tsx"
    }
  ]
}