Files
bsky-social-app/src/lib/custom-animations/CountWheel.web.tsx
T
2026-04-07 09:48:11 -07:00

96 lines
2.4 KiB
TypeScript

import {useEffect, useRef, useState} from 'react'
import {View} from 'react-native'
import {useReducedMotion} from 'react-native-reanimated'
import {decideShouldRoll} from '#/lib/custom-animations/util'
const animationConfig = {
duration: 400,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
fill: 'forwards' as FillMode,
}
const enteringUpKeyframe = [
{opacity: 0, transform: 'translateY(18px)'},
{opacity: 1, transform: 'translateY(0)'},
]
const enteringDownKeyframe = [
{opacity: 0, transform: 'translateY(-18px)'},
{opacity: 1, transform: 'translateY(0)'},
]
const exitingUpKeyframe = [
{opacity: 1, transform: 'translateY(0)'},
{opacity: 0, transform: 'translateY(-18px)'},
]
const exitingDownKeyframe = [
{opacity: 1, transform: 'translateY(0)'},
{opacity: 0, transform: 'translateY(18px)'},
]
export function CountWheel({
count,
isToggled,
hasBeenToggled,
renderCount,
}: {
count: number
isToggled: boolean
hasBeenToggled: boolean
renderCount: (props: {count: number}) => React.ReactNode
}) {
const shouldAnimate = !useReducedMotion() && hasBeenToggled
const shouldRoll = decideShouldRoll(isToggled, count)
const countView = useRef<HTMLDivElement>(null)
const prevCountView = useRef<HTMLDivElement>(null)
const [prevCount, setPrevCount] = useState(count)
const prevIsToggled = useRef(isToggled)
useEffect(() => {
if (isToggled === prevIsToggled.current) {
return
}
const newPrevCount = isToggled ? count - 1 : count + 1
if (shouldAnimate && shouldRoll) {
countView.current?.animate?.(
isToggled ? enteringUpKeyframe : enteringDownKeyframe,
animationConfig,
)
prevCountView.current?.animate?.(
isToggled ? exitingUpKeyframe : exitingDownKeyframe,
animationConfig,
)
setPrevCount(newPrevCount)
}
prevIsToggled.current = isToggled
}, [isToggled, count, shouldAnimate, shouldRoll])
if (count < 1) {
return null
}
return (
<View>
<View
// @ts-expect-error is div
ref={countView}>
{renderCount({count})}
</View>
{shouldAnimate && (count > 1 || !isToggled) ? (
<View
style={{position: 'absolute', opacity: 0}}
aria-disabled={true}
// @ts-expect-error is div
ref={prevCountView}>
{renderCount({count: prevCount})}
</View>
) : null}
</View>
)
}