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.jsonFinance 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