Pulse Feature Showcase
An interactive feature showcase component with elegant animations and responsive design.
Best TV Show Ever
Meet Breaking Bad
Walter White
A Physics Teacher Who Develops Cancer And Decides To Walk Down A Dark Path
Jesse Pinkman
Walt's Trusted Partner Who Slowly Breaks Down
Saul Goodman
A Lawyer With A Mysterious Backstory

Installation
Follow these steps to install and set up the component in your project.
1
Install dependencies
Install the required dependencies to use this component. You'll need motion
for animations and lucide-react
for icons.
npm install motion lucide-react
2
Add the component code
Copy the complete component code into your project. The component is named PulesFeatures
and uses TypeScript interfaces for prop types.
/components/pulse-feature.tsx
"use client"
import { useState, useRef, useEffect } from "react"
import { motion, AnimatePresence } from "motion/react"
import { ArrowRight } from "lucide-react"
import Image from "next/image"
import Link from "next/link"
interface Feature {
title: string
description: string
icon?: string
image?: string
}
interface Props {
category: string
title: string
features: Feature[]
learnMoreLink?: string
learnMoreText?: string
previewImage?: string
}
function MeshPattern() {
return (
<svg
className="absolute inset-0 h-full w-full scale-150"
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
fill="none"
viewBox="0 0 800 800"
opacity="0.5"
>
<defs>
<pattern
id="mesh-pattern"
x="0"
y="0"
width="40"
height="40"
patternUnits="userSpaceOnUse"
>
<path
d="M.5.5h40v40H.5z"
fill="none"
stroke="white"
strokeWidth="0.6"
/>
</pattern>
</defs>
<rect width="100%" height="120%" fill="url(#mesh-pattern)" />
</svg>
)
}
export function PulesFeatures({
category,
title,
features,
learnMoreLink = '/',
learnMoreText = 'Learn more',
previewImage = '/placeholders/newpreview.png'
}: Props) {
const [hoveredFeature, setHoveredFeature] = useState<number | null>(null)
const [activeFeature, setActiveFeature] = useState<number>(0)
const [previousFeature, setPreviousFeature] = useState<number>(0)
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect()
setMousePosition({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
})
}
}
const container = containerRef.current
if (container) {
container.addEventListener('mousemove', handleMouseMove)
return () => container.removeEventListener('mousemove', handleMouseMove)
}
}, [])
const handleFeatureClick = (index: number) => {
setPreviousFeature(activeFeature)
setActiveFeature(index)
}
// Image transition variants
const imageVariants = {
enter: (direction: number) => ({
y: direction > 0 ? 1000 : -1000,
opacity: 0,
scale: 0.95,
}),
center: {
zIndex: 1,
y: 0,
opacity: 1,
scale: 1,
},
exit: (direction: number) => ({
zIndex: 0,
y: direction < 0 ? 1000 : -1000,
opacity: 0,
scale: 1.1,
}),
}
// Determine the direction of transition
const direction = activeFeature > previousFeature ? 1 : -1
return (
<div className="relative w-full overflow-hidden py-2 text-black dark:text-white">
<div
ref={containerRef}
className="mx-auto max-w-6xl rounded-2xl border border-gray-400/50 px-4 py-8 sm:px-6 lg:px-8 relative"
style={{
background: `radial-gradient(600px circle at ${mousePosition.x}px ${mousePosition.y}px, rgba(147, 51, 234, 0.1), transparent 40%)`
}}
>
<div className="grid gap-8 lg:grid-cols-2 lg:gap-16">
<div className="flex flex-col justify-center">
<motion.span
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-4 text-lg text-purple-400"
>
{category}
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="mb-8 text-4xl font-bold tracking-tight sm:text-5xl"
>
{title}
</motion.h2>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="mb-8"
>
<Link
href={learnMoreLink}
className="group inline-flex items-center gap-2 rounded-full border border-purple-400 bg-white/10 px-6 py-2 text-sm font-semibold dark:text-white text-black transition-colors hover:bg-white/20"
>
{learnMoreText}
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
</Link>
</motion.div>
<div className="space-y-6">
{features.map((feature, index) => (
<motion.div
key={feature.title}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 + index * 0.1 }}
className="group relative"
onMouseEnter={() => setHoveredFeature(index)}
onMouseLeave={() => setHoveredFeature(null)}
onClick={() => handleFeatureClick(index)}
>
<div
className={`absolute -inset-x-4 -inset-y-2 rounded-lg transition-all duration-300 sm:m-0 m-2 ${
hoveredFeature === index || activeFeature === index
? "bg-gradient-to-r from-purple-500/10 via-purple-400/10 to-purple-500/10 border border-purple-500/30"
: "bg-transparent border border-transparent"
}`}
/>
<div className="relative flex cursor-pointer items-start gap-4 p-2">
<div className="mt-1 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-purple-500/10 text-purple-400 ring-1 ring-purple-500/20">
{feature.icon ? (
<span className="h-5 w-5">{feature.icon}</span>
) : (
<ArrowRight className="h-5 w-5" />
)}
</div>
<div>
<h3 className="text-lg font-semibold">{feature.title}</h3>
<p className="mt-1 text-sm text-gray-400">
{feature.description}
</p>
</div>
</div>
</motion.div>
))}
</div>
</div>
<motion.div
className="relative lg:mt-0"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
duration: 0.5,
ease: "easeOut"
}}
>
<motion.div
className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-purple-600 to-purple-900 sm:p-8 p-4"
transition={{ duration: 0.3 }}
>
<MeshPattern />
<div className="relative rounded-xl overflow-hidden h-[600px]">
<AnimatePresence initial={false} custom={direction}>
<motion.div
key={activeFeature}
custom={direction}
variants={imageVariants}
initial="enter"
animate="center"
exit="exit"
transition={{
y: { type: "spring", stiffness: 300, damping: 30 },
opacity: { duration: 0.2 },
scale: { duration: 0.4 },
}}
className="absolute inset-0 w-full h-full"
>
<div className="relative w-full h-full">
<Image
src={features[activeFeature]?.image || previewImage}
alt={features[activeFeature]?.title || "Preview"}
fill
className="object-cover object-top rounded"
priority
/>
</div>
</motion.div>
</AnimatePresence>
</div>
</motion.div>
</motion.div>
</div>
</div>
</div>
)
}
export default PulesFeatures;
Props
Name | Type | Default | Description |
---|---|---|---|
category* | string | - | Category label displayed above the title. |
title* | string | - | Main heading text for the features section. |
features* | Feature[] | - | Array of feature objects containing title, description, icon (optional), and image (optional). |
learnMoreLink | string | - | URL for the learn more button. |
learnMoreText | string | - | Text displayed on the learn more button. |
previewImage | string | - | Default preview image URL if feature images are not provided. |