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
JSX
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>
);
}