UNPKG

reactbits-mcp-server

Version:

MCP Server for React Bits - Access 99+ React components with animations, backgrounds, and UI elements

277 lines (257 loc) 8.28 kB
import React, { useState, Children, useRef, useLayoutEffect } from "react"; import { motion, AnimatePresence } from "framer-motion"; export default function Stepper({ children, initialStep = 1, onStepChange = () => { }, onFinalStepCompleted = () => { }, stepCircleContainerClassName = "", stepContainerClassName = "", contentClassName = "", footerClassName = "", backButtonProps = {}, nextButtonProps = {}, backButtonText = "Back", nextButtonText = "Continue", disableStepIndicators = false, renderStepIndicator, ...rest }) { const [currentStep, setCurrentStep] = useState(initialStep); const [direction, setDirection] = useState(0); const stepsArray = Children.toArray(children); const totalSteps = stepsArray.length; const isCompleted = currentStep > totalSteps; const isLastStep = currentStep === totalSteps; const updateStep = (newStep) => { setCurrentStep(newStep); if (newStep > totalSteps) onFinalStepCompleted(); else onStepChange(newStep); }; const handleBack = () => { if (currentStep > 1) { setDirection(-1); updateStep(currentStep - 1); } }; const handleNext = () => { if (!isLastStep) { setDirection(1); updateStep(currentStep + 1); } }; const handleComplete = () => { setDirection(1); updateStep(totalSteps + 1); }; return ( <div className="flex min-h-full flex-1 flex-col items-center justify-center p-4 sm:aspect-[4/3] md:aspect-[2/1]" {...rest} > <div className={`mx-auto w-full max-w-md rounded-4xl shadow-xl ${stepCircleContainerClassName}`} style={{ border: "1px solid #222" }} > <div className={`${stepContainerClassName} flex w-full items-center p-8`}> {stepsArray.map((_, index) => { const stepNumber = index + 1; const isNotLastStep = index < totalSteps - 1; return ( <React.Fragment key={stepNumber}> {renderStepIndicator ? ( renderStepIndicator({ step: stepNumber, currentStep, onStepClick: (clicked) => { setDirection(clicked > currentStep ? 1 : -1); updateStep(clicked); }, }) ) : ( <StepIndicator step={stepNumber} disableStepIndicators={disableStepIndicators} currentStep={currentStep} onClickStep={(clicked) => { setDirection(clicked > currentStep ? 1 : -1); updateStep(clicked); }} /> )} {isNotLastStep && ( <StepConnector isComplete={currentStep > stepNumber} /> )} </React.Fragment> ); })} </div> <StepContentWrapper isCompleted={isCompleted} currentStep={currentStep} direction={direction} className={`space-y-2 px-8 ${contentClassName}`} > {stepsArray[currentStep - 1]} </StepContentWrapper> {!isCompleted && ( <div className={`px-8 pb-8 ${footerClassName}`}> <div className={`mt-10 flex ${currentStep !== 1 ? "justify-between" : "justify-end" }`} > {currentStep !== 1 && ( <button onClick={handleBack} className={`duration-350 rounded px-2 py-1 transition ${currentStep === 1 ? "pointer-events-none opacity-50 text-neutral-400" : "text-neutral-400 hover:text-neutral-700" }`} {...backButtonProps} > {backButtonText} </button> )} <button onClick={isLastStep ? handleComplete : handleNext} className="duration-350 flex items-center justify-center rounded-full bg-green-500 py-1.5 px-3.5 font-medium tracking-tight text-white transition hover:bg-green-600 active:bg-green-700" {...nextButtonProps} > {isLastStep ? "Complete" : nextButtonText} </button> </div> </div> )} </div> </div> ); } function StepContentWrapper({ isCompleted, currentStep, direction, children, className }) { const [parentHeight, setParentHeight] = useState(0); return ( <motion.div style={{ position: "relative", overflow: "hidden" }} animate={{ height: isCompleted ? 0 : parentHeight }} transition={{ type: "spring", duration: 0.4 }} className={className} > <AnimatePresence initial={false} mode="sync" custom={direction}> {!isCompleted && ( <SlideTransition key={currentStep} direction={direction} onHeightReady={(h) => setParentHeight(h)} > {children} </SlideTransition> )} </AnimatePresence> </motion.div> ); } function SlideTransition({ children, direction, onHeightReady }) { const containerRef = useRef(null); useLayoutEffect(() => { if (containerRef.current) onHeightReady(containerRef.current.offsetHeight); }, [children, onHeightReady]); return ( <motion.div ref={containerRef} custom={direction} variants={stepVariants} initial="enter" animate="center" exit="exit" transition={{ duration: 0.4 }} style={{ position: "absolute", left: 0, right: 0, top: 0 }} > {children} </motion.div> ); } const stepVariants = { enter: (dir) => ({ x: dir >= 0 ? "-100%" : "100%", opacity: 0, }), center: { x: "0%", opacity: 1, }, exit: (dir) => ({ x: dir >= 0 ? "50%" : "-50%", opacity: 0, }), }; export function Step({ children }) { return <div className="px-8">{children}</div>; } function StepIndicator({ step, currentStep, onClickStep, disableStepIndicators }) { const status = currentStep === step ? "active" : currentStep < step ? "inactive" : "complete"; const handleClick = () => { if (step !== currentStep && !disableStepIndicators) onClickStep(step); }; return ( <motion.div onClick={handleClick} className="relative cursor-pointer outline-none focus:outline-none" animate={status} initial={false} > <motion.div variants={{ inactive: { scale: 1, backgroundColor: "#222", color: "#a3a3a3" }, active: { scale: 1, backgroundColor: "#5227FF", color: "#5227FF" }, complete: { scale: 1, backgroundColor: "#5227FF", color: "#3b82f6" }, }} transition={{ duration: 0.3 }} className="flex h-8 w-8 items-center justify-center rounded-full font-semibold" > {status === "complete" ? ( <CheckIcon className="h-4 w-4 text-black" /> ) : status === "active" ? ( <div className="h-3 w-3 rounded-full bg-[#060010]" /> ) : ( <span className="text-sm">{step}</span> )} </motion.div> </motion.div> ); } function StepConnector({ isComplete }) { const lineVariants = { incomplete: { width: 0, backgroundColor: "transparent" }, complete: { width: "100%", backgroundColor: "#5227FF" }, }; return ( <div className="relative mx-2 h-0.5 flex-1 overflow-hidden rounded bg-neutral-600"> <motion.div className="absolute left-0 top-0 h-full" variants={lineVariants} initial={false} animate={isComplete ? "complete" : "incomplete"} transition={{ duration: 0.4 }} /> </div> ); } function CheckIcon(props) { return ( <svg {...props} fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24" > <motion.path initial={{ pathLength: 0 }} animate={{ pathLength: 1 }} transition={{ delay: 0.1, type: "tween", ease: "easeOut", duration: 0.3 }} strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /> </svg> ); }