{
  "name": "animated-faq",
  "type": "registry:component",
  "title": "Animated FAQ",
  "description": "Accessible accordion-style FAQ component with smooth GSAP animations",
  "dependencies": [
    "gsap",
    "lucide-react"
  ],
  "registryDependencies": [],
  "exportName": "AnimatedFAQ",
  "exportKind": "default",
  "tier": "free",
  "version": "1.2.0",
  "changelog": [
    {
      "version": "1.2.0",
      "date": "2026-07-28",
      "summary": "Documented existing prefers-reduced-motion support : the accordion open/close tween duration is set to 0 under reduced motion instead of animating.",
      "breaking": false
    },
    {
      "version": "1.1.0",
      "date": "2026-07-23",
      "summary": "Registry copy synced with the production component the demo renders: adds the iconMode prop (rotate / rotate-left-down with ChevronRight), simpler array-based controlled-mode detection, and the height-based open/close animation engine.",
      "breaking": false
    }
  ],
  "files": [
    {
      "path": "index.jsx",
      "type": "registry:component",
      "target": "src/components/effects/animated-faq/index.jsx",
      "content": "// Built using Hyperiux Vault: https://vault.hyperiux.com\n\n\"use client\";\n\nimport React, {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\n\nimport gsap from \"gsap\";\nimport { ChevronDown, ChevronRight } from \"lucide-react\";\n\nconst useIsomorphicLayoutEffect =\n  typeof window !== \"undefined\" ? useLayoutEffect : useEffect;\n\nconst FAQContext = createContext(null);\nconst FAQGroupContext = createContext(null);\n\nconst useFAQContext = () => {\n  const context = useContext(FAQContext);\n\n  if (!context) {\n    throw new Error(\"FAQTitle and FAQContent must be used inside FAQWrapper.\");\n  }\n\n  return context;\n};\n\nexport function FAQGroup({\n  children,\n  allowMultiple = false,\n  defaultOpenItems = [],\n  value,\n  onChange,\n}) {\n  const isControlled = Array.isArray(value);\n  const [internalOpenItems, setInternalOpenItems] = useState(defaultOpenItems);\n\n  const openItems = isControlled ? value : internalOpenItems;\n\n  const toggleItem = useCallback(\n    (itemId) => {\n      const next = (() => {\n        const isOpen = openItems.includes(itemId);\n\n        if (allowMultiple) {\n          return isOpen\n            ? openItems.filter((id) => id !== itemId)\n            : [...openItems, itemId];\n        }\n\n        return isOpen ? [] : [itemId];\n      })();\n\n      if (!isControlled) {\n        setInternalOpenItems(next);\n      }\n\n      onChange?.(next);\n    },\n    [allowMultiple, isControlled, onChange, openItems]\n  );\n\n  const contextValue = useMemo(\n    () => ({\n      allowMultiple,\n      openItems,\n      toggleItem,\n    }),\n    [allowMultiple, openItems, toggleItem]\n  );\n\n  return (\n    <FAQGroupContext.Provider value={contextValue}>\n      {children}\n    </FAQGroupContext.Provider>\n  );\n}\n\nexport function FAQWrapper({\n  children,\n  className = \"\",\n  titleClassName = \"\",\n  contentClassName = \"\",\n  iconClassName = \"\",\n  iconSize = 18,\n  iconStrokeWidth = 2,\n  duration = 0.45,\n  defaultOpen = false,\n  controlledOpen,\n  onToggle,\n  itemId,\n}) {\n  const group = useContext(FAQGroupContext);\n\n  const generatedId = useId();\n  const resolvedItemId = itemId ?? generatedId;\n\n  const isStandaloneControlled = typeof controlledOpen === \"boolean\";\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n\n  const isOpen = group\n    ? group.openItems.includes(resolvedItemId)\n    : isStandaloneControlled\n      ? controlledOpen\n      : internalOpen;\n\n  const contentOuterRef = useRef(null);\n  const contentInnerRef = useRef(null);\n  const openHeightRef = useRef(0);\n  const reduceMotionRef = useRef(\n    typeof window !== \"undefined\" &&\n      (window.matchMedia?.(\"(prefers-reduced-motion: reduce)\")?.matches ?? false)\n  );\n\n  const contentId = useId();\n  const buttonId = useId();\n\n  useEffect(() => {\n    const mq = window.matchMedia?.(\"(prefers-reduced-motion: reduce)\");\n    if (!mq) return;\n    const onChange = (event) => {\n      reduceMotionRef.current = event.matches;\n    };\n    reduceMotionRef.current = mq.matches;\n    mq.addEventListener?.(\"change\", onChange);\n    return () => mq.removeEventListener?.(\"change\", onChange);\n  }, []);\n\n  const handleToggle = () => {\n    if (group) {\n      group.toggleItem(resolvedItemId);\n      onToggle?.(!isOpen);\n      return;\n    }\n\n    if (isStandaloneControlled) {\n      onToggle?.(!controlledOpen);\n      return;\n    }\n\n    setInternalOpen((prev) => {\n      const next = !prev;\n      onToggle?.(next);\n      return next;\n    });\n  };\n\n  const handleWrapperClick = (event) => {\n    const isInteractive = event.target.closest(\n      \"a, button, input, textarea, select\"\n    );\n\n    if (isInteractive) return;\n\n    handleToggle();\n  };\n\n  const handleKeyDown = (event) => {\n    if (event.key === \"Enter\" || event.key === \" \") {\n      event.preventDefault();\n      handleToggle();\n    }\n  };\n\n  useIsomorphicLayoutEffect(() => {\n    if (!contentOuterRef.current || !contentInnerRef.current) return;\n\n    const outer = contentOuterRef.current;\n    const inner = contentInnerRef.current;\n\n    gsap.killTweensOf([outer, inner]);\n\n    const tweenDuration = reduceMotionRef.current ? 0 : duration;\n\n    if (isOpen) {\n      gsap.set(outer, {\n        overflow: \"hidden\",\n        height: \"auto\",\n      });\n\n      const targetHeight = outer.scrollHeight;\n      openHeightRef.current = targetHeight;\n      gsap.set(outer, { height: 0 });\n\n      gsap.fromTo(\n        outer,\n        { height: 0 },\n        {\n          height: targetHeight,\n          duration: tweenDuration,\n          ease: \"power3.out\",\n          onComplete: () => {\n            gsap.set(outer, {\n              height: \"auto\",\n              overflow: \"visible\",\n            });\n          },\n        }\n      );\n    } else {\n      gsap.set(outer, { overflow: \"hidden\" });\n\n      gsap.fromTo(\n        outer,\n        { height: openHeightRef.current },\n        {\n          height: 0,\n          duration: tweenDuration,\n          ease: \"power3.out\",\n        }\n      );\n    }\n  }, [isOpen, duration]);\n\n  const value = {\n    isOpen,\n    contentOuterRef,\n    contentInnerRef,\n    contentId,\n    buttonId,\n    titleClassName,\n    contentClassName,\n    iconClassName,\n    iconSize,\n    iconStrokeWidth,\n  };\n\n  return (\n    <FAQContext.Provider value={value}>\n      <div\n        className={`cursor-pointer ${className}`}\n        role=\"button\"\n        tabIndex={0}\n        aria-expanded={isOpen}\n        aria-controls={contentId}\n        id={buttonId}\n        onClick={handleWrapperClick}\n        onKeyDown={handleKeyDown}\n      >\n        {children}\n      </div>\n    </FAQContext.Provider>\n  );\n}\n\nexport function FAQTitle({\n  children,\n  className = \"\",\n  showIcon = true,\n  iconPosition = \"right\",\n  iconMode = \"rotate\",\n}) {\n  const {\n    isOpen,\n    titleClassName,\n    iconClassName,\n    iconSize,\n    iconStrokeWidth,\n  } = useFAQContext();\n\n  const icon = showIcon ? (\n    <div className={`shrink-0 ${iconClassName}`}>\n      {iconMode === \"rotate-left-down\" ? (\n        <ChevronRight\n          size={iconSize}\n          strokeWidth={iconStrokeWidth}\n          className={`transition-transform duration-300 ease-out motion-reduce:transition-none ${\n            isOpen ? \"rotate-90\" : \"rotate-0\"\n          }`}\n          aria-hidden=\"true\"\n        />\n      ) : (\n        <ChevronDown\n          size={iconSize}\n          strokeWidth={iconStrokeWidth}\n          className={`transition-transform duration-300 ease-out motion-reduce:transition-none ${\n            isOpen ? \"rotate-180\" : \"rotate-0\"\n          }`}\n          aria-hidden=\"true\"\n        />\n      )}\n    </div>\n  ) : null;\n\n  return (\n    <div\n      className={`flex w-full items-center justify-between gap-6 ${titleClassName} ${className}`}\n    >\n      {iconPosition === \"left\" ? (\n        <>\n          {icon}\n          <div className=\"flex-1\">{children}</div>\n        </>\n      ) : (\n        <>\n          <div className=\"flex-1\">{children}</div>\n          {icon}\n        </>\n      )}\n    </div>\n  );\n}\n\nexport function FAQContent({\n  children,\n  className = \"\",\n  innerClassName = \"\",\n}) {\n  const {\n    isOpen,\n    contentOuterRef,\n    contentInnerRef,\n    contentId,\n    buttonId,\n    contentClassName,\n  } = useFAQContext();\n\n  return (\n    <div\n      id={contentId}\n      ref={contentOuterRef}\n      role=\"region\"\n      aria-labelledby={buttonId}\n      style={{\n        height: isOpen ? \"auto\" : 0,\n        overflow: isOpen ? \"visible\" : \"hidden\",\n      }}\n      className={contentClassName}\n    >\n      <div ref={contentInnerRef} className={className}>\n        <div className={innerClassName}>{children}</div>\n      </div>\n    </div>\n  );\n}\n\nexport const AnimatedFAQ = {\n  Group: FAQGroup,\n  Wrapper: FAQWrapper,\n  Title: FAQTitle,\n  Content: FAQContent,\n};\n\nexport default AnimatedFAQ;\n"
    }
  ]
}