A mesmerizing liquid morphing effect using SVG filters and animations for creating organic, blob-like transitions and shapes.
"use client"
import { useState } from "react"
import { AnimatePresence, motion } from "motion/react"
import { Button } from "@/components/ui/button"
import useScreenSize from "@/hooks/useScreenSize"
import GooeySvgFilter from "../components/gooey-svg-filter"
const TAB_CONTENT = [
{
title: "2024",
files: [
"learning-to-meditate.md",
"spring-garden-plans.md",
"travel-wishlist.md",
"new-coding-projects.md",
],
},
{
title: "2023",
files: [
"year-in-review.md",
"marathon-training-log.md",
"recipe-collection.md",
"book-reflections.md",
],
},
{
title: "2022",
files: [
"moving-to-a-new-city.md",
"starting-a-blog.md",
"photography-basics.md",
"first-coding-project.md",
],
},
{
title: "2021",
files: [
"goals-and-aspirations.md",
"daily-gratitude.md",
"learning-to-cook.md",
"remote-work-journal.md",
],
},
]
export default function GooeyDemo() {
const [activeTab, setActiveTab] = useState(0)
const [isGooeyEnabled, setIsGooeyEnabled] = useState(true)
const screenSize = useScreenSize()
return (
<div className="relative w-full h-150 flex justify-center p-8 font-calendas md:text-base text-xs sm:text-sm ">
<GooeySvgFilter
id="gooey-filter"
strength={screenSize.lessThan("md") ? 8 : 15}
/>
<Button
variant="outline"
onClick={() => setIsGooeyEnabled(!isGooeyEnabled)}
className="absolute top-4 left-4 font-overused-grotesk"
>
{isGooeyEnabled ? "Disable filter" : "Enable filter"}
</Button>
<div className="w-11/12 md:w-4/5 relative mt-24">
<div
className="absolute inset-0"
style={{ filter: isGooeyEnabled ? "url(#gooey-filter)" : "none" }}
>
<div className="flex w-full ">
{TAB_CONTENT.map((tab, index) => (
<div key={tab.title} className="relative flex-1 h-8 md:h-12">
{activeTab === index && (
<motion.div
layoutId="active-tab"
className="absolute inset-0 bg-[#404040]"
transition={{
type: "spring",
bounce: 0.0,
duration: 0.4,
}}
/>
)}
</div>
))}
</div>
{/* Content panel */}
<div className="w-full h-50 sm:h-62.5 md:h-75 bg-[#404040] overflow-hidden text-neutral-200">
<AnimatePresence mode="popLayout">
<motion.div
key={activeTab}
initial={{
opacity: 0,
y: 40,
filter: "blur(8px)",
}}
animate={{
opacity: 1,
y: 0,
filter: "blur(0px)",
}}
exit={{
opacity: 0,
y: -40,
filter: "blur(8px)",
}}
transition={{
duration: 0.25,
ease: [0.23, 1, 0.32, 1],
}}
className="p-8 md:p-12"
>
<div className="space-y-2 mt-4 sm:mt-8 md:mt-8">
<ul>
{TAB_CONTENT[activeTab].files.map((file, fileIndex) => (
<motion.li
key={file}
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.3,
delay: fileIndex * 0.04,
ease: [0.23, 1, 0.32, 1],
}}
className="border-b border-muted-foreground/50 pt-2 pb-1 text-white"
>
{file}
</motion.li>
))}
</ul>
</div>
</motion.div>
</AnimatePresence>
</div>
</div>
{/* Interactive text overlay, no filter */}
<div className="relative flex w-full ">
{TAB_CONTENT.map((tab, index) => (
<button
key={tab.title}
onClick={() => setActiveTab(index)}
className="flex-1 h-8 md:h-12 cursor-pointer"
aria-label={`Show ${tab.title} content`}
>
<span
className={`
w-full h-full flex items-center justify-center transition-colors duration-200
${activeTab === index ? "text-neutral-300" : "text-white"}
`}
>
{tab.title}
</span>
</button>
))}
</div>
</div>
</div>
)
}