## Why
`welcome.tsx` was the single biggest source of duplication in the repo:
**8 clone pairs, ~200 duplicated lines**, all of them the same thing
copied nine times.
Every scroll-animated preview on the landing page carried its own copy
of:
```tsx
const [scrollProgress, setScrollProgress] = useState(0);
const containerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const updateProgress = () => { /* measure, clamp 0..1, setState */ };
updateProgress();
window.addEventListener('scroll', updateProgress, { passive: true });
window.addEventListener('resize', updateProgress);
return () => { /* remove both */ };
}, []);
```
Two of them also duplicated a second effect that measures how far a list
can travel before its last row clears the container.
## What changed
New `resources/js/hooks/use-scroll-progress.ts`:
- **`useScrollProgress()`** — returns `{ ref, progress }`, where
progress goes 0 → 1 as the element crosses the viewport. Nine call
sites.
- **`useScrollTranslate({ speed, padding })`** — the self-scrolling
lists (banks, transactions): adds the travel measurement and returns
`translateY` derived from progress instead of keeping it in a second
piece of state. Two call sites.
`welcome.tsx` goes from 2912 to 2647 lines. No visual or behavioural
change intended: same listeners, same passive flag, same clamping, same
values.
## Metrics
| | before | after |
|---|---|---|
| duplicated lines (tsx) | 5.82% | 5.45% |
| duplicated lines (total) | 5.34% | 5.13% |
| clones | 322 | 313 |
| clones in `welcome.tsx` | 8 | 0 |
## Testing
**Unit** — `use-scroll-progress.test.ts` pins the clamped progress at
three positions (below the fold → 0, passed above → 1, mid-viewport →
the expected ratio). Nothing covered this before.
**Browser** — loaded the landing locally and scrolled every preview into
view, asserting each one's inline styles actually change between "just
entering" and "in view". All nine still animate: the eight that move
`transform` / `opacity` / `top`, plus the cashflow chart, which animates
bar `height`. No JS console errors (only external gravatar/placeholder
404s, pre-existing).