@legendapp/list 2.0.0-next.1 → 2.0.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/.cursor/rules/changelog.mdc +60 -0
  3. package/.github/FUNDING.yml +15 -0
  4. package/.gitignore +5 -0
  5. package/.prettierrc.json +5 -0
  6. package/.vscode/settings.json +14 -0
  7. package/CLAUDE.md +126 -0
  8. package/biome.json +46 -0
  9. package/bun.lock +1289 -0
  10. package/bunfig.toml +2 -0
  11. package/dist/CHANGELOG.md +119 -0
  12. package/dist/LICENSE +21 -0
  13. package/dist/README.md +139 -0
  14. package/{animated.d.mts → dist/animated.d.mts} +1 -1
  15. package/{animated.d.ts → dist/animated.d.ts} +1 -1
  16. package/{index.d.mts → dist/index.d.mts} +16 -10
  17. package/{index.d.ts → dist/index.d.ts} +16 -10
  18. package/{index.js → dist/index.js} +52 -32
  19. package/{index.mjs → dist/index.mjs} +52 -32
  20. package/{keyboard-controller.d.mts → dist/keyboard-controller.d.mts} +4 -4
  21. package/{keyboard-controller.d.ts → dist/keyboard-controller.d.ts} +4 -4
  22. package/dist/package.json +35 -0
  23. package/example/README.md +40 -0
  24. package/example/api/data/genres.json +23 -0
  25. package/example/api/data/playlist/10402-10749.json +1 -0
  26. package/example/api/data/playlist/10402-10770.json +1 -0
  27. package/example/api/data/playlist/10402-37.json +1 -0
  28. package/example/api/data/playlist/10749-10752.json +1 -0
  29. package/example/api/data/playlist/10749-10770.json +1 -0
  30. package/example/api/data/playlist/10749-37.json +1 -0
  31. package/example/api/data/playlist/10749-878.json +1 -0
  32. package/example/api/data/playlist/10751-10402.json +1 -0
  33. package/example/api/data/playlist/10751-10752.json +1 -0
  34. package/example/api/data/playlist/10751-37.json +1 -0
  35. package/example/api/data/playlist/10751-53.json +1 -0
  36. package/example/api/data/playlist/10751-878.json +1 -0
  37. package/example/api/data/playlist/10751-9648.json +1 -0
  38. package/example/api/data/playlist/10752-37.json +1 -0
  39. package/example/api/data/playlist/12-10402.json +1 -0
  40. package/example/api/data/playlist/12-10749.json +1 -0
  41. package/example/api/data/playlist/12-18.json +1 -0
  42. package/example/api/data/playlist/12-27.json +1 -0
  43. package/example/api/data/playlist/12-35.json +1 -0
  44. package/example/api/data/playlist/14-36.json +1 -0
  45. package/example/api/data/playlist/14-878.json +1 -0
  46. package/example/api/data/playlist/16-10751.json +1 -0
  47. package/example/api/data/playlist/16-10770.json +1 -0
  48. package/example/api/data/playlist/16-35.json +1 -0
  49. package/example/api/data/playlist/16-36.json +1 -0
  50. package/example/api/data/playlist/16-53.json +1 -0
  51. package/example/api/data/playlist/18-10751.json +1 -0
  52. package/example/api/data/playlist/18-10752.json +1 -0
  53. package/example/api/data/playlist/18-37.json +1 -0
  54. package/example/api/data/playlist/18-53.json +1 -0
  55. package/example/api/data/playlist/18-878.json +1 -0
  56. package/example/api/data/playlist/27-10749.json +1 -0
  57. package/example/api/data/playlist/27-10770.json +1 -0
  58. package/example/api/data/playlist/28-10749.json +1 -0
  59. package/example/api/data/playlist/28-10751.json +1 -0
  60. package/example/api/data/playlist/28-10770.json +1 -0
  61. package/example/api/data/playlist/28-16.json +1 -0
  62. package/example/api/data/playlist/28-18.json +1 -0
  63. package/example/api/data/playlist/28-36.json +1 -0
  64. package/example/api/data/playlist/28-37.json +1 -0
  65. package/example/api/data/playlist/28-53.json +1 -0
  66. package/example/api/data/playlist/28-80.json +1 -0
  67. package/example/api/data/playlist/28-99.json +1 -0
  68. package/example/api/data/playlist/35-10749.json +1 -0
  69. package/example/api/data/playlist/35-10751.json +1 -0
  70. package/example/api/data/playlist/35-10752.json +1 -0
  71. package/example/api/data/playlist/35-27.json +1 -0
  72. package/example/api/data/playlist/35-36.json +1 -0
  73. package/example/api/data/playlist/35-53.json +1 -0
  74. package/example/api/data/playlist/35-80.json +1 -0
  75. package/example/api/data/playlist/36-37.json +1 -0
  76. package/example/api/data/playlist/36-878.json +1 -0
  77. package/example/api/data/playlist/36-9648.json +1 -0
  78. package/example/api/data/playlist/53-10752.json +1 -0
  79. package/example/api/data/playlist/80-10770.json +1 -0
  80. package/example/api/data/playlist/80-14.json +1 -0
  81. package/example/api/data/playlist/80-18.json +1 -0
  82. package/example/api/data/playlist/80-37.json +1 -0
  83. package/example/api/data/playlist/878-37.json +1 -0
  84. package/example/api/data/playlist/9648-10770.json +1 -0
  85. package/example/api/data/playlist/9648-37.json +1 -0
  86. package/example/api/data/playlist/9648-53.json +1 -0
  87. package/example/api/data/playlist/9648-878.json +1 -0
  88. package/example/api/data/playlist/99-10749.json +1 -0
  89. package/example/api/data/playlist/99-14.json +1 -0
  90. package/example/api/data/playlist/99-18.json +1 -0
  91. package/example/api/data/playlist/99-27.json +1 -0
  92. package/example/api/data/playlist/99-53.json +1 -0
  93. package/example/api/data/playlist/99-9648.json +1 -0
  94. package/example/api/data/playlist/index.ts +73 -0
  95. package/example/api/data/rows.json +1 -0
  96. package/example/api/index.ts +36 -0
  97. package/example/app/(tabs)/_layout.tsx +60 -0
  98. package/example/app/(tabs)/cards.tsx +81 -0
  99. package/example/app/(tabs)/index.tsx +205 -0
  100. package/example/app/(tabs)/moviesL.tsx +7 -0
  101. package/example/app/(tabs)/moviesLR.tsx +7 -0
  102. package/example/app/+not-found.tsx +32 -0
  103. package/example/app/_layout.tsx +34 -0
  104. package/example/app/accurate-scrollto/index.tsx +125 -0
  105. package/example/app/accurate-scrollto-2/index.tsx +52 -0
  106. package/example/app/accurate-scrollto-huge/index.tsx +128 -0
  107. package/example/app/add-to-end/index.tsx +82 -0
  108. package/example/app/ai-chat/index.tsx +236 -0
  109. package/example/app/bidirectional-infinite-list/index.tsx +133 -0
  110. package/example/app/cards-columns/index.tsx +37 -0
  111. package/example/app/cards-flashlist/index.tsx +122 -0
  112. package/example/app/cards-flatlist/index.tsx +94 -0
  113. package/example/app/cards-no-recycle/index.tsx +110 -0
  114. package/example/app/cards-renderItem.tsx +354 -0
  115. package/example/app/chat-example/index.tsx +167 -0
  116. package/example/app/chat-infinite/index.tsx +239 -0
  117. package/example/app/chat-keyboard/index.tsx +248 -0
  118. package/example/app/chat-resize-outer/index.tsx +247 -0
  119. package/example/app/columns/index.tsx +78 -0
  120. package/example/app/countries/index.tsx +182 -0
  121. package/example/app/countries-flashlist/index.tsx +163 -0
  122. package/example/app/countries-reorder/index.tsx +187 -0
  123. package/example/app/extra-data/index.tsx +86 -0
  124. package/example/app/filter-elements/filter-data-provider.tsx +55 -0
  125. package/example/app/filter-elements/index.tsx +118 -0
  126. package/example/app/initial-scroll-index/index.tsx +106 -0
  127. package/example/app/initial-scroll-index/renderFixedItem.tsx +215 -0
  128. package/example/app/initial-scroll-index-free-height/index.tsx +70 -0
  129. package/example/app/initial-scroll-index-keyed/index.tsx +62 -0
  130. package/example/app/lazy-list/index.tsx +123 -0
  131. package/example/app/movies-flashlist/index.tsx +7 -0
  132. package/example/app/mutable-cells/index.tsx +104 -0
  133. package/example/app/video-feed/index.tsx +119 -0
  134. package/example/app.config.js +22 -0
  135. package/example/app.json +45 -0
  136. package/example/assets/fonts/SpaceMono-Regular.ttf +0 -0
  137. package/example/assets/images/adaptive-icon.png +0 -0
  138. package/example/assets/images/favicon.png +0 -0
  139. package/example/assets/images/icon.png +0 -0
  140. package/example/assets/images/partial-react-logo.png +0 -0
  141. package/example/assets/images/react-logo.png +0 -0
  142. package/example/assets/images/react-logo@2x.png +0 -0
  143. package/example/assets/images/react-logo@3x.png +0 -0
  144. package/example/assets/images/splash-icon.png +0 -0
  145. package/example/autoscroll.sh +101 -0
  146. package/example/bun.lock +2266 -0
  147. package/example/bunfig.toml +2 -0
  148. package/example/components/Breathe.tsx +54 -0
  149. package/example/components/Circle.tsx +69 -0
  150. package/example/components/Collapsible.tsx +44 -0
  151. package/example/components/ExternalLink.tsx +24 -0
  152. package/example/components/HapticTab.tsx +18 -0
  153. package/example/components/HelloWave.tsx +37 -0
  154. package/example/components/Movies.tsx +179 -0
  155. package/example/components/ParallaxScrollView.tsx +81 -0
  156. package/example/components/ThemedText.tsx +60 -0
  157. package/example/components/ThemedView.tsx +14 -0
  158. package/example/components/__tests__/ThemedText-test.tsx +10 -0
  159. package/example/components/__tests__/__snapshots__/ThemedText-test.tsx.snap +24 -0
  160. package/example/components/ui/IconSymbol.ios.tsx +32 -0
  161. package/example/components/ui/IconSymbol.tsx +43 -0
  162. package/example/components/ui/TabBarBackground.ios.tsx +22 -0
  163. package/example/components/ui/TabBarBackground.tsx +6 -0
  164. package/example/constants/Colors.ts +26 -0
  165. package/example/constants/constants.ts +5 -0
  166. package/example/constants/useScrollTest.ts +19 -0
  167. package/example/hooks/useColorScheme.ts +1 -0
  168. package/example/hooks/useColorScheme.web.ts +8 -0
  169. package/example/hooks/useThemeColor.ts +22 -0
  170. package/example/ios/.xcode.env +11 -0
  171. package/example/ios/Podfile +64 -0
  172. package/example/ios/Podfile.lock +2767 -0
  173. package/example/ios/Podfile.properties.json +5 -0
  174. package/example/ios/listtest/AppDelegate.swift +70 -0
  175. package/example/ios/listtest/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png +0 -0
  176. package/example/ios/listtest/Images.xcassets/AppIcon.appiconset/Contents.json +14 -0
  177. package/example/ios/listtest/Images.xcassets/Contents.json +6 -0
  178. package/example/ios/listtest/Images.xcassets/SplashScreenBackground.colorset/Contents.json +20 -0
  179. package/example/ios/listtest/Images.xcassets/SplashScreenLogo.imageset/Contents.json +23 -0
  180. package/example/ios/listtest/Images.xcassets/SplashScreenLogo.imageset/image.png +0 -0
  181. package/example/ios/listtest/Images.xcassets/SplashScreenLogo.imageset/image@2x.png +0 -0
  182. package/example/ios/listtest/Images.xcassets/SplashScreenLogo.imageset/image@3x.png +0 -0
  183. package/example/ios/listtest/Info.plist +85 -0
  184. package/example/ios/listtest/PrivacyInfo.xcprivacy +48 -0
  185. package/example/ios/listtest/SplashScreen.storyboard +42 -0
  186. package/example/ios/listtest/Supporting/Expo.plist +12 -0
  187. package/example/ios/listtest/listtest-Bridging-Header.h +3 -0
  188. package/example/ios/listtest/listtest.entitlements +5 -0
  189. package/example/ios/listtest.xcodeproj/project.pbxproj +547 -0
  190. package/example/ios/listtest.xcodeproj/xcshareddata/xcschemes/listtest.xcscheme +88 -0
  191. package/example/ios/listtest.xcworkspace/contents.xcworkspacedata +10 -0
  192. package/example/metro.config.js +16 -0
  193. package/example/package.json +73 -0
  194. package/example/scripts/reset-project.js +84 -0
  195. package/example/tsconfig.json +26 -0
  196. package/package.json +88 -34
  197. package/posttsup.ts +24 -0
  198. package/src/Container.tsx +176 -0
  199. package/src/Containers.tsx +85 -0
  200. package/src/ContextContainer.ts +145 -0
  201. package/src/DebugView.tsx +83 -0
  202. package/src/LazyLegendList.tsx +41 -0
  203. package/src/LeanView.tsx +18 -0
  204. package/src/LegendList.tsx +558 -0
  205. package/src/ListComponent.tsx +191 -0
  206. package/src/ScrollAdjust.tsx +24 -0
  207. package/src/ScrollAdjustHandler.ts +26 -0
  208. package/src/Separator.tsx +14 -0
  209. package/src/animated.tsx +6 -0
  210. package/src/calculateItemsInView.ts +363 -0
  211. package/src/calculateOffsetForIndex.ts +23 -0
  212. package/src/calculateOffsetWithOffsetPosition.ts +26 -0
  213. package/src/checkAllSizesKnown.ts +17 -0
  214. package/src/checkAtBottom.ts +36 -0
  215. package/src/checkAtTop.ts +27 -0
  216. package/src/checkThreshold.ts +30 -0
  217. package/src/constants.ts +11 -0
  218. package/src/createColumnWrapperStyle.ts +16 -0
  219. package/src/doInitialAllocateContainers.ts +40 -0
  220. package/src/doMaintainScrollAtEnd.ts +34 -0
  221. package/src/findAvailableContainers.ts +98 -0
  222. package/src/finishScrollTo.ts +8 -0
  223. package/src/getId.ts +21 -0
  224. package/src/getItemSize.ts +52 -0
  225. package/src/getRenderedItem.ts +34 -0
  226. package/src/getScrollVelocity.ts +47 -0
  227. package/src/handleLayout.ts +70 -0
  228. package/src/helpers.ts +39 -0
  229. package/src/index.ts +11 -0
  230. package/src/keyboard-controller.tsx +63 -0
  231. package/src/onScroll.ts +66 -0
  232. package/src/prepareMVCP.ts +50 -0
  233. package/src/reanimated.tsx +63 -0
  234. package/src/requestAdjust.ts +41 -0
  235. package/src/scrollTo.ts +40 -0
  236. package/src/scrollToIndex.ts +34 -0
  237. package/src/setDidLayout.ts +25 -0
  238. package/src/setPaddingTop.ts +28 -0
  239. package/src/state.tsx +304 -0
  240. package/src/types.ts +610 -0
  241. package/src/updateAlignItemsPaddingTop.ts +18 -0
  242. package/src/updateAllPositions.ts +130 -0
  243. package/src/updateItemSize.ts +203 -0
  244. package/src/updateTotalSize.ts +44 -0
  245. package/src/useAnimatedValue.ts +6 -0
  246. package/src/useCombinedRef.ts +22 -0
  247. package/src/useInit.ts +17 -0
  248. package/src/useSyncLayout.tsx +68 -0
  249. package/src/useValue$.ts +53 -0
  250. package/src/viewability.ts +279 -0
  251. package/tsconfig.json +59 -0
  252. package/tsup.config.ts +21 -0
  253. /package/{animated.js → dist/animated.js} +0 -0
  254. /package/{animated.mjs → dist/animated.mjs} +0 -0
  255. /package/{keyboard-controller.js → dist/keyboard-controller.js} +0 -0
  256. /package/{keyboard-controller.mjs → dist/keyboard-controller.mjs} +0 -0
  257. /package/{reanimated.d.mts → dist/reanimated.d.mts} +0 -0
  258. /package/{reanimated.d.ts → dist/reanimated.d.ts} +0 -0
  259. /package/{reanimated.js → dist/reanimated.js} +0 -0
  260. /package/{reanimated.mjs → dist/reanimated.mjs} +0 -0
