01Kamu
Polis
Patterns/Horizontal reel
A row scrolled sideways by scrolling down, with a progress hairline.
components/ds/, with its registry dependencies and npm packages. See Installation for the one-time setup.bunx shadcn@3 add @fastrp/horizontal-reel
01Polis
02Haber
03Kumarhane
04Belediye
05Sokak
06İş dünyası
<HorizontalReel />A row the reader scrolls sideways through by scrolling down: the section pins, and the track slides along for as long as it runs past the screen, with a progress hairline under it. The section is exactly that much taller than a screen, measured — so a card more or less needs no tuning.
| Prop | Type | Default | Notes |
|---|---|---|---|
| children* | ReactNode | — | The cards; they keep their own widths — give each a fixed one. |
| className | string | — | |
| header | ReactNode | — | Above the track: a title, a label. |
npm
Registry
'use client';
import { motion, useScroll, useTransform } from 'motion/react';
import type * as React from 'react';
import { useEffect, useRef, useState } from 'react';
import { useScrollRoot } from '@/components/ds/scroll-root';
import { cn } from '@/lib/utils';
// @fast-ds [email protected]
export type HorizontalReelProps = {
/** Above the track: a title, a label. */
header?: React.ReactNode;
/** The cards; they keep their own widths — give each a fixed one. */
children: React.ReactNode;
className?: string;
};
/**
* A row the reader scrolls sideways through by scrolling down: the section pins, and the track
* slides along for as long as it runs past the screen, with a progress hairline under it. The
* section is exactly that much taller than a screen, measured — so a card more or less needs no
* tuning.
*/
function HorizontalReel({ header, children, className }: HorizontalReelProps) {
const root = useScrollRoot();
const sectionRef = useRef<HTMLElement>(null);
const stageRef = useRef<HTMLDivElement>(null);
const trackRef = useRef<HTMLDivElement>(null);
const [overflow, setOverflow] = useState(0);
useEffect(() => {
const stage = stageRef.current;
const track = trackRef.current;
if (!stage || !track) return;
const measure = () => setOverflow(Math.max(0, track.scrollWidth - stage.clientWidth));
measure();
const observer = new ResizeObserver(measure);
observer.observe(track);
observer.observe(stage);
return () => observer.disconnect();
}, []);
const { scrollYProgress } = useScroll({
container: root,
target: sectionRef,
offset: ['start start', 'end end'],
});
const x = useTransform(scrollYProgress, (progress) => -progress * overflow);
return (
<section
ref={sectionRef}
data-slot="horizontal-reel"
// A measured length in real pixels, through a variable so the height stays a class.
style={{ '--reel-overflow': `${overflow}px` } as React.CSSProperties}
className={cn(
'relative h-[calc(var(--scroll-root-h,100vh)+var(--reel-overflow))] supports-[height:100svh]:h-[calc(var(--scroll-root-h,100svh)+var(--reel-overflow))]',
className,
)}
>
<div
ref={stageRef}
className="sticky top-0 flex h-[var(--scroll-root-h,100vh)] flex-col justify-center gap-10 overflow-hidden supports-[height:100svh]:h-[var(--scroll-root-h,100svh)]"
>
{header && <div className="px-6 md:px-12 lg:px-20">{header}</div>}
<motion.div
ref={trackRef}
style={{ x }}
className="flex w-max gap-5 px-6 md:gap-8 md:px-12 lg:px-20"
>
{children}
</motion.div>
<div className="mx-6 h-px overflow-hidden bg-line md:mx-12 lg:mx-20">
<motion.div
style={{ scaleX: scrollYProgress }}
className="h-full origin-left bg-foreground/70"
/>
</div>
</div>
</section>
);
}
export { HorizontalReel };