forked from emre/www_projectmycelium_com
feat: add animated transitions to cloud features tabs
- Implemented slide and fade animations when switching between feature tabs using Framer Motion - Added animated background indicator that follows the selected tab - Enhanced hover states with scale transitions and outline effects for better interactivity
This commit is contained in:
@@ -130,27 +130,111 @@ const features = [
|
||||
]
|
||||
|
||||
|
||||
interface CustomAnimationProps {
|
||||
isForwards: boolean
|
||||
changeCount: number
|
||||
}
|
||||
|
||||
const maxZIndex = 2147483647
|
||||
|
||||
const bodyVariantBackwards: Variant = {
|
||||
opacity: 0.4,
|
||||
scale: 0.8,
|
||||
zIndex: 0,
|
||||
filter: 'blur(4px)',
|
||||
transition: { duration: 0.4 },
|
||||
}
|
||||
|
||||
const bodyAnimation: MotionProps = {
|
||||
initial: 'initial',
|
||||
animate: 'animate',
|
||||
exit: 'exit',
|
||||
variants: {
|
||||
initial: (custom: CustomAnimationProps) =>
|
||||
custom.isForwards
|
||||
? {
|
||||
y: '100%',
|
||||
zIndex: maxZIndex - custom.changeCount,
|
||||
transition: { duration: 0.4 },
|
||||
}
|
||||
: bodyVariantBackwards,
|
||||
animate: (custom: CustomAnimationProps) => ({
|
||||
y: '0%',
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
zIndex: maxZIndex / 2 - custom.changeCount,
|
||||
filter: 'blur(0px)',
|
||||
transition: { duration: 0.4 },
|
||||
}),
|
||||
exit: (custom: CustomAnimationProps) =>
|
||||
custom.isForwards
|
||||
? bodyVariantBackwards
|
||||
: {
|
||||
y: '100%',
|
||||
zIndex: maxZIndex - custom.changeCount,
|
||||
transition: { duration: 0.4 },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
function usePrevious<T>(value: T) {
|
||||
const ref = useRef<T>()
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = value
|
||||
}, [value])
|
||||
|
||||
return ref.current
|
||||
}
|
||||
|
||||
/* Desktop Component */
|
||||
function CloudFeaturesDesktop() {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
let [changeCount, setChangeCount] = useState(0)
|
||||
let [selectedIndex, setSelectedIndex] = useState(0)
|
||||
let prevIndex = usePrevious(selectedIndex)
|
||||
let isForwards = prevIndex === undefined ? true : selectedIndex > prevIndex
|
||||
|
||||
let onChange = useDebouncedCallback(
|
||||
(selectedIndex: number) => {
|
||||
setSelectedIndex(selectedIndex)
|
||||
setChangeCount((changeCount) => changeCount + 1)
|
||||
},
|
||||
100,
|
||||
{ leading: true },
|
||||
)
|
||||
|
||||
return (
|
||||
<TabGroup vertical className="grid grid-cols-12 gap-10">
|
||||
<TabGroup
|
||||
vertical
|
||||
className="grid grid-cols-12 items-start gap-10"
|
||||
selectedIndex={selectedIndex}
|
||||
onChange={onChange}
|
||||
>
|
||||
<TabList className="col-span-6 space-y-6 pl-4 sm:pl-6 lg:pl-8">
|
||||
{features.map((feature, i) => (
|
||||
{features.map((feature, featureIndex) => (
|
||||
<div
|
||||
key={feature.name}
|
||||
className={clsx(
|
||||
'relative rounded-2xl transition-all hover:scale-[1.02] hover:bg-gray-100',
|
||||
selectedIndex === i
|
||||
? 'ring-2 ring-cyan-500 bg-white shadow'
|
||||
: 'ring-1 ring-transparent',
|
||||
'relative rounded-2xl outline-2 transition-all duration-300 ease-in-out hover:scale-105 hover:bg-gray-100',
|
||||
selectedIndex === featureIndex
|
||||
? 'outline-cyan-500'
|
||||
: 'outline-transparent hover:outline-cyan-500',
|
||||
)}
|
||||
>
|
||||
<div className="p-8">
|
||||
{featureIndex === selectedIndex && (
|
||||
<motion.div
|
||||
layoutId="activeBackground"
|
||||
className="absolute inset-0 bg-white shadow"
|
||||
initial={{ borderRadius: 16 }}
|
||||
/>
|
||||
)}
|
||||
<div className="relative z-10 p-8">
|
||||
<feature.icon className="h-8 w-8" />
|
||||
<FeatureTitle as="h3" className="mt-6 text-gray-900">
|
||||
<Tab>{feature.name}</Tab>
|
||||
<Tab className="text-left data-selected:not-data-focus:outline-hidden">
|
||||
<span className="absolute inset-0 rounded-2xl" />
|
||||
{feature.name}
|
||||
</Tab>
|
||||
</FeatureTitle>
|
||||
<FeatureDescription className="mt-2 text-gray-600">
|
||||
{feature.description}
|
||||
@@ -160,15 +244,30 @@ function CloudFeaturesDesktop() {
|
||||
))}
|
||||
</TabList>
|
||||
|
||||
<div className="col-span-6">
|
||||
<div className="relative col-span-6">
|
||||
<TabPanels as={Fragment}>
|
||||
{features.map((feature, i) => (
|
||||
<TabPanel key={feature.name}>
|
||||
<div className="w-full flex justify-center">
|
||||
<AnimatePresence
|
||||
initial={false}
|
||||
custom={{ isForwards, changeCount }}
|
||||
>
|
||||
{features.map((feature, featureIndex) =>
|
||||
selectedIndex === featureIndex ? (
|
||||
<TabPanel
|
||||
static
|
||||
key={feature.name + changeCount}
|
||||
className="col-start-1 row-start-1 flex focus:outline-offset-32 data-selected:not-data-focus:outline-hidden"
|
||||
>
|
||||
<motion.div
|
||||
{...bodyAnimation}
|
||||
custom={{ isForwards, changeCount }}
|
||||
className="w-full flex justify-center"
|
||||
>
|
||||
<feature.screen />
|
||||
</div>
|
||||
</motion.div>
|
||||
</TabPanel>
|
||||
))}
|
||||
) : null,
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</TabPanels>
|
||||
</div>
|
||||
</TabGroup>
|
||||
|
||||
Reference in New Issue
Block a user