Look at my code:
import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons';
import { LegendList } from '@legendapp/list';
import { useContext, useMemo, useState } from 'react';
import { View } from 'react-native';
import Animated, {
  withTiming,
  LinearTransition,
  Easing,
  EntryAnimationsValues,
  ExitAnimationsValues,
  LayoutAnimation,
  useAnimatedStyle,
} from 'react-native-reanimated';

import { Category, Record, RecordType } from '@/src/types/model.types';
import { MainContext } from '@components/context';
import { Box, HStack } from '@components/ui/gluestack';
import { Text, TouchableRipple } from '@components/ui/react-native-paper';

const ANIMATION_DURATION = 300;

const expand = (values: EntryAnimationsValues): LayoutAnimation => {
  'worklet';
  return {
    initialValues: {
      height: 0,
      opacity: 0,
    },
    animations: {
      height: withTiming(values.targetHeight, { duration: ANIMATION_DURATION }),
      opacity: withTiming(1, { duration: ANIMATION_DURATION }),
    },
  };
};

const collapse = (values: ExitAnimationsValues): LayoutAnimation => {
  'worklet';
  return {
    initialValues: {
      height: values.currentHeight,
      opacity: 1,
    },
    animations: {
      height: withTiming(0, { duration: ANIMATION_DURATION }),
      opacity: withTiming(0, { duration: ANIMATION_DURATION }),
    },
  };
};

const AnimatedChevron: React.FC<{ isExpanded: boolean }> = ({ isExpanded }) => {
  const animatedStyle = useAnimatedStyle(() => {
    return {
      transform: [
        {
          rotate: withTiming(isExpanded ? '-180deg' : '0deg', {
            duration: ANIMATION_DURATION,
          }),
        },
      ],
    };
  });

  return (
    <Animated.View style={animatedStyle}>
      <Text>
        <MaterialCommunityIcons name="menu-down" size={28} />
      </Text>
    </Animated.View>
  );
};

AnimatedChevron.displayName = 'AnimatedChevron';

interface BalanceTransactionsByCategoryProps {
  income: number;
  expenses: number;
  recordType: RecordType;
  records: Record[];
}

export const BalanceTransactionsByCategory: React.FC<
  BalanceTransactionsByCategoryProps
> = ({ records, income, expenses, recordType }) => {
  const { categoriesMap } = useContext(MainContext);
  const [expandedCategoryIds, setExpandedCategoryIds] = useState(
    new Set<string>(),
  );

  const listData = useMemo(() => {
    const grouped = records.reduce((acc, record) => {
      if (record.transfer_id || record.type !== recordType) {
        return acc;
      }
      const categoryId = record.category_id!.toString();
      const entry = acc.get(categoryId);
      if (entry) {
        entry.balance += record.value;
        entry.records.push(record);
      } else {
        acc.set(categoryId, {
          category: categoriesMap[categoryId],
          balance: record.value,
          records: [record],
        });
      }
      return acc;
    }, new Map<string, { category: Category; balance: number; records: Record[] }>());

    return Array.from(grouped.values())
      .sort((a, b) => b.balance - a.balance)
      .flatMap((cat) => {
        const categoryItem = {
          type: 'category' as const,
          category: cat.category,
          balance: cat.balance,
        };
        const recordItems = cat.records.map((record) => ({
          type: 'record' as const,
          record,
          category: cat.category,
        }));
        return [categoryItem, ...recordItems];
      });
  }, [records, categoriesMap, recordType]);

  const toggleCategory = (categoryId: string) => {
    setExpandedCategoryIds((prevSet) => {
      const newSet = new Set(prevSet);
      if (newSet.has(categoryId)) {
        newSet.delete(categoryId);
      } else {
        newSet.add(categoryId);
      }
      return newSet;
    });
  };

  return (
    <View style={{ flex: 1 }}>
      <LegendList
        data={listData}
        keyExtractor={(item) =>
          item.type === 'category'
            ? `cat-${item.category.id}`
            : `rec-${item.record.id}`
        }
        renderItem={({ item }) => {
          if (item.type === 'category') {
            const isExpanded = expandedCategoryIds.has(item.category.id);
            return (
              <Animated.View
                layout={LinearTransition.easing(
                  Easing.bezier(0.25, 0.1, 0.25, 1.0),
                ).duration(ANIMATION_DURATION)}
              >
                <TouchableRipple
                  className="overflow-hidden rounded-full p-4"
                  borderless
                  onPress={() => toggleCategory(item.category.id)}
                  accessibilityRole="button"
                  accessibilityState={{ expanded: isExpanded }}
                  accessibilityLabel={`${item.category.name}, total ${Intl.NumberFormat('en-GB', { style: 'currency', currency: 'EUR' }).format(item.balance)}. ${isExpanded ? 'Tap to collapse.' : 'Tap to expand.'}`}
                >
                  <HStack className="items-center gap-4">
                    <Box
                      className="h-full w-2"
                      style={{ backgroundColor: item.category.color }}
                    />
                    <Text className="flex-1 py-2 font-bold">
                      {item.category.name}
                    </Text>
                    <Text>
                      {Intl.NumberFormat('en-GB', {
                        style: 'currency',
                        currency: 'EUR',
                        currencyDisplay: 'symbol',
                      }).format(item.balance)}
                    </Text>
                    <AnimatedChevron isExpanded={isExpanded} />
                  </HStack>
                </TouchableRipple>
              </Animated.View>
            );
          } else {
            return (
              <Animated.View
                layout={LinearTransition.easing(
                  Easing.bezier(0.25, 0.1, 0.25, 1.0),
                ).duration(ANIMATION_DURATION)}
                entering={expand}
                exiting={collapse}
              >
                <HStack className="w-full py-4 pl-8" space="md">
                  <Text>
                    {Intl.NumberFormat('en-GB', {
                      style: 'currency',
                      currency: 'EUR',
                      currencyDisplay: 'symbol',
                    }).format(item.record.value)}
                  </Text>
                  <Text>{item.record.description}</Text>
                </HStack>
              </Animated.View>
            );
          }
        }}
        recycleItems
        contentContainerStyle={{ paddingBottom: 60 }}
      />
    </View>
  );
};

