# Animated Cards

In this craft, we will create a testimonial card that flips on clicks and shows the testimonial.

Published: 05rd February 2025

This is just a extenstion of my previous craft [3D Flip Card](https://faisalhusa.in/craft/3D-flippy-card) where we created a 3D flip card. In this craft we will create a testimonial card that flips on clicks and shows the testimonial.

Here is code for the component -

```tsx showLineNumbers{}
'use client'
import { useState } from 'react'
import { AnimatePresence, motion } from 'motion/react'
import { ArrowBigLeft, ArrowBigRight } from 'lucide-react'

const GRID_SIZE = 10
const COUNT = GRID_SIZE * GRID_SIZE
const testimonials = [
  {
    quote:
      "The attention to detail and innovative features have completely transformed our workflow. This is exactly what we've been looking for.",
    name: 'Sarah Chen',
    designation: 'Product Manager at TechFlow',
    src:
      'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?q=80&w=3560&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    quote:
      "Implementation was seamless and the results exceeded our expectations. The platform's flexibility is remarkable.",
    name: 'Michael Rodriguez',
    designation: 'CTO at InnovateSphere',
    src:
      'https://images.unsplash.com/photo-1560250097-0b93528c311a?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8cHJvZmVzc2lvbmFsJTIwbWFufGVufDB8fDB8fHww',
  },
  {
    quote:
      "This solution has significantly improved our team's productivity. The intuitive interface makes complex tasks simple.",
    name: 'Emily Watson',
    designation: 'Operations Director at CloudScale',

    src:
      'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8cHJvZmVzc2lvbmFsJTIwd29tYW58ZW58MHx8MHx8fDA%3D',
  },
  {
    quote:
      "Outstanding support and robust features. It's rare to find a product that delivers on all its promises.",
    name: 'James Kim',
    designation: 'Engineering Lead at DataPro',
    src:
      'https://images.unsplash.com/photo-1636041293178-808a6762ab39?q=80&w=3464&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
  {
    quote:
      'The scalability and performance have been game-changing for our organization. Highly recommend to any growing business.',
    name: 'Lisa Thompson',
    designation: 'VP of Technology at FutureNet',
    src:
      'https://images.unsplash.com/photo-1624561172888-ac93c696e10c?q=80&w=2592&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
  },
]

const Testimonial3DCard = () => {
  const [flipped, setFlipped] = useState(false)
  const [clickPosition, setClickPosition] = useState({ x: 0, y: 0 })
  const [isAnimating, setIsAnimating] = useState(false)
  const [currentIndex, setCurrentIndex] = useState(0)
  const [nextIndex, setNextIndex] = useState(1)

  const getManhattanDistance = (
    x1: number,
    y1: number,
    x2: number,
    y2: number,
  ) => {
    return Math.abs(x2 - x1) + Math.abs(y2 - y1)
  }

  const maxDistance = getManhattanDistance(0, 0, GRID_SIZE - 1, GRID_SIZE - 1)
  const totalAnimationDuration = maxDistance * 0.1 + 0.5

  const getRandomGridPosition = () => ({
    x: Math.floor(Math.random() * GRID_SIZE),
    y: Math.floor(Math.random() * GRID_SIZE),
  })

  const triggerFlip = (randomPosition?: { x: number; y: number }) => {
    const position = randomPosition || getRandomGridPosition()

    setClickPosition(position)
    setFlipped((prev) => !prev)
    setIsAnimating(true)

    setTimeout(() => {
      if (!flipped) {
        setCurrentIndex((pre) => (pre + 2) % testimonials.length)
      } else {
        setNextIndex((pre) => (pre + 2) % testimonials.length)
      }
      setIsAnimating(false)
    }, totalAnimationDuration * 1000)
  }

  const handleNextClick = () => {
    if (isAnimating) return
    triggerFlip()
  }

  const handlePrevClick = () => {
    if (isAnimating) return
    triggerFlip()
  }

  return (
    <div
      className="bg-black ring-inset ring-2 ring-zinc-800 rounded-2xl   items-center justify-center w-full grid md:grid-cols-2 grid-cols-1  p-5 "
      style={
        {
          '--current-image': `url(${testimonials[currentIndex].src})`,
          '--next-image': `url(${testimonials[nextIndex].src})`,
        } as React.CSSProperties
      }
    >
      <div className="flex flex-col items-center justify-center px-10 max-w-xl">
        <AnimatePresence mode="wait">
          <motion.div className=" text-white w-full h-40 ">
            {flipped ? (
              <motion.div
                key="next"
                initial={{ y: 20, opacity: 0, filter: 'blur(20px)' }}
                animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
                exit={{ y: -20, opacity: 0, filter: 'blur(20px)' }}
                transition={{
                  ease: 'easeInOut',
                  duration: totalAnimationDuration * 0.75,
                }}
                className="flex flex-col items-start space-y-4"
              >
                <div className="flex flex-col items-start">
                  <span className="text-2xl font-bold">
                    {testimonials[nextIndex].name}
                  </span>
                  <span className="text-xm text-gray-400 font-light">
                    {testimonials[nextIndex].designation}
                  </span>
                </div>
                <span className="text-sm">{testimonials[nextIndex].quote}</span>
              </motion.div>
            ) : (
              <motion.div
                key="current"
                initial={{ y: 20, opacity: 0, filter: 'blur(20px)' }}
                animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
                exit={{ y: -20, opacity: 0, filter: 'blur(20px)' }}
                transition={{
                  ease: 'easeInOut',
                  duration: totalAnimationDuration * 0.75,
                }}
                className="flex flex-col items-start space-y-4"
              >
                <div className="flex flex-col items-start">
                  <span className="text-2xl font-bold">
                    {testimonials[currentIndex].name}
                  </span>
                  <span className="text-xm text-gray-400 font-light">
                    {testimonials[currentIndex].designation}
                  </span>
                </div>
                <span className="text-sm">
                  {testimonials[currentIndex].quote}
                </span>
              </motion.div>
            )}
          </motion.div>
        </AnimatePresence>
        <div className="flex gap-4 mt-10 w-full z-50 ">
          <button
            onClick={handlePrevClick}
            disabled={isAnimating}
            className="size-8 rounded-full bg-gray-100 dark:bg-neutral-800 flex items-center justify-center group/button disabled:opacity-50"
          >
            <ArrowBigLeft className=" size-6 text-black dark:text-neutral-400 group-hover/button:rotate-12 transition-transform duration-300" />
          </button>
          <button
            onClick={handleNextClick}
            disabled={isAnimating}
            className="size-8 rounded-full bg-gray-100 dark:bg-neutral-800 flex items-center justify-center group/button disabled:opacity-50"
          >
            <ArrowBigRight className="size-6 text-black dark:text-neutral-400 group-hover/button:-rotate-12 transition-transform duration-300" />
          </button>
        </div>
      </div>

      <div
        className={`grid grid-cols-10 md:size-[300px] size-[300px] mx-auto  ${
          isAnimating ? 'pointer-events-none' : ''
        } `}
      >
        {Array.from({ length: COUNT }, (_, index) => {
          const x = index % GRID_SIZE
          const y = Math.floor(index / GRID_SIZE)

          return (
            <motion.div
              key={index}
              className={`relative w-full h-full cursor-pointer [transform-style:preserve-3d] transition-transform duration-500`}
              initial="initial"
              animate={{
                rotateX: flipped ? 180 : 0,
                transition: {
                  rotateX: {
                    duration: 0.1,
                    delay:
                      getManhattanDistance(
                        clickPosition.x,
                        clickPosition.y,
                        x,
                        y,
                      ) * 0.1,
                  },
                },
              }}
              style={
                {
                  '--x': x,
                  '--y': y,
                  '--grid-size': GRID_SIZE,
                } as React.CSSProperties
              }
            >
              <div
                className="absolute w-full h-full bg-blue-500 text-center flex items-center justify-center text-xs md:text-base [backface-visibility:hidden] [transform:rotateY(0deg)]"
                style={{
                  backgroundImage: 'var(--current-image)',
                  backgroundPosition: `calc(var(--x, 0) * -100%) calc(var(--y, 0) * -100%)`,
                  backgroundSize: `calc(var(--grid-size, 10) * 100%)`,
                }}
              />
              <div
                className="absolute w-full h-full bg-red-500 text-center flex items-center justify-center text-xs md:text-base [backface-visibility:hidden] [transform:rotateX(180deg)]"
                style={{
                  backgroundImage: 'var(--next-image)',
                  backgroundPosition: `calc(var(--x, 0) * -100%) calc(var(--y, 0) * -100%)`,
                  backgroundSize: `calc(var(--grid-size, 10) * 100%)`,
                }}
              />
            </motion.div>
          )
        })}
      </div>
    </div>
  )
}

export default Testimonial3DCard

```

---

## Resources and Important Links

1. Link to my Craft. [Click here](https://faisalhusa.in/craft/3D-flippy-card)

Original page: https://faisalhusa.in/craft/animated-testimonial
