Getting Started
Components
Search for a command to run...
import { PhotoScatter } from "@/registry/tween/photo-scatter"
export function PhotoScatterDemo() {
return (
<div className="w-full max-w-3xl">
<PhotoScatter />
</div>
)
}
import { PhotoScatter } from "@/components/ui/photo-scatter"<PhotoScatter
headings={[
"A moment before it scatters",
"Pieces drift across the frame",
"Then drawn back into order",
"The stack settles once more",
]}
/>The component pins itself for scrollMultiplier container heights and drives the gallery from scroll progress. As the user scrolls within the box, each cycle flies a fresh batch of cards in from scattered positions, holds them as a loose stack, then flies them back out as the next batch enters.
One heading is shown per cycle, crossfading to the next as each new section begins, so the copy stays in sync with the visuals throughout the pinned scroll.
| Prop | Type | Default | Description |
|---|---|---|---|
intro | ReactNode | "Time loosens its grip…" | Heading shown in the intro section above the gallery. |
outro | ReactNode | "Eventually, the stack…" | Heading shown in the outro section below the gallery. |
headings | string[] | 4 default lines | One heading per scatter cycle. Section count is derived from this length. |
height | number | string | var(--preview-h, 100vh) | Box height. A number is interpreted as pixels. |
cardCount | number | 14 | Number of photo cards per cycle. |
cardWidth | number | 170 | Card width in pixels. |
cardHeight | number | 210 | Card height in pixels. |
imageSeed | string | "tween" | Seed used by the default Picsum image source. |
imageSrc | (sectionIndex, cardIndex) => string | Picsum | Custom image source. Both indices are 0-based. |
scrollMultiplier | number | 4 | Pin length, in container heights. 4 = pin for 4× container height. |
animationDuration | number | 0.8 | Card fly-in / fly-out duration in seconds. |
animationOverlap | number | 0.45 | Overlap between exiting and entering cards on the timeline. |
headingFadeDuration | number | 0.45 | Heading crossfade duration when a new section starts. |
onSectionChange | (sectionIndex: number) => void | — | Called with the new 0-based section index after each transition (and once on mount). |
cardClassName | string | — | Extra classes appended to each card element. |
headingClassName | string | — | Extra classes appended to the gallery heading. |
className | string | — | Extra classes appended to the outer scroll container. |
Decorative photo cards are marked aria-hidden so screen readers skip them and announce only the headings. When prefers-reduced-motion: reduce is set, the scatter animation is suppressed and cards are shown in their resting layout instead of flying in and out.
The component uses theme tokens by default (bg-background, bg-muted, bg-card, border-border), so it inherits your light and dark palettes automatically. Override with className, cardClassName, and headingClassName.