message: pull horizontal rail out into separate thing
diff: @@ -13,36 +13,23 @@ interface Tab {
  renderContent(): React.ReactNode
  }
  -export const StickyHeaderScrollView: React.FC<{
  - tabs: Tab[]
  - headerContent: JSX.Element
  -}> = ({ tabs, headerContent }) => {
  +const SnappyHorizontalRail: React.FC<{ offset: number }> = ({ offset, children }) => {
  + const currentOffset = useAnimatedValue(-offset)
  const { width } = useScreenDimensions()
  - const [activeTabIndex, setActiveTabIndex] = useState(Math.max(tabs.findIndex(tab => tab.initial), 0))
  - const activeTabIndexNative = useAnimatedValue(activeTabIndex)
  - // update the native tab index using a spring animation to drive the pseudo horizontal flatlist
  useEffect(
  () => {
  - Animated.spring(activeTabIndexNative, {
  + Animated.spring(currentOffset, {
  ...Animated.SpringUtils.makeDefaultConfig(),
  stiffness: 600,
  damping: 120,
  - toValue: activeTabIndex,
  + toValue: -offset,
  }).start()
  },
  - [activeTabIndex]
  + [offset]
  )
  - const [headerHeight, setHeaderHeight] = useState<null | number>(null)
  -
  - const headerOffsetY = useAnimatedValue(0)
  -
  return (
  - <Animated.View style={{ flex: 1, position: "relative", overflow: "hidden" }}>
  - {/* put tab content first because we want the header to be absolutely positioned _above_ it */}
  - {headerHeight !== null && (
  - // This mimicks a horizontal flat list
  <Animated.View
  style={{
  flex: 1,
  @@ -50,14 +37,31 @@ export const StickyHeaderScrollView: React.FC<{
  flexDirection: "row",
  transform: [
  {
  - translateX: Animated.interpolate(activeTabIndexNative, {
  - inputRange: [0, 1],
  - outputRange: [0, -width],
  - }) as any,
  + translateX: currentOffset as any,
  },
  ],
  }}
  >
  + {children}
  + </Animated.View>
  + )
  +}
  +
  +export const StickyHeaderScrollView: React.FC<{
  + tabs: Tab[]
  + headerContent: JSX.Element
  +}> = ({ tabs, headerContent }) => {
  + const { width } = useScreenDimensions()
  + const [activeTabIndex, setActiveTabIndex] = useState(Math.max(tabs.findIndex(tab => tab.initial), 0))
  + const [headerHeight, setHeaderHeight] = useState<null | number>(null)
  +
  + const headerOffsetY = useAnimatedValue(0)
  +
  + return (
  + <Animated.View style={{ flex: 1, position: "relative", overflow: "hidden" }}>
  + {/* put tab content first because we want the header to be absolutely positioned _above_ it */}
  + {headerHeight !== null && (
  + <SnappyHorizontalRail offset={activeTabIndex * width}>
  {tabs.map(({ renderContent }, index) => {
  return (
  <View style={{ flex: 1, width }} key={index}>
  @@ -70,7 +74,7 @@ export const StickyHeaderScrollView: React.FC<{
  </View>
  )
  })}
  - </Animated.View>
  + </SnappyHorizontalRail>
  )}
  <Animated.View
  style={{
  @@ -81,13 +85,10 @@ export const StickyHeaderScrollView: React.FC<{
  transform: [{ translateY: headerOffsetY as any }],
  }}
  >
  - {/* header */}
  <View onLayout={e => setHeaderHeight(e.nativeEvent.layout.height)}>
  {headerContent}
  <Spacer mb={1} />
  </View>
  - {/* sticky content */}
  - <View>
  <TabBar>
  {tabs.map(({ title }, index) => (
  <Tab
  @@ -100,7 +101,6 @@ export const StickyHeaderScrollView: React.FC<{
  />
  ))}
  </TabBar>
  - </View>
  </Animated.View>
  </Animated.View>
  )
  