{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "accordion",
  "title": "Accordion",
  "description": "A vertically stacked set of interactive headings that reveal associated content sections.",
  "dependencies": [
    "react-native-reanimated"
  ],
  "registryDependencies": [
    "@tetra-ui/icons",
    "@tetra-ui/slot"
  ],
  "files": [
    {
      "path": "../../packages/tetra-ui/src/components/accordion.tsx",
      "content": "import type { ComponentPropsWithRef, ComponentRef, ReactNode } from \"react\";\nimport {\n  Children,\n  createContext,\n  isValidElement,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  type GestureResponderEvent,\n  Pressable,\n  View,\n  type ViewProps,\n} from \"react-native\";\nimport Animated, {\n  FadeInDown,\n  FadeOutUp,\n  LinearTransition,\n  useAnimatedStyle,\n  useSharedValue,\n  withSpring,\n} from \"react-native-reanimated\";\nimport { cn, mergeRefs } from \"@/lib/utils\";\nimport { ChevronDownIcon } from \"./icons\";\nimport { Slot } from \"./slot\";\n\n// Constants\nconst CONTENT_ENTER = FadeInDown.springify()\n  .damping(28)\n  .stiffness(340)\n  .mass(0.55);\nconst CONTENT_EXIT = FadeOutUp.duration(120).damping(32).stiffness(380);\nconst ITEM_LAYOUT = LinearTransition.springify()\n  .damping(28)\n  .stiffness(340)\n  .mass(0.6);\nconst ICON_SPRING = { damping: 26, mass: 0.5, stiffness: 320 };\n\nconst ACCORDION_ITEM_CLASSNAME = cn(\n  \"my-0 overflow-hidden rounded-none bg-card\",\n  \"data-[layout=open]:my-2 data-[layout=open]:rounded-2xl\",\n  \"data-[layout=closed-only]:rounded-2xl\",\n  \"data-[layout=closed-start]:rounded-t-2xl\",\n  \"data-[layout=closed-end]:rounded-b-2xl\"\n);\n\nconst AnimatedView = Animated.createAnimatedComponent(View);\n\n// Types\ntype AccordionType = \"single\" | \"multiple\";\n\ntype AccordionItemLayout =\n  | \"open\"\n  | \"closed-only\"\n  | \"closed-start\"\n  | \"closed-middle\"\n  | \"closed-end\";\n\ntype AccordionRootContextValue = {\n  type: AccordionType;\n  collapsible: boolean;\n  itemLayouts: Record<string, AccordionItemLayout>;\n  /** single: one value; multiple: open value set */\n  isOpen: (value: string) => boolean;\n  toggle: (value: string) => void;\n};\n\ntype AccordionItemContextValue = {\n  value: string;\n  isOpen: boolean;\n  contentId: string;\n  triggerId: string;\n};\n\nconst AccordionRootContext = createContext<AccordionRootContextValue | null>(\n  null\n);\nconst AccordionItemContext = createContext<AccordionItemContextValue | null>(\n  null\n);\n\nconst useAccordionRoot = () => {\n  const ctx = useContext(AccordionRootContext);\n  if (!ctx) {\n    throw new Error(\"Accordion components must be used within Accordion\");\n  }\n  return ctx;\n};\n\nconst useAccordionItem = () => {\n  const ctx = useContext(AccordionItemContext);\n  if (!ctx) {\n    throw new Error(\n      \"AccordionItem, AccordionTrigger, AccordionContent, and AccordionIndicator must be used within AccordionItem\"\n    );\n  }\n  return ctx;\n};\n\nconst getAccordionItemValues = (children: ReactNode): string[] => {\n  const values: string[] = [];\n\n  Children.forEach(children, (child) => {\n    if (\n      isValidElement<{ value?: string }>(child) &&\n      typeof child.props.value === \"string\"\n    ) {\n      values.push(child.props.value);\n    }\n  });\n\n  return values;\n};\n\nconst getClosedItemLayout = (\n  index: number,\n  itemValues: string[],\n  isItemOpen: (value: string) => boolean\n): AccordionItemLayout => {\n  const prevOpen = index === 0 || isItemOpen(itemValues[index - 1] ?? \"\");\n  const nextOpen =\n    index === itemValues.length - 1 || isItemOpen(itemValues[index + 1] ?? \"\");\n\n  if (prevOpen && nextOpen) {\n    return \"closed-only\";\n  }\n  if (prevOpen) {\n    return \"closed-start\";\n  }\n  if (nextOpen) {\n    return \"closed-end\";\n  }\n  return \"closed-middle\";\n};\n\nexport type AccordionProps = {\n  type?: AccordionType;\n  /** When `type` is `single`, allow closing the open item so none are open */\n  collapsible?: boolean;\n  /** Controlled open value(s) */\n  value?: string | string[];\n  defaultValue?: string | string[];\n  onValueChange?: (value: string | string[] | undefined) => void;\n  children: ReactNode;\n};\n\nexport const Accordion = ({\n  type = \"single\",\n  collapsible = false,\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  children,\n}: AccordionProps) => {\n  const itemValues = useMemo(\n    () => getAccordionItemValues(children),\n    [children]\n  );\n  const isControlled = valueProp !== undefined;\n\n  const [internalSingle, setInternalSingle] = useState<string | undefined>(\n    typeof defaultValue === \"string\" ? defaultValue : undefined\n  );\n  const [internalMultiple, setInternalMultiple] = useState<string[]>(\n    Array.isArray(defaultValue) ? defaultValue : []\n  );\n\n  const singleValue = isControlled\n    ? typeof valueProp === \"string\"\n      ? valueProp\n      : undefined\n    : internalSingle;\n\n  const multipleValues = useMemo(() => {\n    if (type !== \"multiple\") {\n      return new Set<string>();\n    }\n    if (isControlled) {\n      return new Set(Array.isArray(valueProp) ? valueProp : []);\n    }\n    return new Set(internalMultiple);\n  }, [type, isControlled, valueProp, internalMultiple]);\n\n  const isOpen = useCallback(\n    (itemValue: string) => {\n      if (type === \"multiple\") {\n        return multipleValues.has(itemValue);\n      }\n      return singleValue === itemValue;\n    },\n    [type, singleValue, multipleValues]\n  );\n\n  const itemLayouts = useMemo(() => {\n    const layouts: Record<string, AccordionItemLayout> = {};\n\n    for (let index = 0; index < itemValues.length; index += 1) {\n      const itemValue = itemValues[index] ?? \"\";\n\n      layouts[itemValue] = isOpen(itemValue)\n        ? \"open\"\n        : getClosedItemLayout(index, itemValues, isOpen);\n    }\n\n    return layouts;\n  }, [itemValues, isOpen]);\n\n  const toggle = useCallback(\n    (itemValue: string) => {\n      if (type === \"multiple\") {\n        const next = new Set(multipleValues);\n        if (next.has(itemValue)) {\n          next.delete(itemValue);\n        } else {\n          next.add(itemValue);\n        }\n        const arr = [...next];\n        if (!isControlled) {\n          setInternalMultiple(arr);\n        }\n        onValueChange?.(arr);\n        return;\n      }\n\n      let next: string | undefined;\n      if (singleValue === itemValue) {\n        next = collapsible ? undefined : itemValue;\n      } else {\n        next = itemValue;\n      }\n      if (!isControlled) {\n        setInternalSingle(next);\n      }\n      onValueChange?.(next);\n    },\n    [\n      type,\n      multipleValues,\n      singleValue,\n      collapsible,\n      isControlled,\n      onValueChange,\n    ]\n  );\n\n  const ctx = useMemo(\n    () => ({\n      collapsible,\n      isOpen,\n      itemLayouts,\n      toggle,\n      type,\n    }),\n    [type, collapsible, itemLayouts, isOpen, toggle]\n  );\n\n  return (\n    <AccordionRootContext.Provider value={ctx}>\n      {children}\n    </AccordionRootContext.Provider>\n  );\n};\n\nexport type AccordionItemProps = ViewProps & {\n  value: string;\n  children: ReactNode;\n};\n\nexport const AccordionItem = ({\n  value,\n  className,\n  children,\n  ...props\n}: AccordionItemProps) => {\n  const root = useAccordionRoot();\n  const baseId = useId();\n  const triggerId = `${baseId}-trigger`;\n  const contentId = `${baseId}-content`;\n  const open = root.isOpen(value);\n  const layout = root.itemLayouts[value] ?? \"closed-only\";\n\n  const itemCtx = useMemo(\n    () => ({\n      contentId,\n      isOpen: open,\n      triggerId,\n      value,\n    }),\n    [value, open, contentId, triggerId]\n  );\n\n  return (\n    <AccordionItemContext.Provider value={itemCtx}>\n      <AnimatedView\n        className={cn(ACCORDION_ITEM_CLASSNAME, className)}\n        data-layout={layout}\n        layout={ITEM_LAYOUT}\n        {...props}\n      >\n        {children}\n      </AnimatedView>\n    </AccordionItemContext.Provider>\n  );\n};\n\nexport type AccordionTriggerProps = Omit<\n  ComponentPropsWithRef<typeof Pressable>,\n  \"children\"\n> & {\n  asChild?: boolean;\n  children?: ReactNode;\n};\n\nexport const AccordionTrigger = ({\n  asChild,\n  children,\n  className,\n  onPress,\n  ref: refProp,\n  ...props\n}: AccordionTriggerProps) => {\n  const root = useAccordionRoot();\n  const item = useAccordionItem();\n  const ref = useRef<ComponentRef<typeof Pressable>>(null);\n  const mergedRefs = mergeRefs(ref, refProp);\n\n  const handlePress = useCallback(\n    (e: GestureResponderEvent) => {\n      onPress?.(e);\n      root.toggle(item.value);\n    },\n    [onPress, root, item.value]\n  );\n\n  const Comp = asChild ? Slot.Pressable : Pressable;\n\n  return (\n    <Comp\n      accessibilityRole=\"button\"\n      accessibilityState={{ expanded: item.isOpen }}\n      className={cn(\n        \"flex flex-row items-center justify-between gap-2 bg-card px-3 py-3 active:opacity-75\",\n        className\n      )}\n      nativeID={item.triggerId}\n      onPress={handlePress}\n      ref={mergedRefs}\n      {...props}\n    >\n      {children}\n    </Comp>\n  );\n};\n\nexport type AccordionContentProps = ViewProps & {\n  children: ReactNode;\n};\n\nexport const AccordionContent = ({\n  children,\n  className,\n  ...props\n}: AccordionContentProps) => {\n  const item = useAccordionItem();\n\n  if (!item.isOpen) {\n    return null;\n  }\n\n  return (\n    <Animated.View\n      entering={CONTENT_ENTER}\n      exiting={CONTENT_EXIT}\n      nativeID={item.contentId}\n    >\n      <View className={cn(\"px-3 pt-1 pb-3\", className)} {...props}>\n        {children}\n      </View>\n    </Animated.View>\n  );\n};\n\nexport type AccordionIndicatorProps = {\n  className?: string;\n  children?: ReactNode;\n};\n\nexport const AccordionIndicator = ({\n  className,\n  children,\n}: AccordionIndicatorProps) => {\n  const item = useAccordionItem();\n  const rotation = useSharedValue(item.isOpen ? 180 : 0);\n\n  useEffect(() => {\n    rotation.value = withSpring(item.isOpen ? 180 : 0, ICON_SPRING);\n  }, [item.isOpen, rotation]);\n\n  const animatedStyle = useAnimatedStyle(() => ({\n    transform: [{ rotate: `${rotation.value}deg` }],\n  }));\n\n  if (children) {\n    return (\n      <Animated.View className=\"shrink-0\" style={animatedStyle}>\n        {children}\n      </Animated.View>\n    );\n  }\n\n  return (\n    <Animated.View style={animatedStyle}>\n      <ChevronDownIcon\n        className={cn(\"size-5 shrink-0 text-muted-foreground\", className)}\n      />\n    </Animated.View>\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}