Vector Motion
Finance

Expense Breakdown Card - Cost Analysis

Analyze expense distribution across categories. Monitor spending by department and identify cost optimization opportunities.

Finance Expense Breakdown Card

The Finance Expense Breakdown Card categorizes and visualizes expenses, helping teams understand spending patterns and identify cost optimization opportunities.

Preview

Installation

npx shadcn@latest add https://vectormotion.vercel.app/registry/finance-expense-breakdown-card.json
Finance Expense Breakdown Card
"use client"import React, { useState } from 'react';import { PieChart as PieChartIcon, TrendingUp } from 'lucide-react';import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts';import { motion, AnimatePresence } from 'motion/react';import { clsx, type ClassValue } from "clsx"import { twMerge } from "tailwind-merge"function cn(...inputs: ClassValue[]) {    return twMerge(clsx(inputs))}interface ExpenseCategory {    name: string;    value: number;    color: string;    [key: string]: any;}interface ExpenseBreakdownCardProps {    isInteractive?: boolean;    className?: string;    title?: string;    totalSpend?: string;    spendLabel?: string;    data?: ExpenseCategory[];}const DEFAULT_TITLE = "Expense Breakdown";const DEFAULT_TOTAL_SPEND = "$2.5k";const DEFAULT_SPEND_LABEL = "Total Spend";const DEFAULT_DATA: ExpenseCategory[] = [    { name: 'Housing', value: 1200, color: '#10b981' }, // emerald-500    { name: 'Food', value: 600, color: '#3b82f6' },    // blue-500    { name: 'Transport', value: 400, color: '#f59e0b' },// amber-500    { name: 'Utilities', value: 300, color: '#ef4444' },// red-500];export const ExpenseBreakdownCard: React.FC<ExpenseBreakdownCardProps> = ({    isInteractive = true,    className = "",    title = DEFAULT_TITLE,    totalSpend = DEFAULT_TOTAL_SPEND,    spendLabel = DEFAULT_SPEND_LABEL,    data = DEFAULT_DATA,}) => {    const [activeIndex, setActiveIndex] = useState<number | null>(null);    const index = 33;    return (        <motion.div            layoutId={isInteractive ? `card-${index}-${title}` : undefined}            transition={{ duration: 0.4, ease: "easeOut" }}            className={cn(                "relative overflow-hidden rounded-xl border border-border bg-card text-card-foreground p-5 shadow-sm transition-all flex flex-col h-full group",                isInteractive ? "cursor-pointer hover:border-primary/50 hover:shadow-md" : "",                className            )}        >            <div className="mb-2 flex items-start justify-between relative z-10">                <div>                    <h3 className="font-semibold text-lg text-foreground">                        {title}                    </h3>                    <div className="flex items-center gap-2 mt-1">                        <span className="text-2xl font-bold text-foreground">{totalSpend}</span>                        <span className="text-xs text-muted-foreground">{spendLabel}</span>                    </div>                </div>                <div className="rounded-lg bg-emerald-500/10 p-2 text-emerald-500">                    <PieChartIcon className="h-5 w-5" />                </div>            </div>            <div className="relative z-10 flex-1 flex flex-col sm:flex-row items-center gap-4">                <div className="h-[140px] w-[140px] relative flex-shrink-0">                    <ResponsiveContainer width="100%" height="100%">                        <PieChart>                            <Pie                                data={data}                                cx="50%"                                cy="50%"                                innerRadius={40}                                outerRadius={60}                                paddingAngle={4}                                dataKey="value"                                onMouseEnter={(_, index) => setActiveIndex(index)}                                onMouseLeave={() => setActiveIndex(null)}                                stroke="none"                            >                                {data.map((entry, index) => (                                    <Cell                                        key={`cell-${index}`}                                        fill={entry.color}                                        style={{                                            filter: activeIndex === index ? 'brightness(1.1)' : 'none',                                            transform: activeIndex === index ? 'scale(1.05)' : 'scale(1)',                                            transformOrigin: 'center',                                            transition: 'transform 0.2s ease-out'                                        }}                                    />                                ))}                            </Pie>                            <Tooltip                                cursor={false}                                content={() => null}                            />                        </PieChart>                    </ResponsiveContainer>                    <div className="absolute inset-0 flex items-center justify-center pointer-events-none">                        <span className="text-sm font-bold text-foreground">{data.length} Cats</span>                    </div>                </div>                <div className="flex-1 w-full space-y-2">                    <AnimatePresence>                        {data.map((item, index) => (                            <motion.div                                key={item.name}                                initial={{ opacity: 0, x: 10 }}                                animate={{ opacity: 1, x: 0 }}                                transition={{ delay: index * 0.05 }}                                className={`flex items-center justify-between text-xs p-1.5 rounded-md cursor-pointer transition-colors ${activeIndex === index ? 'bg-zinc-100 dark:bg-zinc-800' : ''}`}                                onMouseEnter={() => setActiveIndex(index)}                                onMouseLeave={() => setActiveIndex(null)}                            >                                <div className="flex items-center gap-2">                                    <div                                        className="h-2 w-2 rounded-full"                                        style={{ backgroundColor: item.color }}                                    />                                    <span className="text-zinc-600 dark:text-zinc-300">{item.name}</span>                                </div>                                <span className="font-medium text-foreground">${item.value}</span>                            </motion.div>                        ))}                    </AnimatePresence>                </div>            </div>            <div className="z-10 mt-2 flex items-center justify-between text-xs pt-2 border-t border-border">                <div className="flex items-center gap-1.5 text-muted-foreground">                    <span>Top: Housing (48%)</span>                </div>            </div>        </motion.div>    );};

Props

Prop

Type