{ "name": "collection-surfer", "type": "registry:ui", "files": [ { "path": "components/ui/collection-surfer.tsx", "content": "\"use client\";\n\nimport React, { useRef } from \"react\";\nimport { motion, useScroll, useTransform, useSpring, useMotionValue, MotionValue } from \"framer-motion\";\n\nexport interface CollectionItem {\n id: number;\n image: string;\n title: string;\n}\n\nexport type CollectionSurferVariant = \"magnetic\" | \"uplift\" | \"simple\";\n\n// Default items for the component in case none are provided\nconst ITEMS: CollectionItem[] = [\n { id: 1, image: \"https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?w=800&q=80\", title: \"HERITAGE 01\" },\n { id: 2, image: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=800&q=80\", title: \"HERITAGE 02\" },\n { id: 3, image: \"https://images.unsplash.com/photo-1483985988355-763728e1935b?w=800&q=80\", title: \"HERITAGE 03\" },\n { id: 4, image: \"https://images.unsplash.com/photo-1500917293891-ef795e70e1f6?w=800&q=80\", title: \"HERITAGE 04\" },\n { id: 5, image: \"https://images.unsplash.com/photo-1496747611176-843222e1e57c?w=800&q=80\", title: \"HERITAGE 05\" },\n { id: 6, image: \"https://images.unsplash.com/photo-1532453288672-3a27e9be9efd?w=800&q=80\", title: \"HERITAGE 06\" },\n { id: 7, image: \"https://images.unsplash.com/photo-1552374196-1ab2a1c593e8?w=800&q=80\", title: \"HERITAGE 07\" },\n { id: 8, image: \"https://images.unsplash.com/photo-1509631179647-0177331693ae?w=800&q=80\", title: \"HERITAGE 08\" },\n { id: 9, image: \"https://images.unsplash.com/photo-1502716119720-b23a93e5fe1b?w=800&q=80\", title: \"HERITAGE 09\" },\n { id: 10, image: \"https://images.unsplash.com/photo-1539008835657-9e8e9680c956?w=800&q=80\", title: \"HERITAGE 10\" },\n { id: 11, image: \"https://images.unsplash.com/photo-1469334031218-e382a71b716b?w=800&q=80\", title: \"HERITAGE 11\" },\n { id: 12, image: \"https://images.unsplash.com/photo-1531746020798-e6953c6e8e04?w=800&q=80\", title: \"HERITAGE 12\" },\n { id: 13, image: \"https://images.unsplash.com/photo-1581044777550-4cfa60707c03?w=800&q=80\", title: \"HERITAGE 13\" },\n { id: 14, image: \"https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=800&q=80\", title: \"HERITAGE 14\" },\n { id: 15, image: \"https://images.unsplash.com/photo-1496217590455-aa63a8350eea?w=800&q=80\", title: \"HERITAGE 15\" },\n { id: 16, image: \"https://images.unsplash.com/photo-1571513722275-4b41940f54b8?w=800&q=80\", title: \"HERITAGE 16\" },\n];\n\ninterface CollectionSurferProps {\n items?: CollectionItem[];\n variant?: CollectionSurferVariant;\n}\n\nexport function CollectionSurfer({ items = ITEMS, variant = \"magnetic\" }: CollectionSurferProps) {\n // 1. Loop Setup: Duplicate items to create a buffer\n // We render the list twice: [Original Set, Duplicate Set]\n // When we scroll past the Original Set, we snap back to the start.\n const duplicatedItems = [...items, ...items];\n\n // Scroll sensitivity\n const scrollPerItem = 600;\n\n // The exact scroll distance to complete one full loop of the ORIGINAL items\n const loopDistance = items.length * scrollPerItem;\n\n const { scrollY } = useScroll();\n\n const smoothScroll = useSpring(scrollY, {\n mass: 0.1,\n stiffness: 100,\n damping: 20\n });\n\n // 2. Modulo Logic:\n // Instead of mapping 0 -> totalScroll, we map to a looped value.\n // loops 0 -> loopDistance -> 0 -> loopDistance...\n const loopedProgress = useTransform(smoothScroll, (value) => value % loopDistance);\n\n // Step vector\n const stepX = 240;\n const stepY = -84;\n const stepZ = -288;\n\n // We only move the scene backwards by the length of ONE set of items\n const x = useTransform(loopedProgress, [0, loopDistance], [0, -items.length * stepX]);\n const y = useTransform(loopedProgress, [0, loopDistance], [0, -items.length * stepY]);\n const z = useTransform(loopedProgress, [0, loopDistance], [0, -items.length * stepZ]);\n\n // Mouse position for magnetic effect\n // Initialize off-screen so no card is scaled by default\n const mouseX = useMotionValue(-10000);\n const mouseY = useMotionValue(-10000);\n\n const handleMouseMove = (e: React.MouseEvent) => {\n if (variant === \"simple\") return;\n mouseX.set(e.clientX);\n mouseY.set(e.clientY);\n };\n\n const handleMouseLeave = () => {\n if (variant === \"simple\") return;\n mouseX.set(-10000);\n mouseY.set(-10000);\n };\n\n return (\n
\n {/* 3. Infinite Spacer: \n We make this huge so the user can scroll for a very long time.\n (e.g., 50,000px) */}\n
\n\n {/* Fixed viewport */}\n \n\n {/* UI Overlays */}\n
\n

\n HERITAGE FW25/26\n

\n

\n COLLECTION\n \n ({items.length})\n \n

\n
\n\n
\n scroll to surf\n
\n\n {/* 3D Scene */}\n \n {/* Animated Track */}\n \n {duplicatedItems.map((item, i) => (\n \n ))}\n \n
\n
\n \n );\n}\n\nfunction Card({\n item,\n i,\n stepX,\n stepY,\n stepZ,\n mouseX,\n mouseY,\n scrollSpring,\n variant\n}: {\n item: CollectionItem,\n i: number,\n stepX: number,\n stepY: number,\n stepZ: number,\n mouseX: MotionValue,\n mouseY: MotionValue,\n scrollSpring: MotionValue,\n variant: CollectionSurferVariant\n}) {\n const ref = useRef(null);\n\n // Calculate distance from mouse to center of card\n const distance = useTransform([mouseX, mouseY, scrollSpring], ([x, y]) => {\n if (!ref.current || variant === \"simple\") return 200; // Default large distance\n const rect = ref.current.getBoundingClientRect();\n const centerX = rect.left + rect.width / 2;\n const centerY = rect.top + rect.height / 2;\n const dist = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2));\n return dist;\n });\n\n // --- Magnetic Variant ---\n // Map distance to scale: Closer = larger\n const targetScale = useTransform(distance, [0, 400], [1.5, 1]);\n const springScale = useSpring(targetScale, {\n mass: 0.5,\n stiffness: 300,\n damping: 20\n });\n\n // --- Uplift Variant ---\n // Map distance to Y uplift: Closer = move up (negative Y)\n const targetUplift = useTransform(distance, [0, 400], [-100, 0]);\n const springUplift = useSpring(targetUplift, {\n mass: 0.5,\n stiffness: 300,\n damping: 20\n });\n\n // Combine transforms based on variant\n const transform = useTransform(\n [springScale, springUplift],\n ([s, u]) => {\n let scaleValue = 1;\n let upliftValue = 0;\n\n if (variant === \"magnetic\") {\n scaleValue = Number(s);\n } else if (variant === \"uplift\") {\n upliftValue = Number(u);\n }\n\n const baseX = i * stepX;\n const baseY = i * stepY;\n const baseZ = i * stepZ;\n\n return `translate3d(${baseX}px, ${baseY + upliftValue}px, ${baseZ}px) rotateY(-50deg) scale(${scaleValue})`;\n }\n );\n\n return (\n \n {/* Index number: Using i % 16 + 1 so the duplicate cards show correct numbers (01-16) */}\n
\n {String((i % 16) + 1).padStart(2, '0')}\n
\n\n {/* Image */}\n
\n \n
\n\n
\n \n );\n}\n", "type": "registry:ui" } ] }