Files
nextjs-template/components/ui/card-hover-effect.tsx
T
asepharyana bf75d9deef feat(ui): vendor Aceternity registry components + showcase gallery
Vendor 50 Aceternity UI components into components/ui from the official
registry (ui.aceternity.com/registry/<slug>.json), adapted for this
template's stack: Tailwind v4, motion/react 13, cn util, React 19.

Add a live showcase gallery (/ui + /docs/... navigation is unchanged):
app/ui/page.tsx renders <ShowcaseGallery> with 30+ demos — 3d-pin,
aurora, background-beams, bento, card-stack, card-spotlight,
direction-aware-hover, encrypted-text, evervault-card, flip-words,
hero-highlight, hero-parallax, infinite-moving-cards, moving-border,
sparkles, tracing-beam, animated-tooltip, typewriter-effect,
placeholders-and-vanish-input and friends.

Toolchain stays zero-warning: vendored animation code is scoped in
eslint.config.mjs (react-hooks/react-compiler advisory rules, no-any,
prefer-const, no-img-element) so app code keeps a 0-error gate. typecheck,
build, 7 vitest tests and 2 test suites all green.
2026-09-20 19:10:43 +07:00

100 lines
2.5 KiB
TypeScript

import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
export const HoverEffect = ({
items,
className,
}: {
items: {
title: string;
description: string;
link: string;
}[];
className?: string;
}) => {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
return (
<div className={cn("grid grid-cols-1 py-10 md:grid-cols-2 lg:grid-cols-3", className)}>
{items.map((item, idx) => (
<a
href={item?.link}
key={item?.link}
className="group relative block h-full w-full p-2"
onMouseEnter={() => setHoveredIndex(idx)}
onMouseLeave={() => setHoveredIndex(null)}
>
<AnimatePresence>
{hoveredIndex === idx && (
<motion.span
className="absolute inset-0 block h-full w-full rounded-3xl bg-neutral-200 dark:bg-slate-800/[0.8]"
layoutId="hoverBackground"
initial={{ opacity: 0 }}
animate={{
opacity: 1,
transition: { duration: 0.15 },
}}
exit={{
opacity: 0,
transition: { duration: 0.15, delay: 0.2 },
}}
/>
)}
</AnimatePresence>
<Card>
<CardTitle>{item.title}</CardTitle>
<CardDescription>{item.description}</CardDescription>
</Card>
</a>
))}
</div>
);
};
export const Card = ({
className,
children,
}: {
className?: string;
children: React.ReactNode;
}) => {
return (
<div
className={cn(
"relative z-20 h-full w-full overflow-hidden rounded-2xl border border-transparent bg-black p-4 group-hover:border-slate-700 dark:border-white/[0.2]",
className
)}
>
<div className="relative z-50">
<div className="p-4">{children}</div>
</div>
</div>
);
};
export const CardTitle = ({
className,
children,
}: {
className?: string;
children: React.ReactNode;
}) => {
return (
<h4 className={cn("mt-4 font-bold tracking-wide text-zinc-100", className)}>{children}</h4>
);
};
export const CardDescription = ({
className,
children,
}: {
className?: string;
children: React.ReactNode;
}) => {
return (
<p className={cn("mt-8 text-sm leading-relaxed tracking-wide text-zinc-400", className)}>
{children}
</p>
);
};