// components/ui/accordion.tsx
import * as React from 'react';
import { View, Text, Pressable } from 'react-native';
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
Easing,
} from 'react-native-reanimated';
import { cn } from '@/lib/utils';
type AccordionType = 'single' | 'multiple';
interface AccordionProps {
type?: AccordionType;
value?: string | string[];
onValueChange?: (value: string | string[]) => void;
children: React.ReactNode;
className?: string;
collapsible?: boolean;
}
interface AccordionItemProps {
value: string;
children: React.ReactNode;
className?: string;
disabled?: boolean;
}
interface AccordionTriggerProps {
children: React.ReactNode;
className?: string;
}
interface AccordionContentProps {
children: React.ReactNode;
className?: string;
}
const AccordionContext = React.createContext<{
type: AccordionType;
value?: string | string[];
onValueChange?: (value: string | string[]) => void;
collapsible: boolean;
} | null>(null);
const AccordionItemContext = React.createContext<{
value: string;
isOpen: boolean;
toggle: () => void;
disabled: boolean;
} | null>(null);
function useAccordion() {
const context = React.useContext(AccordionContext);
if (!context) {
throw new Error('Accordion components must be used within Accordion');
}
return context;
}
function useAccordionItem() {
const context = React.useContext(AccordionItemContext);
if (!context) {
throw new Error('AccordionTrigger and AccordionContent must be used within AccordionItem');
}
return context;
}
export function Accordion({
type = 'single',
value,
onValueChange,
children,
className,
collapsible = false,
}: AccordionProps) {
return (
{children}
);
}
export function AccordionItem({ value, children, className, disabled = false }: AccordionItemProps) {
const { type, value: accordionValue, onValueChange, collapsible } = useAccordion();
const isOpen = React.useMemo(() => {
if (type === 'single') {
return accordionValue === value;
} else {
return Array.isArray(accordionValue) && accordionValue.includes(value);
}
}, [type, accordionValue, value]);
const toggle = () => {
if (disabled) return;
if (type === 'single') {
if (isOpen && collapsible) {
onValueChange?.('');
} else {
onValueChange?.(value);
}
} else {
const currentValues = (accordionValue as string[]) || [];
if (isOpen) {
onValueChange?.(currentValues.filter((v) => v !== value));
} else {
onValueChange?.([...currentValues, value]);
}
}
};
return (
{children}
);
}
export function AccordionTrigger({ children, className }: AccordionTriggerProps) {
const { isOpen, toggle, disabled } = useAccordionItem();
const rotation = useSharedValue(0);
React.useEffect(() => {
rotation.value = withTiming(isOpen ? 180 : 0, {
duration: 200,
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
});
}, [isOpen]);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value}deg` }],
}));
return (
{children}
▼
);
}
export function AccordionContent({ children, className }: AccordionContentProps) {
const { isOpen } = useAccordionItem();
const height = useSharedValue(0);
const opacity = useSharedValue(0);
const [measured, setMeasured] = React.useState(false);
const [contentHeight, setContentHeight] = React.useState(0);
React.useEffect(() => {
if (measured) {
height.value = withTiming(isOpen ? contentHeight : 0, {
duration: 250,
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
});
opacity.value = withTiming(isOpen ? 1 : 0, {
duration: 200,
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
});
}
}, [isOpen, measured, contentHeight]);
const animatedStyle = useAnimatedStyle(() => ({
height: measured ? height.value : undefined,
opacity: measured ? opacity.value : 1,
overflow: 'hidden',
}));
const contentWrapperStyle = useAnimatedStyle(() => ({
// Fix text rewrap: use absolute positioning during animation
position: measured && height.value > 0 && height.value < contentHeight ? 'absolute' : 'relative',
top: 0,
left: 0,
right: 0,
}));
return (
{
const h = event.nativeEvent.layout.height;
if (h > 0 && !measured) {
setContentHeight(h);
setMeasured(true);
height.value = isOpen ? h : 0;
opacity.value = isOpen ? 1 : 0;
}
}}
className={cn('px-4 pb-4 bg-slate-50', className)}
>
{children}
);
}
export function AccordionTriggerText({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return {children};
}
export function AccordionContentText({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return {children};
}