BalanceTransactionsByCategory.displayName = 'BalanceTransactionsByCategory';

Animations feel janky and I am looking to follow the Accordion official docs from React Native Reanimated: import React from 'react';
import { StyleSheet, View, Button, SafeAreaView } from 'react-native';
import Animated, {
  useAnimatedStyle,
  useDerivedValue,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated';

function AccordionItem({
  isExpanded,
  children,
  viewKey,
  style,
  duration = 500,
}) {
  const height = useSharedValue(0);

  const derivedHeight = useDerivedValue(() =>
    withTiming(height.value * Number(isExpanded.value), {
      duration,
    })
  );
  const bodyStyle = useAnimatedStyle(() => ({
    height: derivedHeight.value,
  }));

  return (
    <Animated.View
      key={`accordionItem_${viewKey}`}
      style={[styles.animatedView, bodyStyle, style]}>
      <View
        onLayout={(e) => {
          height.value = e.nativeEvent.layout.height;
        }}
        style={styles.wrapper}>
        {children}
      </View>
    </Animated.View>
  );
}

function Item() {
  return <View style={styles.box} />;
}

function Parent({ open }) {
  return (
    <View style={styles.parent}>
      <AccordionItem isExpanded={open} viewKey="Accordion">
        <Item />
      </AccordionItem>
    </View>
  );
}

export default function App() {
  const open = useSharedValue(false);
  const onPress = () => {
    open.value = !open.value;
  };

  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.buttonContainer}>
        <Button onPress={onPress} title="Click me" />
      </View>

      <View style={styles.content}>
        <Parent open={open} />
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    paddingTop: 24,
  },
  buttonContainer: {
    flex: 1,
    paddingBottom: '1rem',
    display: 'flex',
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
  },
  content: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  parent: {
    width: 200,
  },
  wrapper: {
    width: '100%',
    position: 'absolute',
    display: 'flex',
    alignItems: 'center',
  },
  animatedView: {
    width: '100%',
    overflow: 'hidden',
  },
  box: {
    height: 120,
    width: 120,
    color: '#f8f9ff',
    backgroundColor: '#b58df1',
    borderRadius: 20,
    alignItems: 'center',
    justifyContent: 'center',
  },
});

The problem is records can be thousands so I am trying to use a virtualize list like LegenState List to avoid lag issues and have high FPS, that's why my data is normalized and flattened because I cannot have a ScrollView inside another ScrollView. Is it possible follow a similar approach for animation as the example code snippet?