Skip to content
Arc
Book a call

Showreel

npx shadcn@latest add @arc/showreel
"use client";

import { AnimatePresence, motion } from "motion/react";
import Image from "next/image";
import Link from "next/link";
import type React from "react";
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";

const IMAGE_LINKS = [
  "https://494510hkri.ufs.sh/f/3d9AyaVNUM8w36URyDVNUM8wsF1VBZOHcvIdTi9DhgQ7Rpmu",
  "https://494510hkri.ufs.sh/f/3d9AyaVNUM8wtc1hUFMYZjvNFCAUWr1ncwPQR7T4hm8pI5kV",
  "https://494510hkri.ufs.sh/f/3d9AyaVNUM8wOqI2ETkCmxdvfqiDUQANc63RleuKp0a1ygHt",
  "https://494510hkri.ufs.sh/f/3d9AyaVNUM8wmo2TwcaI0TuRiKvWN36gjOJDhEzkplXCmeAY",
  "https://494510hkri.ufs.sh/f/3d9AyaVNUM8wEwPxiMqhFvnVqJMwHyLro5RN9sWceEY6Dgpl",
  "https://494510hkri.ufs.sh/f/3d9AyaVNUM8wCLBrHN02g0NPOiTH8EVzMJdeZDkqQ5hlp9uf",
  "https://494510hkri.ufs.sh/f/3d9AyaVNUM8wrx0Z3MnFtc80pzOYxZ3yevLma4hXGJluCH9i",
  "https://494510hkri.ufs.sh/f/3d9AyaVNUM8wBIBmaGlvQ6EnK4ZYi1eOC2y9IVwq0JdcbWBS",
  "https://494510hkri.ufs.sh/f/3d9AyaVNUM8w1Rsybv6xglOBVtHY5mXKeI3TMDawiLuErfNZ",
  "https://494510hkri.ufs.sh/f/3d9AyaVNUM8wzcVZxM1pfMwZhC4cjWIA8mLPTOJias9FulD3",
  "https://494510hkri.ufs.sh/f/3d9AyaVNUM8w4UQS1ed5YfAKtoRjdOaDVs1c9Fnql80rEx3B",
  "https://494510hkri.ufs.sh/f/3d9AyaVNUM8wdQY0JGCEtBUvonjplAG039xzaROYHSZCwWbN",
  "https://494510hkri.ufs.sh/f/3d9AyaVNUM8w3ko5lDDVNUM8wsF1VBZOHcvIdTi9DhgQ7Rpm",
];

type ShowreelProps = {
  compact?: boolean;
  link?: string | null;
  linkTitle?: string;
  buttonTitle?: string;
};

const Showreel = ({
  compact = false,
  link = "/works",
  linkTitle = "View works",
  buttonTitle = "Preview",
}: ShowreelProps) => {
  const [currentIndex, setCurrentIndex] = useState(0);
  const [isHovering, setIsHovering] = useState(false);
  const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
  const [isMounted, setIsMounted] = useState(false);
  const containerRef = useRef<HTMLElement | null>(null);

  useEffect(() => {
    if (isHovering) return;
    const interval = setInterval(() => {
      setCurrentIndex((prev) => (prev + 1) % IMAGE_LINKS.length);
    }, 1500);

    return () => clearInterval(interval);
  }, [isHovering]);

  useEffect(() => {
    setIsMounted(true);
  }, []);

  useEffect(() => {
    if (!isHovering) return;

    const handleWindowMouseMove = (e: MouseEvent) => {
      setMousePos({ x: e.clientX, y: e.clientY });
      const el = containerRef.current;
      if (!el) return;
      const rect = el.getBoundingClientRect();
      const isInside =
        e.clientX >= rect.left &&
        e.clientX <= rect.right &&
        e.clientY >= rect.top &&
        e.clientY <= rect.bottom;
      if (!isInside) setIsHovering(false);
    };

    window.addEventListener("mousemove", handleWindowMouseMove);
    return () => window.removeEventListener("mousemove", handleWindowMouseMove);
  }, [isHovering]);

  const handleMouseMove = (e: React.MouseEvent) => {
    setMousePos({ x: e.clientX, y: e.clientY });
  };

  const handleMouseEnter = (e: React.MouseEvent<HTMLElement>) => {
    setMousePos({ x: e.clientX, y: e.clientY });
    setIsHovering(true);
  };

  const baseClassName = [
    "flex items-center justify-center bg-[#f9f9f9] relative",
    compact
      ? "w-full h-full p-4 md:p-6 lg:p-8 xl:p-10"
      : "h-96 md:h-[64rem] md:p-12 lg:p-16 xl:p-24",
  ]
    .filter(Boolean)
    .join(" ");
  const interactiveClassName =
    `${baseClassName} ${isHovering ? "cursor-none" : ""}`.trim();

  const content = (
    <div className="relative w-full h-full overflow-hidden">
      {IMAGE_LINKS.map((src, index) => (
        <Image
          key={src}
          src={src}
          alt={`Showreel image ${index + 1}`}
          fill
          sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 70vw"
          draggable={false}
          className={`object-contain ${
            index === currentIndex ? "opacity-100" : "opacity-0"
          }`}
          priority={index === 0}
        />
      ))}
    </div>
  );

  return (
    <>
      <motion.div
        initial={{ filter: "blur(10px)", opacity: 0, y: 20 }}
        animate={{ filter: "blur(0px)", opacity: 1, y: 0 }}
        transition={{ duration: 0.6, ease: "easeOut", delay: 0.6 }}
        className={`flex flex-col w-full will-change-[filter] backface-hidden ${
          compact ? "h-full p-0 mb-0" : "p-4 mb-16"
        }`}
      >
        {link ? (
          <Link
            href={link}
            ref={containerRef as React.RefObject<HTMLAnchorElement>}
            onMouseEnter={handleMouseEnter}
            onMouseLeave={() => setIsHovering(false)}
            onMouseMove={handleMouseMove}
            className={interactiveClassName}
          >
            {content}
          </Link>
        ) : (
          <button
            type="button"
            aria-label={buttonTitle}
            ref={containerRef as React.RefObject<HTMLButtonElement>}
            onMouseEnter={handleMouseEnter}
            onMouseLeave={() => setIsHovering(false)}
            onMouseMove={handleMouseMove}
            className={`appearance-none border-0 ${interactiveClassName}`}
          >
            {content}
          </button>
        )}
      </motion.div>

      {isMounted
        ? createPortal(
            <AnimatePresence>
              {isHovering && (
                <motion.div
                  initial={{ opacity: 0, scale: 0.6, filter: "blur(4px)" }}
                  animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
                  exit={{ opacity: 0, scale: 0.6, filter: "blur(4px)" }}
                  transition={{ duration: 0.2, ease: "easeOut" }}
                  className="pointer-events-none fixed left-0 top-0 z-50 flex items-center justify-center rounded-full bg-black px-5 py-2.5 text-sm font-medium text-white will-change-transform"
                  style={{
                    left: mousePos.x,
                    top: mousePos.y,
                    translate: "-50% -50%",
                  }}
                >
                  {link ? linkTitle : buttonTitle}
                </motion.div>
              )}
            </AnimatePresence>,
            document.body,
          )
        : null}
    </>
  );
};

export default Showreel;