{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "cash-flow-chart", "type": "registry:ui", "title": "Accessible Cash Flow Chart", "description": "Interactive cash flow bar chart with keyboard focus, tooltips, and screen reader support", "dependencies": [ "framer-motion", "react" ], "files": [ { "path": "@uitripled/react-shadcn/src/components/data/charts/cash-flow-chart.tsx", "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion, useReducedMotion } from \"framer-motion\";\nimport { useMemo, useState } from \"react\";\n\ntype ChartView = \"monthly\" | \"yearly\";\n\ninterface CashFlowDatum {\n label: string;\n shortLabel: string;\n value: number;\n cashflow: number;\n inflow: number;\n dateRange: string;\n}\n\nconst monthlyData: CashFlowDatum[] = [\n {\n label: \"January 2026\",\n shortLabel: \"Jan\",\n value: 35000,\n cashflow: 35000,\n inflow: 12000,\n dateRange: \"January 2026\",\n },\n {\n label: \"February 2026\",\n shortLabel: \"Feb\",\n value: 28000,\n cashflow: 28000,\n inflow: 9500,\n dateRange: \"February 2026\",\n },\n {\n label: \"March 2026\",\n shortLabel: \"Mar\",\n value: 52000,\n cashflow: 52000,\n inflow: 14200,\n dateRange: \"March 2026\",\n },\n {\n label: \"April 2026\",\n shortLabel: \"Apr\",\n value: 31000,\n cashflow: 31000,\n inflow: 10100,\n dateRange: \"April 2026\",\n },\n {\n label: \"May 2026\",\n shortLabel: \"May\",\n value: 38000,\n cashflow: 38000,\n inflow: 11800,\n dateRange: \"May 2026\",\n },\n {\n label: \"June 2026\",\n shortLabel: \"Jun\",\n value: 22000,\n cashflow: 22000,\n inflow: 8200,\n dateRange: \"June 2026\",\n },\n {\n label: \"July 2026\",\n shortLabel: \"Jul\",\n value: 33000,\n cashflow: 33000,\n inflow: 9600,\n dateRange: \"July 2026\",\n },\n];\n\nconst yearlyData: CashFlowDatum[] = [\n {\n label: \"2020\",\n shortLabel: \"2020\",\n value: 265000,\n cashflow: 265000,\n inflow: 62000,\n dateRange: \"Fiscal Year 2020\",\n },\n {\n label: \"2021\",\n shortLabel: \"2021\",\n value: 302000,\n cashflow: 302000,\n inflow: 78500,\n dateRange: \"Fiscal Year 2021\",\n },\n {\n label: \"2022\",\n shortLabel: \"2022\",\n value: 358000,\n cashflow: 358000,\n inflow: 91200,\n dateRange: \"Fiscal Year 2022\",\n },\n {\n label: \"2023\",\n shortLabel: \"2023\",\n value: 402000,\n cashflow: 402000,\n inflow: 103500,\n dateRange: \"Fiscal Year 2023\",\n },\n {\n label: \"2024\",\n shortLabel: \"2024\",\n value: 446000,\n cashflow: 446000,\n inflow: 118000,\n dateRange: \"Fiscal Year 2024\",\n },\n {\n label: \"2025\",\n shortLabel: \"2025\",\n value: 489000,\n cashflow: 489000,\n inflow: 124300,\n dateRange: \"Fiscal Year 2025 (projected)\",\n },\n];\n\nconst formatCurrency = (value: number) =>\n new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: \"USD\",\n maximumFractionDigits: 0,\n }).format(value);\n\nexport function CashFlowChart() {\n const [hoveredBar, setHoveredBar] = useState(null);\n const [activeView, setActiveView] = useState(\"yearly\");\n const prefersReducedMotion = useReducedMotion();\n\n const displayData = useMemo(\n () => (activeView === \"monthly\" ? monthlyData : yearlyData),\n [activeView]\n );\n\n const maxValue = useMemo(\n () => Math.max(...displayData.map((d) => d.value)),\n [displayData]\n );\n\n const summaryText = useMemo(() => {\n if (!displayData.length) {\n return \"No cash flow data available.\";\n }\n\n const highest = displayData.reduce((prev, current) =>\n current.value > prev.value ? current : prev\n );\n const lowest = displayData.reduce((prev, current) =>\n current.value < prev.value ? current : prev\n );\n const first = displayData[0];\n const last = displayData[displayData.length - 1];\n\n if (activeView === \"monthly\") {\n return `Monthly cash flow from ${first.label} to ${last.label}. Highest cash flow in ${highest.label} at ${formatCurrency(\n highest.value\n )}. Lowest in ${lowest.label} at ${formatCurrency(lowest.value)}.`;\n }\n\n return `Yearly cash flow from ${first.label} to ${last.label}. Highest cash flow in ${highest.label} at ${formatCurrency(\n highest.value\n )}. Lowest in ${lowest.label} at ${formatCurrency(lowest.value)}.`;\n }, [activeView, displayData]);\n\n const chartTitleId = \"cashflow-chart-title\";\n const chartSummaryId = \"cashflow-chart-summary\";\n\n const handleViewChange = (view: ChartView) => {\n setActiveView(view);\n setHoveredBar(null);\n };\n\n const shouldAnimate = !prefersReducedMotion;\n\n return (\n \n
\n
\n \n Cash Flow\n

\n \n {formatCurrency(\n displayData.reduce((total, datum) => total + datum.cashflow, 0)\n )}\n \n

\n {summaryText}\n

\n
\n\n \n handleViewChange(\"monthly\")}\n className={cn(\n \"px-6 py-2 rounded-full text-sm font-medium transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-secondary\",\n activeView === \"monthly\"\n ? \"bg-primary text-primary-foreground shadow-lg\"\n : \"text-foreground hover:text-primary\"\n )}\n aria-pressed={activeView === \"monthly\"}\n aria-controls=\"cashflow-chart-bars\"\n >\n Monthly\n \n handleViewChange(\"yearly\")}\n className={cn(\n \"px-6 py-2 rounded-full text-sm font-medium transition-all duration-300 flex items-center gap-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-secondary\",\n activeView === \"yearly\"\n ? \"bg-primary text-primary-foreground shadow-lg\"\n : \"text-foreground hover:text-primary\"\n )}\n aria-pressed={activeView === \"yearly\"}\n aria-controls=\"cashflow-chart-bars\"\n >\n Currently selected: \n Yearly\n \n
\n \n\n \n
\n Bar chart comparing cash flow performance by period. Use the Monthly\n or Yearly toggle to change the data set.\n
\n\n
\n \n {[50, 40, 30, 20, 10, 0].map((value) => (\n
{value}k
\n ))}\n
\n\n \n {displayData.map((data, index) => {\n const heightPercentage = maxValue\n ? Math.max((data.value / maxValue) * 100, 4)\n : 0;\n // Keep tooltip inside the chart area so it doesn't get clipped\n const tooltipBase = Math.min(heightPercentage + 8, 88);\n const tooltipVisible = hoveredBar === index;\n const tooltipPosition = `${tooltipBase}%`;\n\n return (\n setHoveredBar(index)}\n onMouseLeave={() => setHoveredBar(null)}\n role=\"listitem\"\n >\n \n {tooltipVisible && (\n \n
\n {data.dateRange}\n
\n
\n \n Cash flow\n \n \n {formatCurrency(data.cashflow)}\n \n
\n
\n \n Inflow\n \n \n {formatCurrency(data.inflow)}\n \n
\n \n )}\n
\n\n setHoveredBar(index)}\n onBlur={() => setHoveredBar(null)}\n className={`w-full rounded-t-2xl relative overflow-hidden cursor-pointer transition-colors duration-300 min-h-[0.5rem] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background ${tooltipVisible ? \"bg-gradient-to-b from-primary to-primary/20\" : \"bg-primary/30\"}\n `}\n aria-label={`${data.label} cash flow ${formatCurrency(\n data.cashflow\n )} with ${formatCurrency(data.inflow)} in inflow`}\n aria-describedby={`${data.shortLabel}-summary`}\n >\n \n {`${data.label}: cash flow ${formatCurrency(\n data.cashflow\n )}, inflow ${formatCurrency(data.inflow)}`}\n \n \n\n \n {data.shortLabel}\n \n \n );\n })}\n \n \n\n \n \n \n \n \n \n \n \n \n \n {displayData.map((datum) => (\n \n \n \n \n \n ))}\n \n
\n Tabular representation of the cash flow data for assistive\n technologies.\n
PeriodCash flowInflow
{datum.label}{formatCurrency(datum.cashflow)}{formatCurrency(datum.inflow)}
\n \n \n );\n}\n", "type": "registry:ui", "target": "components/uitripled/cash-flow-chart-carbon.tsx" } ] }