Linear

Linear's cards animation when clicked

  • Some title here

  • Some title here

  • Some title here

Installation

Run the following command

It will create a new file linear.tsx inside the components/cards/linear.tsx directory.

mkdir -p components/cards && touch components/cards/linear.tsx

Paste the code

Open the newly created file and paste the following code:

"use client";
 
import { AnimatePresence, motion } from "framer-motion";
import { Plus } from "lucide-react";
import { ReactNode, useState } from "react";
 
export const CARDS: Card[] = [
  {
    id: 1,
    title: "Some title here",
    description:
      "Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptatibus doloremque optio recusandae dolorem ipsa odit perferendis, repellat rem corporis sit soluta beatae neque illum molestias ex quidem delectus adipisci. Laboriosam!",
  },
  {
    id: 2,
    title: "Some title here",
    description:
      "Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptatibus doloremque optio recusandae dolorem ipsa odit perferendis, repellat rem corporis sit soluta beatae neque illum molestias ex quidem delectus adipisci. Laboriosam!",
  },
  {
    id: 3,
    title: "Some title here",
    description:
      "Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptatibus doloremque optio recusandae dolorem ipsa odit perferendis, repellat rem corporis sit soluta beatae neque illum molestias ex quidem delectus adipisci. Laboriosam!",
  },
];
 
type Card = {
  id: number;
  title: string;
  description: string;
};
 
const LinearCards = () => {
  const [selectedCard, setSelectedCard] = useState<Card | null>(null);
 
  const [isHorizontal, setIsHorizontal] = useState(true);
 
  return (
    <>
      <main className="h-full center relative w-full">
        <div className="absolute top-10 right-10 flex items-center gap-2 p-2 border border-border rounded-full">
          <div
            className="h-6 w-8  flex items-center justify-center gap-1 cursor-pointer"
            onClick={() => setIsHorizontal(false)}
          >
            {Array.from({ length: 3 }).map((_, index) => (
              <span key={index} className="h-full w-1 bg-primary" />
            ))}
          </div>
          <div
            className="h-8 w-8 flex items-center justify-center flex-col gap-1 cursor-pointer"
            onClick={() => setIsHorizontal(true)}
          >
            {Array.from({ length: 3 }).map((_, index) => (
              <span key={index} className="h-1 w-full bg-primary" />
            ))}
          </div>
        </div>
        <motion.ul
          className={`flex flex-wrap gap-4 justify-center items-center size-full ${
            isHorizontal ? "flex-row" : "flex-col"
          }`}
          layout
          transition={{ duration: 0.5, ease: "easeInOut" }}
        >
          {CARDS.map((card) => (
            <Card
              key={card.id}
              card={card}
              onClick={() => setSelectedCard(card)}
            />
          ))}
        </motion.ul>
      </main>
      <Modal card={selectedCard} onClick={() => setSelectedCard(null)} />
    </>
  );
};
 
function Card(props: { card: Card; onClick: () => void }) {
  return (
    <motion.li
      key={props.card.title}
      className="text-white h-60 w-64 py-8 px-7 rounded-[30px] bg-black/20 text-[21px] hover:brightness-125 flex justify-end flex-col text-balance cursor-pointer"
      layoutId={`card-${props.card.id}`}
      onClick={props.onClick}
    >
      <div className="flex justify-between items-center">
        <motion.p
          className="text-balance"
          layoutId={`heading-${props.card.id}`}
        >
          {props.card.title}
        </motion.p>
        <Button>
          <Plus className="size-4" />
        </Button>
      </div>
      <motion.span layoutId={`description-${props.card.id}`} />
    </motion.li>
  );
}
 
function Modal(props: { card: Card | null; onClick: () => void }) {
  return (
    <>
      <AnimatePresence>
        {!!props.card && (
          <motion.div
            className="fixed inset-0 flex items-center justify-center z-[1000000]"
            initial={{ backdropFilter: "blur(0px)" }}
            animate={{ backdropFilter: "blur(32px)" }}
            exit={{ backdropFilter: "blur(0px)" }}
            transition={{ duration: 0.3, ease: "easeOut" }}
          />
        )}
      </AnimatePresence>
 
      <AnimatePresence>
        {!!props.card && (
          <motion.div
            className="fixed inset-0 z-10 flex flex-col justify-center"
            onClick={props.onClick}
          >
            <motion.div
              className="p-8 max-w-[500px] mx-auto h-[400px] rounded-[30px] relative overflow-hidden flex items-center justify-center flex-col bg-black/20"
              layoutId={`card-${props.card.id}`}
            >
              <div className="max-w-xl mx-auto">
                <motion.p
                  className="text-white font-medium text-balance"
                  layoutId={`heading-${props.card.id}`}
                >
                  {props.card.title}
                </motion.p>
                <motion.p
                  className="text-[#969799] font-medium text-[15px] mt-8"
                  layoutId={`description-${props.card.id}`}
                >
                  {props.card.description}
                </motion.p>
              </div>
              <Button className="absolute top-8 right-8">
                <Plus className="rotate-45" />
              </Button>
            </motion.div>
          </motion.div>
        )}
      </AnimatePresence>
    </>
  );
}
 
export function Button(props: { className?: string; children: ReactNode }) {
  return (
    <button
      className={`p-2 border-2 border-[#161616] rounded-full hover:bg-[#161616] flex items-center justify-center text-[#9C9BA1] hover:text-white ${
        props.className || ""
      }`.trim()}
    >
      {props.children}
    </button>
  );
}
 
export default LinearCards;

Credits

Built by Bossadi Zenith