{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "feature-carousel", "type": "registry:ui", "description": "Feature carousel component with smooth transitions and customizable layouts", "dependencies": [ "motion" ], "files": [ { "path": "registry/default/ui/feature-carousel.tsx", "content": "/*\n ! Add the following to your .globals.css\n\n .animated-cards::before {\n @apply pointer-events-none absolute select-none rounded-3xl opacity-0 transition-opacity duration-300 hover:opacity-100;\n background: radial-gradient(\n 1000px circle at var(--x) var(--y),\n #c9ee80 0,\n #eebbe2 10%,\n #adc0ec 25%,\n #c9ee80 35%,\n rgba(255, 255, 255, 0) 50%,\n transparent 80%\n );\n z-index: -1;\n content: \"\";\n inset: -1px;\n }\n*/\n\"use client\"\n\nimport {\n forwardRef,\n useCallback,\n useEffect,\n useRef,\n useState,\n type MouseEvent,\n} from \"react\"\nimport Image, { type StaticImageData } from \"next/image\"\nimport clsx from \"clsx\"\nimport {\n AnimatePresence,\n motion,\n useMotionTemplate,\n useMotionValue,\n type MotionStyle,\n type MotionValue,\n type Variants,\n} from \"motion/react\"\nimport Balancer from \"react-wrap-balancer\"\n\nimport { cn } from \"@/lib/utils\"\n\n// Types\ntype WrapperStyle = MotionStyle & {\n \"--x\": MotionValue\n \"--y\": MotionValue\n}\n\ninterface CardProps {\n title: string\n description: string\n bgClass?: string\n}\n\ninterface ImageSet {\n step1dark1?: StaticImageData | string\n step1dark2?: StaticImageData | string\n step1light1: StaticImageData | string\n step1light2: StaticImageData | string\n step2dark1?: StaticImageData | string\n step2dark2?: StaticImageData | string\n step2light1: StaticImageData | string\n step2light2: StaticImageData | string\n step3dark?: StaticImageData | string\n step3light: StaticImageData | string\n step4light: StaticImageData | string\n alt: string\n}\n\ninterface FeatureCarouselProps extends CardProps {\n step1img1Class?: string\n step1img2Class?: string\n step2img1Class?: string\n step2img2Class?: string\n step3imgClass?: string\n step4imgClass?: string\n image: ImageSet\n}\n\ninterface StepImageProps {\n src: StaticImageData | string\n alt: string\n className?: string\n style?: React.CSSProperties\n width?: number\n height?: number\n}\n\ninterface Step {\n id: string\n name: string\n title: string\n description: string\n}\n\n// Constants\nconst TOTAL_STEPS = 4\n\nconst steps = [\n {\n id: \"1\",\n name: \"Step 1\",\n title: \"Feature 1\",\n description: \"Feature 1 description \",\n },\n {\n id: \"2\",\n name: \"Step 2\",\n title: \"Feature 2\",\n description: \"Feature 2 description\",\n },\n {\n id: \"3\",\n name: \"Step 3\",\n title: \"Feature 3\",\n description: \"Feature 3 description\",\n },\n {\n id: \"4\",\n name: \"Step 4\",\n title: \"Feature 4\",\n description: \"Feature 4 description\",\n },\n] as const\n\n/**\n * Animation presets for reusable motion configurations.\n * Each preset defines the initial, animate, and exit states,\n * along with spring physics parameters for smooth transitions.\n */\nconst ANIMATION_PRESETS = {\n fadeInScale: {\n initial: { opacity: 0, scale: 0.95 },\n animate: { opacity: 1, scale: 1 },\n exit: { opacity: 0, scale: 0.95 },\n transition: {\n type: \"spring\",\n stiffness: 300, // Higher value = more rigid spring\n damping: 25, // Higher value = less oscillation\n mass: 0.5, // Lower value = faster movement\n },\n },\n slideInRight: {\n initial: { opacity: 0, x: 20 },\n animate: { opacity: 1, x: 0 },\n exit: { opacity: 0, x: -20 },\n transition: {\n type: \"spring\",\n stiffness: 300,\n damping: 25,\n mass: 0.5,\n },\n },\n slideInLeft: {\n initial: { opacity: 0, x: -20 },\n animate: { opacity: 1, x: 0 },\n exit: { opacity: 0, x: 20 },\n transition: {\n type: \"spring\",\n stiffness: 300,\n damping: 25,\n mass: 0.5,\n },\n },\n} as const\n\ntype AnimationPreset = keyof typeof ANIMATION_PRESETS\n\ninterface AnimatedStepImageProps extends StepImageProps {\n preset?: AnimationPreset\n delay?: number\n onAnimationComplete?: () => void\n}\n\n/**\n * Custom hook for managing cyclic transitions with auto-play functionality.\n * Handles both automatic cycling and manual transitions between steps.\n */\nfunction useNumberCycler(\n totalSteps: number = TOTAL_STEPS,\n interval: number = 3000\n) {\n const [currentNumber, setCurrentNumber] = useState(0)\n const [isManualInteraction, setIsManualInteraction] = useState(false)\n const timerRef = useRef(null)\n\n // Setup timer function\n const setupTimer = useCallback(() => {\n console.log(\"Setting up timer\")\n // Clear any existing timer\n if (timerRef.current) {\n clearTimeout(timerRef.current)\n }\n\n timerRef.current = setTimeout(() => {\n console.log(\"Timer triggered, advancing to next step\")\n setCurrentNumber((prev) => (prev + 1) % totalSteps)\n setIsManualInteraction(false)\n // Recursively setup next timer\n setupTimer()\n }, interval)\n }, [interval, totalSteps])\n\n // Handle manual increment\n const increment = useCallback(() => {\n console.log(\"Manual increment triggered\")\n setIsManualInteraction(true)\n setCurrentNumber((prev) => (prev + 1) % totalSteps)\n\n // Reset timer on manual interaction\n setupTimer()\n }, [totalSteps, setupTimer])\n\n // Initial timer setup and cleanup\n useEffect(() => {\n console.log(\"Initial timer setup\")\n setupTimer()\n\n return () => {\n console.log(\"Cleaning up timer\")\n if (timerRef.current) {\n clearTimeout(timerRef.current)\n }\n }\n }, [setupTimer])\n\n // Debug logging\n useEffect(() => {\n console.log(\"Current state:\", {\n currentNumber,\n isManualInteraction,\n hasTimer: !!timerRef.current,\n })\n }, [currentNumber, isManualInteraction])\n\n return {\n currentNumber,\n increment,\n isManualInteraction,\n }\n}\n\nfunction useIsMobile() {\n const [isMobile, setIsMobile] = useState(false)\n\n useEffect(() => {\n const userAgent = navigator.userAgent\n const isSmall = window.matchMedia(\"(max-width: 768px)\").matches\n const isMobile = Boolean(\n /Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i.exec(\n userAgent\n )\n )\n\n const isDev = process.env.NODE_ENV !== \"production\"\n if (isDev) setIsMobile(isSmall || isMobile)\n\n setIsMobile(isSmall && isMobile)\n }, [])\n\n return isMobile\n}\n\n// Components\nfunction IconCheck({ className, ...props }: React.ComponentProps<\"svg\">) {\n return (\n \n \n \n )\n}\n\nconst stepVariants: Variants = {\n inactive: {\n scale: 0.8,\n opacity: 0.5,\n },\n active: {\n scale: 1,\n opacity: 1,\n },\n}\n\nconst StepImage = forwardRef<\n HTMLImageElement,\n StepImageProps & { [key: string]: any }\n>(\n (\n { src, alt, className, style, width = 1200, height = 630, ...props },\n ref\n ) => {\n return (\n \n )\n }\n)\nStepImage.displayName = \"StepImage\"\n\nconst MotionStepImage = motion(StepImage)\n\n/**\n * Wrapper component for StepImage that applies animation presets.\n * Simplifies the application of complex animations through preset configurations.\n */\nconst AnimatedStepImage = ({\n preset = \"fadeInScale\",\n delay = 0,\n onAnimationComplete,\n ...props\n}: AnimatedStepImageProps) => {\n const presetConfig = ANIMATION_PRESETS[preset]\n return (\n \n )\n}\n\n/**\n * Main card component that handles mouse tracking for gradient effect.\n * Uses motion values to create an interactive gradient that follows the cursor.\n */\nfunction FeatureCard({\n bgClass,\n children,\n step,\n}: CardProps & {\n children: React.ReactNode\n step: number\n}) {\n const [mounted, setMounted] = useState(false)\n const mouseX = useMotionValue(0)\n const mouseY = useMotionValue(0)\n const isMobile = useIsMobile()\n\n function handleMouseMove({ currentTarget, clientX, clientY }: MouseEvent) {\n if (isMobile) return\n const { left, top } = currentTarget.getBoundingClientRect()\n mouseX.set(clientX - left)\n mouseY.set(clientY - top)\n }\n\n useEffect(() => {\n setMounted(true)\n }, [])\n\n return (\n \n \n
\n \n \n \n {steps[step].title}\n \n \n

\n {steps[step].description}\n

\n \n \n
\n {mounted ? children : null}\n
\n \n \n )\n}\n\n/**\n * Progress indicator component that shows current step and completion status.\n * Handles complex state transitions and animations for step indicators.\n */\nfunction Steps({\n steps,\n current,\n onChange,\n}: {\n steps: readonly Step[]\n current: number\n onChange: (index: number) => void\n}) {\n return (\n \n )\n}\n\nconst defaultClasses = {\n step1img1:\n \"pointer-events-none w-[50%] border border-border-100/10 transition-all duration-500 dark:border-border-700/50 rounded-2xl\",\n step1img2:\n \"pointer-events-none w-[60%] border border-border-100/10 dark:border-border-700/50 transition-all duration-500 overflow-hidden rounded-2xl\",\n step2img1:\n \"pointer-events-none w-[50%] border border-border-100/10 transition-all duration-500 dark:border-border-700 rounded-2xl overflow-hidden\",\n step2img2:\n \"pointer-events-none w-[40%] border border-border-100/10 dark:border-border-700 transition-all duration-500 rounded-2xl overflow-hidden\",\n step3img:\n \"pointer-events-none w-[90%] border border-border-100/10 dark:border-border-700 rounded-2xl transition-all duration-500 overflow-hidden\",\n step4img:\n \"pointer-events-none w-[90%] border border-border-100/10 dark:border-border-700 rounded-2xl transition-all duration-500 overflow-hidden\",\n} as const\n\n/**\n * Main component that orchestrates the multi-step animation sequence.\n * Manages state transitions, handles animation timing, and prevents\n * animation conflicts through the isAnimating flag.\n */\nexport function FeatureCarousel({\n image,\n step1img1Class = defaultClasses.step1img1,\n step1img2Class = defaultClasses.step1img2,\n step2img1Class = defaultClasses.step2img1,\n step2img2Class = defaultClasses.step2img2,\n step3imgClass = defaultClasses.step3img,\n step4imgClass = defaultClasses.step4img,\n ...props\n}: FeatureCarouselProps) {\n const { currentNumber: step, increment } = useNumberCycler()\n const [isAnimating, setIsAnimating] = useState(false)\n\n const handleIncrement = () => {\n if (isAnimating) return\n setIsAnimating(true)\n increment()\n }\n\n const handleAnimationComplete = () => {\n setIsAnimating(false)\n }\n\n const renderStepContent = () => {\n const content = () => {\n switch (step) {\n case 0:\n /**\n * Layout: Two images side by side\n * - Left image (step1img1): 50% width, positioned left\n * - Right image (step1img2): 60% width, positioned right\n * Animation:\n * - Left image slides in from left\n * - Right image slides in from right with 0.1s delay\n * - Both use spring animation for smooth motion\n */\n return (\n \n \n \n \n )\n case 1:\n /**\n * Layout: Two images with overlapping composition\n * - First image (step2img1): 50% width, positioned left\n * - Second image (step2img2): 40% width, overlaps first image\n * Animation:\n * - Both images fade in and scale up from 95%\n * - Second image has 0.1s delay for staggered effect\n * - Uses spring physics for natural motion\n */\n return (\n \n \n \n \n )\n case 2:\n /**\n * Layout: Single centered image\n * - Full width image (step3img): 90% width, centered\n * Animation:\n * - Fades in and scales up from 95%\n * - Uses spring animation for smooth scaling\n * - Triggers animation complete callback\n */\n return (\n \n )\n case 3:\n /**\n * Layout: Final showcase layout\n * - Container: Centered, 60% width on desktop\n * - Image (cult): 90% width, positioned slightly up\n * Animation:\n * - Container fades in and scales up\n * - Image follows with 0.1s delay\n * - Both use spring physics for natural motion\n */\n return (\n \n \n \n )\n default:\n return null\n }\n }\n\n return (\n \n \n {content()}\n \n \n )\n }\n\n return (\n \n {renderStepContent()}\n \n {}} steps={steps} />\n \n \n \n )\n}\n\nexport default FeatureCarousel\n", "type": "registry:ui" } ] }