crucıble

Spindle

R3F film-bobbin gallery: frames on a continuous sprocketed film strip wound helically around a reel, unwinding frame by frame into a flat contact strip as you scroll. Drag to spin or pan, arrow keys to browse, click or Enter to lift a frame into a lightbox. Off-focus duotone, idle spin, offscreen-paused, reduced-motion-safe.

r3fpro

Installation

CLI

npx shadcn@latest add @crucible/spindle

Manual — install dependencies, then copy the source

npm install three @react-three/fiber

Props

PropTypeDefaultDescription
itemsSpindleItem[]nine seeded procedural platesFrames wound onto the bobbin.
progressnumberControlled unwind, 0 (wound on the bobbin) → 1 (flat contact strip). When set, scroll and wheel input no longer drive the unwind.
scrollDrivenbooleantrueDrive the unwind from scroll: the component becomes a tall pin track (pinLength × the scroller's viewport) with a sticky stage inside. When false (and uncontrolled), wheel / vertical drag over the stage unwinds instead.
scrollerReact.RefObject<HTMLElement | null> | HTMLElement | nullthe nearest scrollable ancestor, else the windowThe scroll container that drives the unwind.
pinLengthnumber2.6Pin track length as a multiple of the scroller's viewport height.
duotone[string, string] | falsecool silverOff-focus duotone as [ink, paper] hex colors; the focused frame shows full color. false keeps every frame in full color.
interactivebooleantrueDrag to spin / pan, click to open, wheel to unwind. Keyboard always works.
pausedbooleanfalseFreeze the idle spin and the intro; transitions still ease on demand.
labelstring"Film gallery"Accessible name for the gallery.
childrenReact.ReactNodeOverlay content laid over the stage (pointer events pass through to the film).
classNamestringExtra classes for the root (the pin track when scroll-driven).
refReact.Ref<SpindleHandle>Imperative handle: unwind(), rewind(), open(i), close().

Honors prefers-reduced-motion with a designed static fallback, pauses offscreen and on hidden tabs, and passes className through.