Two presentational components, both player-agnostic:
ChapterTicks — absolute overlay that draws tick marks on the progress
slider, one per chapter start (skipping the leading 0ms marker).
- Reads markers from a memoized prop computed by the caller so the
filter/sort runs at most once per chapters change, not per render.
- Snaps tick position AND width to the device pixel grid via
PixelRatio.roundToNearestPixel(). Without this, fractional dp values
land at different sub-pixel fractions on non-integer density displays
(420dpi -> 2.625x ratio) and Android anti-aliases each tick
differently, making some look visibly thicker than others.
- Tick colour defaults to rgba(0,0,0,0.55), contrasting against both
the filled progress (#fff) and the unfilled track so ticks stay
visible as playback advances.
- pointerEvents="none" so the slider underneath still receives touches.
- overflow: "visible" so taller ticks can bleed past the parent track.
ChapterList — bottom-sheet modal listing chapters with their timestamps.
- Highlights the currently active row (purple primary tint).
- Falls back to a localized "Chapter N" label when a chapter has no name.
- Imperatively scrolls to the active row each time the sheet becomes
visible. <Modal> keeps its children mounted across visible toggles,
so FlatList.initialScrollIndex (which only fires at first mount) would
only work on the very first open. Uses a ref + useEffect on `visible`
+ scrollToIndex inside requestAnimationFrame, with an
onScrollToIndexFailed fallback for indices outside the render window.
- All static styles in StyleSheet.create() — only dynamic backgroundColor
/ text colour stays inline. The list re-renders on every playback tick
so cutting the per-render style allocations is worth it.
- Colors from constants/Colors.ts (primary, background, text, icon),
no hardcoded hex.