Skip to content
Docs
Fund Widget

Fund Widget

This component is a financial dashboard-like UI element for showing the current status of the funds.

Loading...

Installation

CLI

pnpm dlx shadcn@latest add https://animata.design/r/widget/fund-widget.json

Manual

Install dependencies

npm install motion

Run the following command

It will create a new file fund-widget.tsx inside the components/animata/widget directory.

mkdir -p components/animata/widget && touch components/animata/widget/fund-widget.tsx

Paste the code

Open the newly created file and paste the following code:

import { AnimatePresence, motion, type PanInfo } from "motion/react";
import { useEffect, useState } from "react";
 
import { cn } from "@/lib/utils";
 
type Fund = {
  value: string;
  change: number;
  label: string;
};
 
interface FundWidgetProps {
  /**
   * The array which contains all the funds with their value, changes, and label.
   */
  funds: Fund[];
 
  /**
   * Class name for the background element.
   */
  backgroundClassName?: string;
 
  /**
   * Class name for the container element.
   */
  containerClassName?: string;
}
 
export default function FundWidget({
  funds = [
    { value: "2.7Cr", change: 12, label: "Stocks" },
    { value: "3.5Cr", change: -8, label: "Funds" },
    { value: "1.2Cr", change: 6, label: "Deposits" },
  ],
  backgroundClassName,
  containerClassName,
}: FundWidgetProps) {
  const len = funds.length;
 
  const [[activeDiv, direction], setDirection] = useState([0, 0]);
  const [dragDistance, setDragDistance] = useState(0);
 
  // Reset dragDistance after the drag ends to remove blur/rotate effects
  useEffect(() => {
    if (dragDistance !== 0) {
      const timer = setTimeout(() => {
        setDragDistance(0);
      }, 500);
      return () => clearTimeout(timer);
    }
  }, [dragDistance]);
 
  const sliderVariants = {
    incoming: (direction: number) => ({
      y: direction > 0 ? "100%" : "-100%",
      scale: 1.0,
      opacity: 0,
    }),
    active: { y: 0, scale: 1, opacity: 1 },
    exit: (direction: number) => ({
      y: direction > 0 ? "100%" : "-100%",
      scale: 1,
      opacity: 0.2,
    }),
  };
 
  const sliderTransition = {
    duration: 0.5,
    ease: [0.56, 0.03, 0.12, 1.04] as [number, number, number, number],
  };
 
  const swipeToAction = (direction: number) => {
    const newDiv = activeDiv + direction;
    if (newDiv < 0 || newDiv >= len) return;
 
    setDirection([newDiv, direction]);
  };
 
  const draghandler = (dragInfo: PanInfo) => {
    const dragDistanceY = dragInfo.offset.y;
    const swipeThreshold = 20;
 
    // Only swipe down if not at the first div (activeDiv !== 0)
    if (dragDistanceY > swipeThreshold) {
      swipeToAction(-1);
    }
    // Only swipe up if not at the last div (activeDiv !== len - 1)
    else if (dragDistanceY < -swipeThreshold) {
      swipeToAction(1);
    }
 
    setDragDistance(0);
  };
 
  const skipToDiv = (divId: number) => {
    let changeDirection = 1;
    if (divId > activeDiv) {
      changeDirection = 1;
    } else if (divId < activeDiv) {
      changeDirection = -1;
    }
    setDirection([divId, changeDirection]);
  };
 
  const blurValue = Math.min(Math.abs(dragDistance / 20), 10);
  const rotateYValue = Math.min(dragDistance / 10, 15);
 
  return (
    <div className={cn("relative flex size-52 items-center justify-center", containerClassName)}>
      <AnimatePresence initial={false}>
        <div className="flex h-72 w-64 origin-center scale-[0.72] flex-col items-center">
          <div className="z-20 flex h-64 w-72 overflow-clip rounded-[35px] bg-white px-6 pt-6 shadow-[0px_0px_10px_1px_#bec2bf]">
            <motion.div
              key={activeDiv}
              custom={direction}
              className="block h-full w-full"
              variants={sliderVariants}
              initial="incoming"
              animate="active"
              transition={sliderTransition}
              drag="y"
              dragConstraints={{ top: 0, bottom: 0 }}
              dragElastic={1}
              onDragEnd={(_, dragInfo) => draghandler(dragInfo)}
              onDrag={(_event, info) => setDragDistance(info.offset.y)}
              style={{
                filter: `blur(${blurValue}px)`,
                transform: `rotateY(${rotateYValue}deg)`,
              }}
            >
              <div className="h-56 w-56 bg-white">
                <h1 className="mb-3 text-6xl font-bold">{funds[activeDiv].value}</h1>
                {funds[activeDiv].change < 0 ? (
                  <h2 className="text-2xl font-bold text-red-500">
                    {funds[activeDiv].change}% &#8595;
                  </h2>
                ) : (
                  <h2 className="text-2xl font-bold text-green-500">
                    {funds[activeDiv].change}% &#8593;
                  </h2>
                )}
                <h1 className="mb-2 mt-14 text-4xl font-bold text-gray-500">
                  {funds[activeDiv].label}
                </h1>
              </div>
            </motion.div>
            <div className="-my-2 flex h-full w-10 flex-col items-center justify-center">
              {funds.map((_, index) => {
                return (
                  <motion.span
                    key={index}
                    className="my-1 h-2 w-2 rounded-full bg-black"
                    style={{ backgroundColor: index === activeDiv ? "black" : "gray" }}
                    initial={{ height: 8 }}
                    animate={{ height: index === activeDiv ? 30 : 8 }}
                    onClick={() => skipToDiv(index)}
                  ></motion.span>
                );
              })}
            </div>
          </div>
          <div className="z-10 -mt-8 h-10 w-[270px] rounded-b-[35px] bg-white shadow-[0px_0px_5px_1px_#bec2bf]"></div>
        </div>
      </AnimatePresence>
    </div>
  );
}

Credits

Built by Vishal