@@ -0,0 +1,2 @@
1
+ [install]
2
+ saveTextLockfile = true
@@ -0,0 +1,54 @@
1
+ import React, { useEffect } from "react";
2
+ import { View, StyleSheet } from "react-native";
3
+ import Animated, {
4
+ Easing,
5
+ useAnimatedStyle,
6
+ useSharedValue,
7
+ withRepeat,
8
+ withTiming,
9
+ } from "react-native-reanimated";
10
+ import { mix } from "react-native-redash";
11
+
12
+ import Circle from "./Circle";
13
+
14
+ const styles = StyleSheet.create({
15
+ container: {
16
+ flex: 1,
17
+ backgroundColor: "black",
18
+ },
19
+ });
20
+ const Breathe = () => {
21
+ const progress = useSharedValue(0);
22
+ const goesDown = useSharedValue(false);
23
+ useEffect(() => {
24
+ progress.value = withRepeat(
25
+ withTiming(
26
+ 1,
27
+ { duration: 3000, easing: Easing.bezier(0.5, 0, 0.5, 1) },
28
+ () => (goesDown.value = !goesDown.value)
29
+ ),
30
+ -1,
31
+ true
32
+ );
33
+ }, [goesDown, progress]);
34
+ const style = useAnimatedStyle(() => ({
35
+ flex: 1,
36
+ transform: [{ rotate: `${mix(progress.value, -Math.PI, 0)}rad` }],
37
+ }));
38
+ return (
39
+ <View style={styles.container}>
40
+ <Animated.View style={style}>
41
+ {new Array(6).fill(0).map((_, index) => (
42
+ <Circle
43
+ progress={progress}
44
+ index={index}
45
+ key={index}
46
+ goesDown={goesDown}
47
+ />
48
+ ))}
49
+ </Animated.View>
50
+ </View>
51
+ );
52
+ };
53
+
54
+ export default Breathe;
@@ -0,0 +1,69 @@
1
+ import React from "react";
2
+ import { Dimensions, StyleSheet } from "react-native";
3
+ import Animated, {
4
+ Extrapolate,
5
+ interpolate,
6
+ useAnimatedStyle,
7
+ } from "react-native-reanimated";
8
+ import { clamp, mix, polar2Canvas } from "react-native-redash";
9
+ import { LinearGradient } from "expo-linear-gradient";
10
+
11
+ const { width } = Dimensions.get("window");
12
+ const SIZE = width / 2;
13
+ const styles = StyleSheet.create({
14
+ container: {
15
+ ...StyleSheet.absoluteFillObject,
16
+ justifyContent: "center",
17
+ alignItems: "center",
18
+ },
19
+ circle: {
20
+ width: SIZE,
21
+ height: SIZE,
22
+ borderRadius: SIZE / 2,
23
+ opacity: 0.5,
24
+ },
25
+ });
26
+
27
+ const transform = (theta: number, value: number) => {
28
+ "worklet";
29
+ const { x, y } = polar2Canvas({ theta, radius: SIZE / 2 }, { x: 0, y: 0 });
30
+ const translateX = mix(value, 0, x);
31
+ const translateY = mix(value, 0, y);
32
+ return [{ translateX }, { translateY }, { scale: mix(value, 0.3, 1) }];
33
+ };
34
+
35
+ interface CircleProps {
36
+ progress: Animated.SharedValue<number>;
37
+ goesDown: Animated.SharedValue<boolean>;
38
+ index: number;
39
+ }
40
+
41
+ const Circle = ({ progress, goesDown, index }: CircleProps) => {
42
+ const theta = (index * (2 * Math.PI)) / 6;
43
+ const style = useAnimatedStyle(() => {
44
+ return {
45
+ transform: transform(theta, progress.value),
46
+ };
47
+ });
48
+ const style1 = useAnimatedStyle(() => {
49
+ const value = goesDown.value
50
+ ? clamp(progress.value + 0.1, 0, 1)
51
+ : progress.value;
52
+ return {
53
+ opacity: interpolate(value, [0.6, 1], [0, 1], Extrapolate.CLAMP),
54
+ transform: transform(theta, value),
55
+ };
56
+ });
57
+ return (
58
+ <>
59
+ <Animated.View style={[styles.container, style1]}>
60
+ <LinearGradient colors={["#62BFA1", "#529CA0"]} style={styles.circle} />
61
+ </Animated.View>
62
+ <Animated.View style={[styles.container, style]}>
63
+ <LinearGradient colors={["#62BFA1", "#529CA0"]} style={styles.circle} />
64
+ </Animated.View>
65
+ </>
66
+ );
67
+ };
68
+
69
+ export default Circle;
@@ -0,0 +1,44 @@
1
+ import { PropsWithChildren, useState } from 'react';
2
+ import { StyleSheet, TouchableOpacity, useColorScheme } from 'react-native';
3
+
4
+ import { ThemedText } from '@/components/ThemedText';
5
+ import { ThemedView } from '@/components/ThemedView';
6
+ import { IconSymbol } from '@/components/ui/IconSymbol';
7
+ import { Colors } from '@/constants/Colors';
8
+
9
+ export function Collapsible({ children, title }: PropsWithChildren & { title: string }) {
10
+ const [isOpen, setIsOpen] = useState(false);
11
+ const theme = useColorScheme() ?? 'light';
12
+
13
+ return (
14
+ <ThemedView>
15
+ <TouchableOpacity
16
+ style={styles.heading}
17
+ onPress={() => setIsOpen((value) => !value)}
18
+ activeOpacity={0.8}>
19
+ <IconSymbol
20
+ name="chevron.right"
21
+ size={18}
22
+ weight="medium"
23
+ color={theme === 'light' ? Colors.light.icon : Colors.dark.icon}
24
+ style={{ transform: [{ rotate: isOpen ? '90deg' : '0deg' }] }}
25
+ />
26
+
27
+ <ThemedText type="defaultSemiBold">{title}</ThemedText>
28
+ </TouchableOpacity>
29
+ {isOpen && <ThemedView style={styles.content}>{children}</ThemedView>}
30
+ </ThemedView>
31
+ );
32
+ }
33
+
34
+ const styles = StyleSheet.create({
35
+ heading: {
36
+ flexDirection: 'row',
37
+ alignItems: 'center',
38
+ gap: 6,
39
+ },
40
+ content: {
41
+ marginTop: 6,
42
+ marginLeft: 24,
43
+ },
44
+ });
@@ -0,0 +1,24 @@
1
+ import { Link } from 'expo-router';
2
+ import { openBrowserAsync } from 'expo-web-browser';
3
+ import { type ComponentProps } from 'react';
4
+ import { Platform } from 'react-native';
5
+
6
+ type Props = Omit<ComponentProps<typeof Link>, 'href'> & { href: string };
7
+
8
+ export function ExternalLink({ href, ...rest }: Props) {
9
+ return (
10
+ <Link
11
+ target="_blank"
12
+ {...rest}
13
+ href={href}
14
+ onPress={async (event) => {
15
+ if (Platform.OS !== 'web') {
16
+ // Prevent the default behavior of linking to the default browser on native.
17
+ event.preventDefault();
18
+ // Open the link in an in-app browser.
19
+ await openBrowserAsync(href);
20
+ }
21
+ }}
22
+ />
23
+ );
24
+ }
@@ -0,0 +1,18 @@
1
+ import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs';
2
+ import { PlatformPressable } from '@react-navigation/elements';
3
+ import * as Haptics from 'expo-haptics';
4
+
5
+ export function HapticTab(props: BottomTabBarButtonProps) {
6
+ return (
7
+ <PlatformPressable
8
+ {...props}
9
+ onPressIn={(ev) => {
10
+ if (process.env.EXPO_OS === 'ios') {
11
+ // Add a soft haptic feedback when pressing down on the tabs.
12
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
13
+ }
14
+ props.onPressIn?.(ev);
15
+ }}
16
+ />
17
+ );
18
+ }
@@ -0,0 +1,37 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import Animated, {
3
+ useSharedValue,
4
+ useAnimatedStyle,
5
+ withTiming,
6
+ withRepeat,
7
+ withSequence,
8
+ } from 'react-native-reanimated';
9
+
10
+ import { ThemedText } from '@/components/ThemedText';
11
+
12
+ export function HelloWave() {
13
+ const rotationAnimation = useSharedValue(0);
14
+
15
+ rotationAnimation.value = withRepeat(
16
+ withSequence(withTiming(25, { duration: 150 }), withTiming(0, { duration: 150 })),
17
+ 4 // Run the animation 4 times
18
+ );
19
+
20
+ const animatedStyle = useAnimatedStyle(() => ({
21
+ transform: [{ rotate: `${rotationAnimation.value}deg` }],
22
+ }));
23
+
24
+ return (
25
+ <Animated.View style={animatedStyle}>
26
+ <ThemedText style={styles.text}>👋</ThemedText>
27
+ </Animated.View>
28
+ );
29
+ }
30
+
31
+ const styles = StyleSheet.create({
32
+ text: {
33
+ fontSize: 28,
34
+ lineHeight: 32,
35
+ marginTop: -6,
36
+ },
37
+ });
@@ -0,0 +1,179 @@
1
+ // Forked from https://github.com/Almouro/rn-list-comparison-movies
2
+ // Full credit to Alex Moreaux (@Almouro) for the original code
3
+
4
+ import { LegendList, type LegendListRenderItemProps } from "@legendapp/list";
5
+ import { FlashList } from "@shopify/flash-list";
6
+ import * as React from "react";
7
+ import { Dimensions, Image, StyleSheet, Text, View } from "react-native";
8
+ import { IMAGE_SIZE, type Movie, type Playlist, getImageUrl } from "../api";
9
+ import { playlists as playlistData } from "../api/data/playlist";
10
+
11
+ const itemCount = 0;
12
+
13
+ const cardStyles = StyleSheet.create({
14
+ image: {
15
+ width: IMAGE_SIZE.width,
16
+ height: IMAGE_SIZE.height,
17
+ borderRadius: 5,
18
+ },
19
+ background: {
20
+ ...StyleSheet.absoluteFillObject,
21
+ backgroundColor: "#272829",
22
+ },
23
+ });
24
+
25
+ const MoviePortrait = ({ movie }: { movie: Movie }) => {
26
+ return (
27
+ <View style={cardStyles.image}>
28
+ <View style={cardStyles.background} />
29
+ <Image
30
+ key={movie.id}
31
+ source={{ uri: getImageUrl(movie.poster_path) }}
32
+ style={cardStyles.image}
33
+ fadeDuration={0}
34
+ />
35
+ </View>
36
+ );
37
+ };
38
+
39
+ const MarginBetweenItems = () => <View style={{ width: margins.s }} />;
40
+
41
+ const margins = {
42
+ s: 5,
43
+ m: 10,
44
+ l: 20,
45
+ };
46
+
47
+ const rowStyles = StyleSheet.create({
48
+ title: {
49
+ fontSize: 20,
50
+ fontWeight: "bold",
51
+ color: "white",
52
+ marginHorizontal: margins.m,
53
+ marginBottom: margins.s,
54
+ },
55
+ container: {
56
+ minHeight: cardStyles.image.height,
57
+ marginBottom: margins.l,
58
+ width: Dimensions.get("window").width,
59
+ },
60
+ listContainer: {
61
+ paddingHorizontal: margins.m,
62
+ },
63
+ });
64
+
65
+ const rowCount = 0;
66
+
67
+ const MovieRow = ({
68
+ playlist,
69
+ ListComponent,
70
+ isLegend,
71
+ }: {
72
+ playlist: Playlist;
73
+ ListComponent: typeof FlashList | typeof LegendList;
74
+ isLegend: boolean;
75
+ }) => {
76
+ const movies = playlistData[playlist.id]();
77
+ const DRAW_DISTANCE_ROW = 500;
78
+ // let opacity = 0;
79
+ // if (isLegend) {
80
+ // const [_opacity, setOpacity] = useRecyclingState<number>(() => {
81
+ // if (setOpacity) {
82
+ // requestAnimationFrame(() => setOpacity(1));
83
+ // return 0;
84
+ // }
85
+ // return 1;
86
+ // });
87
+ // opacity = _opacity;
88
+ // } else {
89
+ // opacity = 1;
90
+ // }
91
+
92
+ // const listRef = useRef<FlashList<Movie>>(null);
93
+
94
+ // const {onMomentumScrollBegin, onScroll} = useRememberListScroll(
95
+ // listRef,
96
+ // playlist.id,
97
+ // );
98
+
99
+ // useEffect(() => {
100
+ // rowCount++;
101
+ // console.log("rowCount", rowCount);
102
+ // }, []);
103
+
104
+ // const fadeAnim = useRef(new Animated.Value(0)).current;
105
+ // // useEffect(() => {
106
+ // // itemCount++;
107
+ // // console.log("itemCount", itemCount);
108
+ // // }, []);
109
+
110
+ // useRecyclingEffect(() => {
111
+ // console.log("useRecyclingEffect");
112
+ // fadeAnim.setValue(0);
113
+ // Animated.timing(fadeAnim, {
114
+ // toValue: 1,
115
+ // duration: 2000,
116
+ // useNativeDriver: true,
117
+ // }).start();
118
+ // });
119
+
120
+ return (
121
+ <React.Fragment>
122
+ <Text numberOfLines={1} style={rowStyles.title}>
123
+ {playlist.title}
124
+ </Text>
125
+ <View style={[rowStyles.container]}>
126
+ <ListComponent
127
+ contentContainerStyle={rowStyles.listContainer}
128
+ // See https://shopify.github.io/flash-list/docs/fundamentals/performant-components/#remove-key-prop
129
+ // keyExtractor={(movie: Movie, index: number) => (isLegend ? movie.id.toString() : index.toString())}
130
+ // keyExtractor={(movie: Movie, index: number) => index.toString()}
131
+ ItemSeparatorComponent={MarginBetweenItems}
132
+ horizontal
133
+ estimatedItemSize={cardStyles.image.width + 5}
134
+ data={movies}
135
+ // recycleItems
136
+ renderItem={({ item }: { item: Movie }) => <MoviePortrait movie={item} />}
137
+ // ref={listRef}
138
+ // onMomentumScrollBegin={onMomentumScrollBegin}
139
+ // onScroll={onScroll}
140
+ drawDistance={DRAW_DISTANCE_ROW}
141
+ />
142
+ </View>
143
+ </React.Fragment>
144
+ );
145
+ };
146
+
147
+ const listStyles = StyleSheet.create({
148
+ container: {
149
+ backgroundColor: "black",
150
+ paddingVertical: margins.m,
151
+ },
152
+ });
153
+
154
+ const Movies = ({ isLegend, recycleItems }: { isLegend: boolean; recycleItems?: boolean }) => {
155
+ const playlists = require("../api/data/rows.json");
156
+
157
+ const ListComponent: typeof LegendList = isLegend ? LegendList : (FlashList as any);
158
+
159
+ // Flashlist appears to internally multiple the draw distance by 2-3 so increase the draw distance
160
+ // for the Legend version to get the same effect
161
+ const DRAW_DISTANCE = 500;
162
+ console.log("is legend", isLegend, DRAW_DISTANCE);
163
+
164
+ return (
165
+ <ListComponent
166
+ data={playlists}
167
+ keyExtractor={(playlist: Playlist) => playlist.id}
168
+ estimatedItemSize={cardStyles.image.height + 52}
169
+ renderItem={({ item: playlist }: LegendListRenderItemProps<Playlist>) => (
170
+ <MovieRow ListComponent={ListComponent} isLegend={isLegend} playlist={playlist} />
171
+ )}
172
+ contentContainerStyle={listStyles.container}
173
+ drawDistance={DRAW_DISTANCE}
174
+ recycleItems={recycleItems}
175
+ />
176
+ );
177
+ };
178
+
179
+ export default Movies;
@@ -0,0 +1,81 @@
1
+ import type { PropsWithChildren, ReactElement } from 'react';
2
+ import { StyleSheet, useColorScheme } from 'react-native';
3
+ import Animated, {
4
+ interpolate,
5
+ useAnimatedRef,
6
+ useAnimatedStyle,
7
+ useScrollViewOffset,
8
+ } from 'react-native-reanimated';
9
+
10
+ import { ThemedView } from '@/components/ThemedView';
11
+ import { useBottomTabOverflow } from '@/components/ui/TabBarBackground';
12
+
13
+ const HEADER_HEIGHT = 250;
14
+
15
+ type Props = PropsWithChildren<{
16
+ headerImage: ReactElement;
17
+ headerBackgroundColor: { dark: string; light: string };
18
+ }>;
19
+
20
+ export default function ParallaxScrollView({
21
+ children,
22
+ headerImage,
23
+ headerBackgroundColor,
24
+ }: Props) {
25
+ const colorScheme = useColorScheme() ?? 'light';
26
+ const scrollRef = useAnimatedRef<Animated.ScrollView>();
27
+ const scrollOffset = useScrollViewOffset(scrollRef);
28
+ const bottom = useBottomTabOverflow();
29
+ const headerAnimatedStyle = useAnimatedStyle(() => {
30
+ return {
31
+ transform: [
32
+ {
33
+ translateY: interpolate(
34
+ scrollOffset.value,
35
+ [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
36
+ [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75]
37
+ ),
38
+ },
39
+ {
40
+ scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]),
41
+ },
42
+ ],
43
+ };
44
+ });
45
+
46
+ return (
47
+ <ThemedView style={styles.container}>
48
+ <Animated.ScrollView
49
+ ref={scrollRef}
50
+ scrollEventThrottle={16}
51
+ scrollIndicatorInsets={{ bottom }}
52
+ contentContainerStyle={{ paddingBottom: bottom }}>
53
+ <Animated.View
54
+ style={[
55
+ styles.header,
56
+ { backgroundColor: headerBackgroundColor[colorScheme] },
57
+ headerAnimatedStyle,
58
+ ]}>
59
+ {headerImage}
60
+ </Animated.View>
61
+ <ThemedView style={styles.content}>{children}</ThemedView>
62
+ </Animated.ScrollView>
63
+ </ThemedView>
64
+ );
65
+ }
66
+
67
+ const styles = StyleSheet.create({
68
+ container: {
69
+ flex: 1,
70
+ },
71
+ header: {
72
+ height: HEADER_HEIGHT,
73
+ overflow: 'hidden',
74
+ },
75
+ content: {
76
+ flex: 1,
77
+ padding: 32,
78
+ gap: 16,
79
+ overflow: 'hidden',
80
+ },
81
+ });
@@ -0,0 +1,60 @@
1
+ import { Text, type TextProps, StyleSheet } from 'react-native';
2
+
3
+ import { useThemeColor } from '@/hooks/useThemeColor';
4
+
5
+ export type ThemedTextProps = TextProps & {
6
+ lightColor?: string;
7
+ darkColor?: string;
8
+ type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
9
+ };
10
+
11
+ export function ThemedText({
12
+ style,
13
+ lightColor,
14
+ darkColor,
15
+ type = 'default',
16
+ ...rest
17
+ }: ThemedTextProps) {
18
+ const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
19
+
20
+ return (
21
+ <Text
22
+ style={[
23
+ { color },
24
+ type === 'default' ? styles.default : undefined,
25
+ type === 'title' ? styles.title : undefined,
26
+ type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined,
27
+ type === 'subtitle' ? styles.subtitle : undefined,
28
+ type === 'link' ? styles.link : undefined,
29
+ style,
30
+ ]}
31
+ {...rest}
32
+ />
33
+ );
34
+ }
35
+
36
+ const styles = StyleSheet.create({
37
+ default: {
38
+ fontSize: 16,
39
+ lineHeight: 24,
40
+ },
41
+ defaultSemiBold: {
42
+ fontSize: 16,
43
+ lineHeight: 24,
44
+ fontWeight: '600',
45
+ },
46
+ title: {
47
+ fontSize: 32,
48
+ fontWeight: 'bold',
49
+ lineHeight: 32,
50
+ },
51
+ subtitle: {
52
+ fontSize: 20,
53
+ fontWeight: 'bold',
54
+ },
55
+ link: {
56
+ lineHeight: 30,
57
+ fontSize: 16,
58
+ color: '#0a7ea4',
59
+ },
60
+ });
@@ -0,0 +1,14 @@
1
+ import { View, type ViewProps } from 'react-native';
2
+
3
+ import { useThemeColor } from '@/hooks/useThemeColor';
4
+
5
+ export type ThemedViewProps = ViewProps & {
6
+ lightColor?: string;
7
+ darkColor?: string;
8
+ };
9
+
10
+ export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) {
11
+ const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
12
+
13
+ return <View style={[{ backgroundColor }, style]} {...otherProps} />;
14
+ }
@@ -0,0 +1,10 @@
1
+ import * as React from 'react';
2
+ import renderer from 'react-test-renderer';
3
+
4
+ import { ThemedText } from '../ThemedText';
5
+
6
+ it(`renders correctly`, () => {
7
+ const tree = renderer.create(<ThemedText>Snapshot test!</ThemedText>).toJSON();
8
+
9
+ expect(tree).toMatchSnapshot();
10
+ });
@@ -0,0 +1,24 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`renders correctly 1`] = `
4
+ <Text
5
+ style={
6
+ [
7
+ {
8
+ "color": "#11181C",
9
+ },
10
+ {
11
+ "fontSize": 16,
12
+ "lineHeight": 24,
13
+ },
14
+ undefined,
15
+ undefined,
16
+ undefined,
17
+ undefined,
18
+ undefined,
19
+ ]
20
+ }
21
+ >
22
+ Snapshot test!
23
+ </Text>
24
+ `;
@@ -0,0 +1,32 @@
1
+ import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols';
2
+ import { StyleProp, ViewStyle } from 'react-native';
3
+
4
+ export function IconSymbol({
5
+ name,
6
+ size = 24,
7
+ color,
8
+ style,
9
+ weight = 'regular',
10
+ }: {
11
+ name: SymbolViewProps['name'];
12
+ size?: number;
13
+ color: string;
14
+ style?: StyleProp<ViewStyle>;
15
+ weight?: SymbolWeight;
16
+ }) {
17
+ return (
18
+ <SymbolView
19
+ weight={weight}
20
+ tintColor={color}
21
+ resizeMode="scaleAspectFit"
22
+ name={name}
23
+ style={[
24
+ {
25
+ width: size,
26
+ height: size,
27
+ },
28
+ style,
29
+ ]}
30
+ />
31
+ );
32
+ }