Docs
Interactive Card

Interactive Card

A card component that can be used to display multiple items with animation.

Game Cube

Price: $123

basic image

Game Cube

A game console developed and manufactured by Nintendo.
components/snippet/interactive-card.tsx
"use client"
 
import * as React from "react"
import { AnimatePresence, MotionProps, motion } from "framer-motion"
 
import { cn } from "@/lib/utils"
 
interface InteractiveCardProps
  extends Omit<MotionProps, "onAnimationStart" | "onAnimationComplete"> {
  className?: string
  topContent?: React.ReactNode
  middleContent?: React.ReactNode
  topAnimateContent?: React.ReactNode
  bottomContent?: React.ReactNode
}
 
const InteractiveCardHeader = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ children, ...props }, ref) => (
  <div ref={ref} {...props}>
    {children}
  </div>
))
InteractiveCardHeader.displayName = "InteractiveCardHeader"
 
interface InteractiveCardContentProps extends React.HTMLAttributes<HTMLDivElement> {
  isHovered: boolean
}
const InteractiveCardContent = React.forwardRef<
  HTMLDivElement,
  InteractiveCardContentProps
>(({ isHovered, children, ...divProps }, ref) => {
  // Explicitly define motion props to avoid passing incompatible HTML attributes
  const motionProps: MotionProps = {
    initial: { opacity: 0, height: 0 },
    animate: isHovered
      ? { opacity: 1, height: 194 }
      : { opacity: 1, height: 38 },
    transition: { duration: 0.3, delay: 0.1, ease: "circIn" },
  }
 
  return (
    <motion.div
      key="interactive-card-content"
      ref={ref}
      {...motionProps}
      className={divProps.className}
    >
      {children}
    </motion.div>
  )
})
InteractiveCardContent.displayName = "ShiftCardContent"
 
const InteractiveCard = React.forwardRef<HTMLDivElement, InteractiveCardProps>(
  (
    {
      className,
      topContent,
      topAnimateContent,
      middleContent,
      bottomContent,
      ...props
    },
    ref
  ) => {
    const [isHovered, setHovered] = React.useState(false)
    const handleMouseEnter = () => setHovered(true)
    const handleMouseLeave = () => setHovered(false)
    const handleTapStart = () => setHovered(true)
    const handleTapCancel = () => setHovered(false)
    const handleTap = () => setHovered(false)
 
    return (
      <motion.div
        ref={ref}
        className={cn(
          "min-h-[240px] w-[280px] md:min-h-[300px] md:w-[300px]",
          " group relative flex flex-col items-center justify-between overflow-hidden rounded-xl  p-3 text-sm ",
          " hover:cursor-pointer bg-card  ",
          "shadow-[0px_1px_1px_0px_rgba(0,0,0,0.05),0px_1px_1px_0px_rgba(255,252,240,0.5)_inset,0px_0px_0px_1px_hsla(0,0%,100%,0.1)_inset,0px_0px_1px_0px_rgba(28,27,26,0.5)]",
          /* 
          Applies a multi-layer shadow effect in dark mode:
          1. Two subtle white inset shadows: one horizontal at 1px height, and one around the entire border at 1px width, both with a very light opacity of 0.03, creating a slight inner glow.
          2. A series of black shadows:
              - A basic 1px black border shadow with 10% opacity to subtly outline the element.
              - Three progressively larger diffuse shadows at 2px, 4px, and 8px, each with 10% opacity, creating a depth effect that enhances the element's visual separation from the background.
          */
          "dark:shadow-[0_1px_0_0_rgba(255,255,255,0.03)_inset,0_0_0_1px_rgba(255,255,255,0.03)_inset,0_0_0_1px_rgba(0,0,0,0.1),0_2px_2px_0_rgba(0,0,0,0.1),0_4px_4px_0_rgba(0,0,0,0.1),0_8px_8px_0_rgba(0,0,0,0.1)]",
          className
        )}
        initial={{ y: 20, opacity: 0 }}
        animate={{ y: 0, opacity: 1 }}
        whileHover={{ scale: 1.02 }}
        onMouseEnter={handleMouseEnter}
        onMouseLeave={handleMouseLeave}
        onTapStart={handleTapStart}
        onTapCancel={handleTapCancel}
        onTap={handleTap}
        {...props}
      >
        <InteractiveCardHeader className="flex h-[46px] w-full flex-col relative text-primary-foreground ">
          <div className=" w-full">
            {topContent}
 
            <AnimatePresence>
              {isHovered ? <>{topAnimateContent}</> : null}
            </AnimatePresence>
          </div>
        </InteractiveCardHeader>
 
        <div className="pb-12 ">
          <AnimatePresence>
            {!isHovered ? <>{middleContent}</> : null}
          </AnimatePresence>
        </div>
 
        <InteractiveCardContent
          isHovered={isHovered}
          className="absolute bottom-0 left-0 right-0 flex  flex-col gap-4  rounded-xl  "
        >
          <motion.div className="flex w-full flex-col gap-1  ">
            {bottomContent}
          </motion.div>
        </InteractiveCardContent>
      </motion.div>
    )
  }
)
 
InteractiveCard.displayName = "InteractiveCard"
 
export { InteractiveCard, InteractiveCardHeader, InteractiveCardContent}