Profile Peek

PreviousNext

Hover to peek profile with smooth reveal, right from the image.

Docs
paceuiui

Preview

Loading preview…
gsap/profile-peek.tsx
"use client";

import { ComponentProps, ReactNode, useRef } from "react";

import { useGSAP } from "@gsap/react";
import gsap from "gsap";

import { cn } from "@/lib/utils";

type HoverProfileCardProps = Omit<ComponentProps<"div">, "content"> & {
    trigger?: ReactNode;
    content?: ReactNode;
};

export const ProfilePeek = ({ trigger, content, className, ...props }: HoverProfileCardProps) => {
    const componentRef = useRef<HTMLDivElement>(null);
    const cardRef = useRef<HTMLDivElement>(null);
    const triggerRef = useRef<HTMLDivElement>(null);
    const contentRef = useRef<HTMLDivElement>(null);

    useGSAP(
        () => {
            const component = componentRef.current;
            const card = cardRef.current;
            const content = contentRef.current;
            const trigger = triggerRef.current;
            if (!component || !card || !content || !trigger) return;

            const timeline = gsap.timeline({
                paused: true,
                defaults: { ease: "power2.inOut", duration: 0.4 },
            });

            gsap.set(card, {
                opacity: 0,
                scale: 0.9,
                y: -40,
                rotationX: -25,
                rotationY: 25,
                transformOrigin: "top left",
            });

            gsap.set(content, { y: -10, opacity: 0, display: "none" });

            timeline
                .to(content, {
                    display: "block",
                    duration: 0,
                })
                .to(component, {
                    zIndex: 10,
                    duration: 0,
                })
                .to(card, {
                    y: 0,
                    rotationX: 0,
                    rotationY: 0,
                    scale: 1,
                    left: -16,
                    top: -16,
                    opacity: 1,
                    duration: 0.6,
                    ease: "back.out(3)",
                })
                .to(
                    triggerRef.current,
                    {
                        scale: 1.1,
                        duration: 0.4,
                    },
                    "<",
                )
                .to(
                    content,
                    {
                        x: 0,
                        y: 0,
                        opacity: 1,
                        duration: 0.3,
                    },
                    "-=0.4",
                );

            const onMouseEnter = () => {
                timeline.play();
            };

            const onMouseLeave = () => {
                timeline.reverse();
            };

            trigger.addEventListener("mouseenter", onMouseEnter);
            component.addEventListener("mouseleave", onMouseLeave);

            return () => {
                trigger.removeEventListener("mouseenter", onMouseEnter);
                component.removeEventListener("mouseleave", onMouseLeave);
            };
        },
        { scope: componentRef },
    );

    return (
        <div {...props} ref={componentRef} className={cn("relative z-0 [perspective:800px]", className)}>
            <div ref={cardRef} className="absolute [transform-style:preserve-3d]">
                <div ref={contentRef} style={{ display: "none" }}>
                    {content}
                </div>
            </div>

            <div className="relative" ref={triggerRef}>
                {trigger}
            </div>
        </div>
    );
};

Installation

npx shadcn@latest add @paceui/profile-peek

Usage

import { ProfilePeek } from "@/components/ui/profile-peek"
<ProfilePeek />