{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "faq-accordion",
  "type": "registry:ui",
  "title": "FAQ Accordion",
  "description": "A single-open accordion that animates its height open while the answer reveals line by line through a SplitText mask and the icon morphs from plus to minus.",
  "dependencies": [
    "gsap",
    "@gsap/react"
  ],
  "registryDependencies": [],
  "cssVars": {
    "theme": {}
  },
  "files": [
    {
      "path": "components/tweenui/faq-accordion.tsx",
      "type": "registry:component",
      "target": "components/tweenui/faq-accordion.tsx",
      "content": "'use client';\n\nimport { useId, useRef, useState, type ComponentPropsWithoutRef, type ReactNode } from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { CustomEase } from 'gsap/CustomEase';\nimport { SplitText } from 'gsap/SplitText';\nimport { cn } from '@/lib/utils';\n\ngsap.registerPlugin(useGSAP, CustomEase, SplitText);\nif (!CustomEase.get('faq-ease')) {\n  CustomEase.create('faq-ease', '0.625, 0.05, 0, 1');\n}\n\nconst prefersReducedMotion = () =>\n  typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\nexport interface FaqAccordionItem {\n  question: ReactNode;\n  answer: ReactNode;\n}\n\nexport interface FaqAccordionProps extends Omit<ComponentPropsWithoutRef<'div'>, 'onChange'> {\n  items: FaqAccordionItem[];\n  /** Index open on first render when uncontrolled. `null` starts fully closed. */\n  defaultOpen?: number | null;\n  /** Controlled open index. `null` = all closed. */\n  open?: number | null;\n  /** Fires with the newly open index (or `null`). */\n  onOpenChange?: (index: number | null) => void;\n  /** Let the open item close when clicked again. Defaults to `true`. */\n  collapsible?: boolean;\n}\n\nconst FaqAccordion = ({\n  items,\n  defaultOpen = 0,\n  open,\n  onOpenChange,\n  collapsible = true,\n  className,\n  ...props\n}: FaqAccordionProps) => {\n  const isControlled = open !== undefined;\n  const [internal, setInternal] = useState<number | null>(defaultOpen);\n  const openIndex = isControlled ? open : internal;\n\n  const rootRef = useRef<HTMLDivElement>(null);\n  const contentRefs = useRef<(HTMLDivElement | null)[]>([]);\n  const textRefs = useRef<(HTMLElement | null)[]>([]);\n  const splitRefs = useRef<(SplitText | null)[]>([]);\n  const firstRun = useRef(true);\n  const uid = useId();\n\n  const setOpen = (next: number | null) => {\n    if (!isControlled) setInternal(next);\n    onOpenChange?.(next);\n  };\n\n  const toggle = (index: number) =>\n    setOpen(openIndex === index ? (collapsible ? null : index) : index);\n\n  useGSAP(\n    () => {\n      const reduced = prefersReducedMotion();\n      const isFirst = firstRun.current;\n      firstRun.current = false;\n\n      const clearSplit = (index: number) => {\n        splitRefs.current[index]?.revert();\n        splitRefs.current[index] = null;\n      };\n\n      // Split into masked lines only while animating; revert after so text\n      // reflows normally on resize.\n      const splitLines = (index: number) => {\n        const text = textRefs.current[index];\n        if (!text) return null;\n        clearSplit(index);\n        const split = SplitText.create(text, {\n          type: 'lines',\n          mask: 'lines',\n          linesClass: 'faq-line',\n        });\n        splitRefs.current[index] = split;\n        return split;\n      };\n\n      contentRefs.current.forEach((content, index) => {\n        if (!content) return;\n        const opened = index === openIndex;\n        gsap.killTweensOf(content);\n\n        if (opened) {\n          if (isFirst || reduced) {\n            content.style.height = 'auto';\n            return;\n          }\n          gsap.set(content, { height: 'auto' });\n          const target = content.offsetHeight;\n          const split = splitLines(index);\n          gsap.fromTo(\n            content,\n            { height: 0 },\n            {\n              height: target,\n              duration: 0.6,\n              ease: 'faq-ease',\n              onComplete: () => {\n                content.style.height = 'auto';\n              },\n            }\n          );\n          if (split) {\n            gsap.fromTo(\n              split.lines,\n              { yPercent: 110 },\n              {\n                yPercent: 0,\n                duration: 0.65,\n                stagger: 0.06,\n                ease: 'power3.out',\n                onComplete: () => clearSplit(index),\n              }\n            );\n          }\n        } else {\n          if (isFirst || reduced) {\n            content.style.height = '0px';\n            return;\n          }\n          const split = splitLines(index);\n          if (split) {\n            gsap.to(split.lines, {\n              yPercent: 110,\n              duration: 0.45,\n              stagger: 0.04,\n              ease: 'power3.out',\n              onComplete: () => clearSplit(index),\n            });\n          }\n          gsap.to(content, { height: 0, duration: 0.6, ease: 'faq-ease' });\n        }\n      });\n\n      return () => {\n        splitRefs.current.forEach((split) => split?.revert());\n      };\n    },\n    { scope: rootRef, dependencies: [openIndex] }\n  );\n\n  return (\n    <div\n      ref={rootRef}\n      data-faq-accordion\n      className={cn('w-full max-w-[560px] space-y-3', className)}\n      {...props}\n    >\n      {items.map((item, index) => {\n        const opened = index === openIndex;\n        const buttonId = `${uid}-trigger-${index}`;\n        const panelId = `${uid}-panel-${index}`;\n\n        return (\n          <div\n            key={index}\n            data-faq-item\n            data-expand={opened ? 'true' : 'false'}\n            className=\"min-w-0 overflow-hidden rounded-2xl border border-[#045f64]/15 bg-white dark:border-[#045f64]/40 dark:bg-[#12161F]\"\n          >\n            <h3 className=\"min-w-0\">\n              <button\n                type=\"button\"\n                id={buttonId}\n                aria-expanded={opened}\n                aria-controls={panelId}\n                onClick={() => toggle(index)}\n                className=\"flex w-full min-w-0 cursor-pointer items-start justify-between gap-x-4 px-6 pt-5 pb-5 text-left text-base font-medium text-[#12161F] data-[expand=true]:pb-3 dark:text-white\"\n                data-expand={opened ? 'true' : 'false'}\n              >\n                <span className=\"min-w-0 flex-1 text-wrap\">{item.question}</span>\n                <span\n                  data-expand={opened ? 'true' : 'false'}\n                  className=\"group grid size-6 shrink-0 place-items-center text-[#045f64] dark:text-[#9fd4d6]\"\n                >\n                  <svg\n                    viewBox=\"0 0 24 24\"\n                    className=\"size-5 stroke-current\"\n                    fill=\"none\"\n                    strokeWidth={1.75}\n                    aria-hidden=\"true\"\n                  >\n                    <path d=\"M4 12h16\" strokeLinecap=\"round\" />\n                    <path\n                      d=\"M12 4v16\"\n                      strokeLinecap=\"round\"\n                      className=\"origin-center transition-opacity duration-300 group-data-[expand=true]:opacity-0 motion-reduce:transition-none\"\n                    />\n                  </svg>\n                </span>\n              </button>\n            </h3>\n\n            <div\n              id={panelId}\n              role=\"region\"\n              aria-labelledby={buttonId}\n              data-faq-content\n              ref={(node) => {\n                contentRefs.current[index] = node;\n              }}\n              className=\"min-w-0 overflow-hidden\"\n            >\n              <div\n                data-faq-text\n                ref={(node) => {\n                  textRefs.current[index] = node;\n                }}\n                className=\"px-6 pb-6 text-sm wrap-break-word text-[#045f64]/70 dark:text-[#9fd4d6]/80\"\n              >\n                {item.answer}\n              </div>\n            </div>\n          </div>\n        );\n      })}\n    </div>\n  );\n};\n\nexport default FaqAccordion;\n"
    }
  ],
  "docs": "Docs, props and examples: https://tween-ui.vercel.app/component/faq-accordion\nGSAP plugins (CustomEase, SplitText) are registered inside the component, so there is nothing to set up."
}