Image Trail
loomui/image-trailFreeComponent
Images dropped along the pointer's path, spaced by distance travelled rather than by time.
Live preview
live · rendered by the registry
Rendered by loomui on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://loomui.design/r/image-trail.jsonSource
1"use client"23import * as React from "react"45import { cn } from "@/lib/utils"67export interface ImageTrailProps extends React.ComponentProps<"div"> {8 /** Sources dropped along the pointer's path, in order and on a loop. */9 images: string[]10 /** Pixels the pointer must travel before the next image is dropped. */11 distance?: number12 /** Milliseconds an image takes to arrive, hold and fade. */13 life?: number14 /** Most images alive at once. Older ones are dropped first. */15 max?: number16 /** Largest tilt either side of straight, in degrees. */17 tilt?: number18 /** Classes for each dropped image. Size and radius belong here. */19 imageClassName?: string20}2122interface Drop {23 id: number24 x: number25 y: number26 src: string27 tilt: number28}2930/**31 * Images are dropped by distance travelled rather than on a timer, so a slow32 * drag leaves the same trail a fast one does and a still pointer leaves none.33 */34export function ImageTrail({35 images,36 children,37 className,38 distance = 90,39 life = 900,40 max = 12,41 tilt = 14,42 imageClassName,43 onPointerMove,44 ...props45}: ImageTrailProps) {46 const [drops, setDrops] = React.useState<Drop[]>([])47 const last = React.useRef<{ x: number; y: number } | null>(null)48 const nextId = React.useRef(0)4950 const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {51 onPointerMove?.(event)5253 if (54 images.length === 0 ||55 window.matchMedia("(prefers-reduced-motion: reduce)").matches56 ) {57 return58 }5960 const rect = event.currentTarget.getBoundingClientRect()61 const x = event.clientX - rect.left62 const y = event.clientY - rect.top63 const previous = last.current6465 if (previous && Math.hypot(x - previous.x, y - previous.y) < distance) {66 return67 }6869 last.current = { x, y }70 const id = nextId.current++7172 setDrops((current) =>73 [74 ...current,75 {76 id,77 x,78 y,79 src: images[id % images.length],80 // Seeded off the id, so a given image in the trail always lands at81 // the same angle instead of jittering on re-render.82 tilt: ((id * 37) % (tilt * 2 + 1)) - tilt,83 },84 ].slice(-max)85 )86 }8788 const drop = React.useCallback((id: number) => {89 setDrops((current) => current.filter((item) => item.id !== id))90 }, [])9192 return (93 <div94 data-slot="image-trail"95 onPointerMove={handlePointerMove}96 onPointerLeave={() => {97 last.current = null98// … truncated
What it pulls in
Other registry items
Files it writes
More from loomui
All 91 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Aurora Backdropaurora-backdrop | loomui | Components | Free | no deps | |
| Bento Gridbento-grid | loomui | Components | Free | no deps | |
| Card Stackcard-stack | loomui | Components | Free | no deps | |
| Compare Slidercompare-slider | loomui | Components | Free | no deps | |
| Confetti Buttonconfetti-button | loomui | Components | Free | no deps | |
| Count Upcount-up | loomui | Components | Free | no deps | |
| Credit Cardcredit-card | loomui | Components | Free | no deps | |
| Drawerdrawer | loomui | Components | Free | 1 dep |
