
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Carta-style company equity capitalization table, share class breakdown, fully diluted ownership percentages, shareholder vesting progress, and interactive round dilution modeling simulator.
Also available for Vue ->$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/cap-table-ownership-summary.json$npx shadcn@latest add https://uipkge.dev/r/react/cap-table-ownership-summary.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/cap-table-ownership-summary.json$bunx shadcn@latest add https://uipkge.dev/r/react/cap-table-ownership-summary.jsonnpx shadcn@latest add @uipkge-react/cap-table-ownership-summaryInstalls to:components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
initialInvestment | number | — | optional |
initialPreMoney | number | — | optional |
className | string | — | optional |
Type aliases exported from this item's source. Use these to shape the data you pass in.
Shareholderinterface Shareholder {
id: string
name: string
role: string
avatar: string
shareClass: 'Common Stock' | 'Series Seed Preferred' | 'ISO Option Pool'
shares: number
ownershipPct: number
equityValue: number
vestingDetail: string
vestedMonths: number
totalMonths: number
isVestedComplete: boolean
grantDate: string
liquidationPref?: string
}'use client'
import * as React from 'react'
import {
Award,
Building2,
Calculator,
Check,
Coins,
FileSpreadsheet,
Info,
Layers,
Plus,
RefreshCw,
Search,
ShieldCheck,
TrendingUp,
Users,
X,
} from 'lucide-react'
import { cn } from '@/lib/utils'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
export interface CapTableOwnershipSummaryProps {
initialInvestment?: number
initialPreMoney?: number
className?: string
}
export interface Shareholder {
id: string
name: string
role: string
avatar: string
shareClass: 'Common Stock' | 'Series Seed Preferred' | 'ISO Option Pool'
shares: number
ownershipPct: number
equityValue: number
vestingDetail: string
vestedMonths: number
totalMonths: number
isVestedComplete: boolean
grantDate: string
liquidationPref?: string
}
const initialShareholders: Shareholder[] = [
{
id: 'sh-1',
name: 'Elena Rostova',
role: 'Co-Founder & CEO',
avatar: 'ER',
shareClass: 'Common Stock',
shares: 3500000,
ownershipPct: 35.0,
equityValue: 15750000,
vestingDetail: '48-Month Vesting · 24/48 Months Vested',
vestedMonths: 24,
totalMonths: 48,
isVestedComplete: false,
grantDate: 'Feb 15, 2024 · 1-yr Cliff (Satisfied)',
},
{
id: 'sh-2',
name: 'Marcus Vance',
role: 'Co-Founder & CTO',
avatar: 'MV',
shareClass: 'Common Stock',
shares: 2500000,
ownershipPct: 25.0,
equityValue: 11250000,
vestingDetail: '48-Month Vesting · 24/48 Months Vested',
vestedMonths: 24,
totalMonths: 48,
isVestedComplete: false,
grantDate: 'Feb 15, 2024 · 1-yr Cliff (Satisfied)',
},
{
id: 'sh-3',
name: 'Founders Fund / Seed Syndicate',
role: 'Lead Seed Investor',
avatar: 'FF',
shareClass: 'Series Seed Preferred',
shares: 1500000,
ownershipPct: 15.0,
equityValue: 6750000,
vestingDetail: 'Fully Vested (100%) · 1.0x Non-Participating',
vestedMonths: 48,
totalMonths: 48,
isVestedComplete: true,
grantDate: 'Aug 10, 2024 · Board Seat (1)',
liquidationPref: '1.0x Pref ($6.75M)',
},
{
id: 'sh-4',
name: 'Sequoia Scout Seed SPV',
role: 'Seed Co-Investor',
avatar: 'SQ',
shareClass: 'Series Seed Preferred',
shares: 1000000,
ownershipPct: 10.0,
equityValue: 4500000,
vestingDetail: 'Fully Vested (100%) · 1.0x Non-Participating',
vestedMonths: 48,
totalMonths: 48,
isVestedComplete: true,
grantDate: 'Aug 10, 2024 · Pro-Rata Rights',
liquidationPref: '1.0x Pref ($4.50M)',
},
{
id: 'sh-5',
name: 'Unallocated Employee Option Pool',
role: '2024 Equity Incentive Plan (EIP)',
avatar: 'EP',
shareClass: 'ISO Option Pool',
shares: 1500000,
ownershipPct: 15.0,
equityValue: 6750000,
vestingDetail: 'Authorized Pool · Available for Future Key Hires',
vestedMonths: 0,
totalMonths: 48,
isVestedComplete: false,
grantDate: 'Board Authorized · Dec 01, 2024',
},
]
export function CapTableOwnershipSummary({
initialInvestment = 10000000,
initialPreMoney = 50000000,
className,
}: CapTableOwnershipSummaryProps) {
const currentSharePrice = 4.5
const currentTotalShares = 10000000
const [shareholders, setShareholders] = React.useState<Shareholder[]>(initialShareholders)
const [searchQuery, setSearchQuery] = React.useState('')
const [selectedClassFilter, setSelectedClassFilter] = React.useState<
'All' | 'Common Stock' | 'Series Seed Preferred' | 'ISO Option Pool'
>('All')
// Export Cap Table feedback state
const [isExporting, setIsExporting] = React.useState(false)
const [exportSuccess, setExportSuccess] = React.useState(false)
const handleExportCapTable = () => {
setIsExporting(true)
setTimeout(() => {
setIsExporting(false)
setExportSuccess(true)
setTimeout(() => {
setExportSuccess(false)
}, 2500)
}, 1000)
}
// Issue Grant Modal State
const [isGrantDialogOpen, setIsGrantDialogOpen] = React.useState(false)
const [grantSuccessMessage, setGrantSuccessMessage] = React.useState(false)
const [grantName, setGrantName] = React.useState('')
const [grantRole, setGrantRole] = React.useState('')
const [grantShares, setGrantShares] = React.useState(50000)
const [grantClass, setGrantClass] = React.useState<'ISO Options' | 'NSO Options' | 'Restricted Stock'>('ISO Options')
const grantVesting = '4-Year Vesting · 1-Year Cliff (25%), then Monthly'
const handleCreateGrant = (e: React.FormEvent) => {
e.preventDefault()
if (!grantName.trim() || grantShares <= 0) return
setGrantSuccessMessage(true)
setTimeout(() => {
setGrantSuccessMessage(false)
setIsGrantDialogOpen(false)
setGrantName('')
setGrantRole('')
setGrantShares(50000)
}, 1500)
}
// Filtered Shareholders
const filteredShareholders = React.useMemo(() => {
return shareholders.filter((sh) => {
if (selectedClassFilter !== 'All' && sh.shareClass !== selectedClassFilter) {
return false
}
if (searchQuery.trim()) {
const q = searchQuery.toLowerCase()
const matchName = sh.name.toLowerCase().includes(q)
const matchRole = sh.role.toLowerCase().includes(q)
const matchClass = sh.shareClass.toLowerCase().includes(q)
return matchName || matchRole || matchClass
}
return true
})
}, [shareholders, selectedClassFilter, searchQuery])
// Round Modeling Simulator State
const [simPreMoney, setSimPreMoney] = React.useState(initialPreMoney)
const [simInvestment, setSimInvestment] = React.useState(initialInvestment)
const setPresetScenario = (preMoney: number, investment: number) => {
setSimPreMoney(preMoney)
setSimInvestment(investment)
}
// Dynamic Round Calculations
const simPostMoney = React.useMemo(() => simPreMoney + simInvestment, [simPreMoney, simInvestment])
const simNewSharePrice = React.useMemo(() => {
if (currentTotalShares <= 0) return 0
return simPreMoney / currentTotalShares
}, [simPreMoney, currentTotalShares])
const simNewSharesIssued = React.useMemo(() => {
if (simNewSharePrice <= 0) return 0
return Math.round(simInvestment / simNewSharePrice)
}, [simInvestment, simNewSharePrice])
const simTotalPostShares = React.useMemo(
() => currentTotalShares + simNewSharesIssued,
[currentTotalShares, simNewSharesIssued],
)
const simFounderShares = 6000000
const simSeedShares = 2500000
const simOptionShares = 1500000
const simFounderPct = React.useMemo(() => (simFounderShares / simTotalPostShares) * 100, [simTotalPostShares])
const simSeedPct = React.useMemo(() => (simSeedShares / simTotalPostShares) * 100, [simTotalPostShares])
const simOptionPct = React.useMemo(() => (simOptionShares / simTotalPostShares) * 100, [simTotalPostShares])
const simNewInvestorPct = React.useMemo(
() => (simNewSharesIssued / simTotalPostShares) * 100,
[simNewSharesIssued, simTotalPostShares],
)
const simFounderValue = React.useMemo(() => (simFounderPct / 100) * simPostMoney, [simFounderPct, simPostMoney])
const simSeedValue = React.useMemo(() => (simSeedPct / 100) * simPostMoney, [simSeedPct, simPostMoney])
const simOptionValue = React.useMemo(() => (simOptionPct / 100) * simPostMoney, [simOptionPct, simPostMoney])
const formatCurrency = (val: number): string => {
return val.toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
}
const formatNumber = (val: number): string => {
return val.toLocaleString('en-US')
}
const formatPercent = (val: number, decimals = 1): string => {
return `${val.toFixed(decimals)}%`
}
return (
<div data-slot="cap-table-ownership-summary" className={cn('mx-auto w-full max-w-6xl space-y-6', className)}>
{/* Top Header */}
<header className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-1.5">
<div className="flex flex-wrap items-center gap-2.5">
<div
className="bg-primary/10 text-primary border-primary/20 flex size-9 shrink-0 items-center justify-center rounded-lg border shadow-xs"
aria-hidden="true"
>
<Building2 className="size-5" />
</div>
<div>
<h1 className="text-foreground text-2xl font-bold tracking-tight">
Capitalization Table & Equity Ownership
</h1>
</div>
<Badge variant="outline" className="border-success/30 bg-success/10 text-success gap-1.5 font-normal">
<span className="bg-success size-1.5 animate-pulse rounded-full" aria-hidden="true" />
Audited · Series Seed
</Badge>
</div>
<div className="text-muted-foreground flex flex-wrap items-center gap-2 text-xs">
<span className="bg-muted/70 text-foreground border-border/80 rounded border px-2 py-0.5 font-medium">
UIPKGE Technologies Inc. · Delaware C-Corp
</span>
<span>EIN: 93-8472910</span>
<span className="inline-flex items-center gap-1 font-mono">
<ShieldCheck className="text-primary size-3.5" aria-hidden="true" />
409A FMV: ${currentSharePrice.toFixed(2)}/share
</span>
</div>
</div>
{/* Action Buttons */}
<div className="flex flex-wrap items-center gap-2">
<Button
variant="outline"
size="sm"
className="gap-1.5 shadow-xs"
disabled={isExporting}
onClick={handleExportCapTable}
>
{isExporting ? (
<RefreshCw className="size-4 animate-spin" aria-hidden="true" />
) : exportSuccess ? (
<Check className="text-success size-4" aria-hidden="true" />
) : (
<FileSpreadsheet className="size-4" aria-hidden="true" />
)}
<span>
{exportSuccess ? 'Cap Table Exported!' : isExporting ? 'Exporting...' : 'Export Cap Table (Excel)'}
</span>
</Button>
<Button size="sm" className="gap-1.5 shadow-xs" onClick={() => setIsGrantDialogOpen(true)}>
<Plus className="size-4" aria-hidden="true" />
Issue Equity Grant
</Button>
</div>
</header>
{/* Issue Equity Grant Dialog Modal Overlay */}
{isGrantDialogOpen && (
<div
className="bg-background/80 fixed inset-0 z-50 flex items-center justify-center p-4 backdrop-blur-xs"
role="dialog"
aria-modal="true"
aria-labelledby="grant-dialog-title-react"
>
<Card className="border-border w-full max-w-lg shadow-lg">
<CardHeader className="pb-3">
<div className="flex flex-wrap items-center justify-between gap-x-2">
<CardTitle id="grant-dialog-title-react" className="flex items-center gap-2 text-base font-semibold">
<Award className="text-primary size-5" />
Issue New Equity Option Grant
</CardTitle>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground hover:text-foreground size-7"
aria-label="Close dialog"
onClick={() => setIsGrantDialogOpen(false)}
>
<X className="size-4" />
</Button>
</div>
<CardDescription className="text-xs">
Authorize and draft an option grant under the 2024 Equity Incentive Plan (EIP).
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{grantSuccessMessage ? (
<div className="border-success/20 bg-success/10 rounded-lg border p-4 text-center">
<Check className="text-success mx-auto size-6" />
<p className="text-foreground mt-2 text-sm font-semibold">Option Grant Issued & Drafted!</p>
<p className="text-muted-foreground mt-0.5 text-xs">
Sent to Board of Directors for electronic consent signature.
</p>
</div>
) : (
<form className="space-y-3.5" onSubmit={handleCreateGrant}>
<div className="space-y-1.5">
<label htmlFor="grantee-name-react" className="text-foreground text-xs font-medium">
Grantee Legal Name
</label>
<Input
id="grantee-name-react"
value={grantName}
onChange={(e) => setGrantName(e.target.value)}
placeholder="e.g. Dr. Sarah Chen"
required
className="h-9 text-xs"
/>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div className="space-y-1.5">
<label htmlFor="grantee-role-react" className="text-foreground text-xs font-medium">
Role / Title
</label>
<Input
id="grantee-role-react"
value={grantRole}
onChange={(e) => setGrantRole(e.target.value)}
placeholder="e.g. Principal AI Scientist"
required
className="h-9 text-xs"
/>
</div>
<div className="space-y-1.5">
<label htmlFor="grant-quantity-react" className="text-foreground text-xs font-medium">
Number of Options / Shares
</label>
<Input
id="grant-quantity-react"
type="number"
min="1000"
step="1000"
value={grantShares}
onChange={(e) => setGrantShares(Number(e.target.value))}
required
className="h-9 font-mono text-xs tabular-nums"
/>
</div>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div className="space-y-1.5">
<label className="text-foreground text-xs font-medium">Equity Class</label>
<div className="border-border bg-muted/30 flex h-9 items-center justify-between rounded-md border px-3 text-xs">
<span className="font-medium">{grantClass}</span>
<Badge variant="outline" className="text-xs">
EIP-2024
</Badge>
</div>
</div>
<div className="space-y-1.5">
<label className="text-foreground text-xs font-medium">Exercise Price (409A FMV)</label>
<div className="border-border bg-muted/30 flex h-9 items-center justify-between rounded-md border px-3 font-mono text-xs tabular-nums">
<span>${currentSharePrice.toFixed(2)} / share</span>
<span className="text-muted-foreground text-xs">Fair Market</span>
</div>
</div>
</div>
<div className="space-y-1.5">
<label className="text-foreground text-xs font-medium">Standard Vesting Schedule</label>
<Input value={grantVesting} readOnly className="bg-muted/40 h-9 text-xs" />
<p className="text-muted-foreground text-xs">
25% vests at 12-month cliff; remaining 75% vests monthly in 36 equal installments.
</p>
</div>
<div className="flex items-center justify-end gap-2 pt-3">
<Button type="button" variant="outline" size="sm" onClick={() => setIsGrantDialogOpen(false)}>
Cancel
</Button>
<Button type="submit" size="sm" className="gap-1.5">
<Check className="size-4" />
Draft & Issue Grant
</Button>
</div>
</form>
)}
</CardContent>
</Card>
</div>
)}
{/* 4 Capital Structure Overview Cards */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{/* Total Fully Diluted Shares Card */}
<Card className="border-border relative overflow-hidden shadow-xs">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-muted-foreground text-xs font-medium">Total Fully Diluted Shares</CardTitle>
<div className="bg-primary/10 text-primary rounded-md p-1.5">
<Layers className="size-4" aria-hidden="true" />
</div>
</CardHeader>
<CardContent className="space-y-1">
<div className="text-foreground text-2xl font-bold tracking-tight tabular-nums">10,000,000</div>
<div className="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2 text-xs">
<span>Authorized: 15,000,000</span>
<span className="text-success font-mono">66.7% Issued</span>
</div>
</CardContent>
</Card>
{/* Founder Common Stock Card */}
<Card className="border-border relative overflow-hidden shadow-xs">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-muted-foreground text-xs font-medium">Founder Common Stock</CardTitle>
<div className="bg-success/10 text-success rounded-md p-1.5">
<Users className="size-4" aria-hidden="true" />
</div>
</CardHeader>
<CardContent className="space-y-1">
<div className="flex items-baseline gap-2">
<span className="text-foreground text-2xl font-bold tracking-tight tabular-nums">6,000,000</span>
<span className="text-success font-mono text-xs font-semibold">60.0%</span>
</div>
<div className="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2 text-xs">
<span>2 Co-Founders</span>
<span className="font-mono tabular-nums">$27,000,000.00</span>
</div>
</CardContent>
</Card>
{/* Series Seed Preferred Card */}
<Card className="border-border relative overflow-hidden shadow-xs">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-muted-foreground text-xs font-medium">Series Seed Preferred</CardTitle>
<div className="bg-chart-2/10 text-chart-2 rounded-md p-1.5">
<TrendingUp className="size-4" aria-hidden="true" />
</div>
</CardHeader>
<CardContent className="space-y-1">
<div className="flex items-baseline gap-2">
<span className="text-foreground text-2xl font-bold tracking-tight tabular-nums">2,500,000</span>
<span className="text-chart-2 font-mono text-xs font-semibold">25.0%</span>
</div>
<div className="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2 text-xs">
<span>2 Seed Investors · 1.0x Pref</span>
<span className="font-mono tabular-nums">$11,250,000.00</span>
</div>
</CardContent>
</Card>
{/* Unallocated Option Pool Card */}
<Card className="border-border relative overflow-hidden shadow-xs">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-muted-foreground text-xs font-medium">Unallocated Option Pool</CardTitle>
<div className="bg-info/10 text-info rounded-md p-1.5">
<Coins className="size-4" aria-hidden="true" />
</div>
</CardHeader>
<CardContent className="space-y-1">
<div className="flex items-baseline gap-2">
<span className="text-foreground text-2xl font-bold tracking-tight tabular-nums">1,500,000</span>
<span className="text-info font-mono text-xs font-semibold">15.0%</span>
</div>
<div className="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2 text-xs">
<span>2024 EIP Pool</span>
<span className="font-mono tabular-nums">$6,750,000.00</span>
</div>
</CardContent>
</Card>
</div>
{/* Company Valuation Hero & Visual Stacked Ownership Bar Card */}
<Card className="border-border overflow-hidden shadow-xs">
<CardHeader className="pb-3">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<CardDescription className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
Current Post-Money Valuation
</CardDescription>
<div className="mt-1 flex flex-wrap items-baseline gap-3">
<span className="text-foreground text-3xl font-bold tracking-tight tabular-nums sm:text-4xl">
$45,000,000.00
</span>
<div className="border-success/20 bg-success/10 text-success text-success inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-semibold tabular-nums">
<TrendingUp className="size-3.5" aria-hidden="true" />
Seed Round Closed
<span className="text-muted-foreground ml-0.5 font-normal">($4.50/share)</span>
</div>
</div>
</div>
<div className="text-muted-foreground flex items-center gap-3 text-xs">
<div className="flex items-center gap-1.5">
<span className="bg-success size-2 rounded-full" aria-hidden="true" />
<span>Cap Table Reconciled</span>
</div>
<span className="font-mono">10,000,000 Total Units</span>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4 pt-1">
{/* Multi-segment Stacked Ownership Bar */}
<div className="space-y-2">
<div className="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2 text-xs">
<span className="text-foreground font-medium">Fully Diluted Equity Distribution</span>
<span className="tabular-nums">100.0% Allocation</span>
</div>
<div className="bg-muted/60 border-border/50 flex h-4 w-full gap-0.5 overflow-hidden rounded-full border p-0.5">
{/* Founders 60% Emerald */}
<div
style={{ width: '60%' }}
className="bg-success h-full rounded-l-full transition-opacity duration-300 hover:opacity-90"
title="Founders (Common Stock): 60.0% · 6,000,000 Shares"
/>
{/* Seed Investors 25% */}
<div
style={{ width: '25%' }}
className="bg-chart-2 h-full transition-opacity duration-300 hover:opacity-90"
title="Seed Investors (Preferred): 25.0% · 2,500,000 Shares"
/>
{/* Option Pool 15% Blue */}
<div
style={{ width: '15%' }}
className="bg-info h-full rounded-r-full transition-opacity duration-300 hover:opacity-90"
title="Unallocated Option Pool: 15.0% · 1,500,000 Shares"
/>
</div>
{/* Stacked Bar Legend */}
<div className="grid grid-cols-1 gap-3 pt-2 sm:grid-cols-3">
<div className="border-border/60 bg-muted/20 flex flex-wrap items-center justify-between gap-x-2 rounded-md border p-2.5 text-xs">
<div className="flex items-center gap-2">
<span className="bg-success size-2.5 shrink-0 rounded-full shadow-xs" aria-hidden="true" />
<div>
<div className="text-foreground font-medium">Founders (Common)</div>
<div className="text-muted-foreground font-mono tabular-nums">6,000,000 shares</div>
</div>
</div>
<div className="text-right">
<div className="text-foreground font-bold tabular-nums">60.0%</div>
<div className="text-muted-foreground font-mono tabular-nums">$27,000,000.00</div>
</div>
</div>
<div className="border-border/60 bg-muted/20 flex flex-wrap items-center justify-between gap-x-2 rounded-md border p-2.5 text-xs">
<div className="flex items-center gap-2">
<span className="bg-chart-2 size-2.5 shrink-0 rounded-full shadow-xs" aria-hidden="true" />
<div>
<div className="text-foreground font-medium">Seed Investors (Preferred)</div>
<div className="text-muted-foreground font-mono tabular-nums">2,500,000 shares</div>
</div>
</div>
<div className="text-right">
<div className="text-foreground font-bold tabular-nums">25.0%</div>
<div className="text-muted-foreground font-mono tabular-nums">$11,250,000.00</div>
</div>
</div>
<div className="border-border/60 bg-muted/20 flex flex-wrap items-center justify-between gap-x-2 rounded-md border p-2.5 text-xs">
<div className="flex items-center gap-2">
<span className="bg-info size-2.5 shrink-0 rounded-full shadow-xs" aria-hidden="true" />
<div>
<div className="text-foreground font-medium">Employee Option Pool</div>
<div className="text-muted-foreground font-mono tabular-nums">1,500,000 shares</div>
</div>
</div>
<div className="text-right">
<div className="text-foreground font-bold tabular-nums">15.0%</div>
<div className="text-muted-foreground font-mono tabular-nums">$6,750,000.00</div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Shareholder Breakdown Table Card */}
<Card className="border-border shadow-xs">
<CardHeader className="border-border/60 border-b pb-4">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<CardTitle className="text-foreground text-base font-semibold">
Shareholder & Equity Register
</CardTitle>
<CardDescription className="text-muted-foreground mt-0.5 text-xs">
Detailed registry of individual grants, share classes, fully diluted ownership, and vesting timelines.
</CardDescription>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
{/* Search */}
<div className="relative w-full sm:w-56">
<Search className="text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2" />
<Input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search shareholder or role..."
className="h-8 pl-8 text-xs"
/>
</div>
{/* Share Class Filter Buttons */}
<div className="border-border bg-muted/40 flex items-center rounded-lg border p-0.5">
<button
type="button"
className={cn(
'rounded-md px-2.5 py-1 text-xs font-medium transition-colors',
selectedClassFilter === 'All'
? 'bg-background text-foreground shadow-xs'
: 'text-muted-foreground hover:text-foreground',
)}
onClick={() => setSelectedClassFilter('All')}
>
All (5)
</button>
<button
type="button"
className={cn(
'rounded-md px-2.5 py-1 text-xs font-medium transition-colors',
selectedClassFilter === 'Common Stock'
? 'bg-background text-foreground shadow-xs'
: 'text-muted-foreground hover:text-foreground',
)}
onClick={() => setSelectedClassFilter('Common Stock')}
>
Common (2)
</button>
<button
type="button"
className={cn(
'rounded-md px-2.5 py-1 text-xs font-medium transition-colors',
selectedClassFilter === 'Series Seed Preferred'
? 'bg-background text-foreground shadow-xs'
: 'text-muted-foreground hover:text-foreground',
)}
onClick={() => setSelectedClassFilter('Series Seed Preferred')}
>
Preferred (2)
</button>
<button
type="button"
className={cn(
'rounded-md px-2.5 py-1 text-xs font-medium transition-colors',
selectedClassFilter === 'ISO Option Pool'
? 'bg-background text-foreground shadow-xs'
: 'text-muted-foreground hover:text-foreground',
)}
onClick={() => setSelectedClassFilter('ISO Option Pool')}
>
Pool (1)
</button>
</div>
</div>
</div>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead className="w-[260px] pl-6 text-xs font-medium">Shareholder / Entity</TableHead>
<TableHead className="w-[180px] text-xs font-medium">Share Class</TableHead>
<TableHead className="text-right text-xs font-medium">Shares Owned</TableHead>
<TableHead className="text-right text-xs font-medium">Fully Diluted %</TableHead>
<TableHead className="text-right text-xs font-medium">Total Equity Value</TableHead>
<TableHead className="min-w-[240px] pr-6 text-xs font-medium">Vesting & Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredShareholders.map((sh) => (
<TableRow key={sh.id} className="hover:bg-muted/30 transition-colors">
{/* Shareholder Name & Role */}
<TableCell className="py-3.5 pl-6">
<div className="flex items-center gap-3">
<Avatar className="border-border/80 size-9 border shadow-xs">
<AvatarFallback
className={cn(
'text-xs font-bold',
sh.shareClass === 'Common Stock'
? 'bg-success/10 text-success'
: sh.shareClass === 'Series Seed Preferred'
? 'bg-chart-2/10 text-chart-2'
: 'bg-info/10 text-info',
)}
>
{
{
'Common Stock': sh.avatar,
'Series Seed Preferred': sh.avatar,
'ISO Option Pool': sh.avatar,
}[sh.shareClass]
}
</AvatarFallback>
</Avatar>
<div className="min-w-0">
<div className="text-foreground truncate text-xs font-semibold">{sh.name}</div>
<div className="text-muted-foreground truncate text-xs">{sh.role}</div>
<div className="text-muted-foreground/70 font-mono text-xs">{sh.grantDate}</div>
</div>
</div>
</TableCell>
{/* Share Class Badge */}
<TableCell className="py-3.5">
{sh.shareClass === 'Common Stock' ? (
<Badge
variant="outline"
className="border-success/30 bg-success/10 text-success text-xs font-medium"
>
Common Stock
</Badge>
) : sh.shareClass === 'Series Seed Preferred' ? (
<Badge
variant="outline"
className="border-chart-2/30 bg-chart-2/10 text-chart-2 text-xs font-medium"
>
Series Seed Preferred
</Badge>
) : (
<Badge variant="outline" className="border-info/30 bg-info/10 text-info text-xs font-medium">
ISO Option Pool
</Badge>
)}
</TableCell>
{/* Shares Owned */}
<TableCell className="py-3.5 text-right">
<div className="text-foreground font-mono text-sm font-bold tabular-nums">
{
{
'Common Stock': formatNumber(sh.shares),
'Series Seed Preferred': formatNumber(sh.shares),
'ISO Option Pool': formatNumber(sh.shares),
}[sh.shareClass]
}
</div>
<div className="text-muted-foreground text-xs">Shares</div>
</TableCell>
{/* Fully Diluted Ownership Percentage */}
<TableCell className="py-3.5 text-right">
<div className="text-foreground font-mono text-sm font-semibold tabular-nums">
{formatPercent(sh.ownershipPct)}
</div>
<div className="text-muted-foreground text-xs">of 10,000,000</div>
</TableCell>
{/* Total Equity Value */}
<TableCell className="py-3.5 text-right">
<div className="text-foreground font-mono text-sm font-semibold tabular-nums">
{formatCurrency(sh.equityValue)}
</div>
<div className="text-muted-foreground text-xs">@ $4.50/share</div>
</TableCell>
{/* Vesting Status Progress */}
<TableCell className="py-3.5 pr-6">
<div className="space-y-1.5">
<div className="flex flex-wrap items-center justify-between gap-x-2 text-xs">
<span className="text-foreground font-medium">
{sh.isVestedComplete
? '100% Vested'
: sh.vestedMonths > 0
? `${Math.round((sh.vestedMonths / sh.totalMonths) * 100)}% Vested`
: 'Unallocated'}
</span>
<span className="text-muted-foreground font-mono tabular-nums">
{sh.vestedMonths}/{sh.totalMonths} mo
</span>
</div>
<div className="bg-muted/80 relative h-2 w-full overflow-hidden rounded-full">
<div
className={cn(
'h-full transition-[width,background-color] duration-300',
sh.isVestedComplete ? 'bg-primary' : sh.vestedMonths > 0 ? 'bg-success' : 'bg-info/40',
)}
style={{
width: `${sh.isVestedComplete ? 100 : (sh.vestedMonths / sh.totalMonths) * 100}%`,
}}
/>
</div>
<div className="text-muted-foreground text-xs">{sh.vestingDetail}</div>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* Table Summary Bar */}
<div className="border-border/60 bg-muted/20 flex flex-wrap items-center justify-between gap-4 border-t px-6 py-3 text-xs">
<div className="text-muted-foreground">
Showing <span className="text-foreground font-semibold">{filteredShareholders.length}</span> of{' '}
<span className="text-foreground font-semibold">5</span> cap table stakeholders
</div>
<div className="flex flex-wrap items-center gap-6 font-mono">
<div>
<span className="text-muted-foreground">Total Shares: </span>
<span className="text-foreground font-bold tabular-nums">10,000,000</span>
</div>
<div>
<span className="text-muted-foreground">Total Ownership: </span>
<span className="text-foreground font-bold tabular-nums">100.0%</span>
</div>
<div>
<span className="text-muted-foreground">Total Valuation: </span>
<span className="text-foreground font-bold tabular-nums">$45,000,000.00</span>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Round Modeling Simulator Card (Series A Dilution Simulator) */}
<Card className="border-border shadow-xs">
<CardHeader className="pb-3">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2">
<div className="bg-primary/10 text-primary rounded-md p-1.5">
<Calculator className="size-4" aria-hidden="true" />
</div>
<CardTitle className="text-foreground text-base font-semibold">
Round Modeling Simulator (Series A Pro-Forma)
</CardTitle>
</div>
<CardDescription className="text-muted-foreground text-xs">
Simulate Series A pre-money valuation and capital raised to preview diluted ownership percentages, new
share price, and stakeholder equity value accretion.
</CardDescription>
</div>
{/* Preset Scenario Buttons */}
<div className="flex flex-wrap items-center gap-1.5">
<Button
variant="outline"
size="sm"
className="h-7 text-xs font-medium"
onClick={() => setPresetScenario(30000000, 5000000)}
>
$5M @ $30M Pre
</Button>
<Button
variant="outline"
size="sm"
className="h-7 text-xs font-medium"
onClick={() => setPresetScenario(50000000, 10000000)}
>
$10M @ $50M Pre
</Button>
<Button
variant="outline"
size="sm"
className="h-7 text-xs font-medium"
onClick={() => setPresetScenario(60000000, 15000000)}
>
$15M @ $60M Pre
</Button>
</div>
</div>
</CardHeader>
<CardContent className="space-y-6">
{/* Simulator Inputs */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{/* Pre-Money Valuation Input */}
<div className="bg-muted/30 border-border/80 space-y-2 rounded-lg border p-4">
<div className="flex flex-wrap items-center justify-between gap-x-2 text-xs">
<label htmlFor="sim-pre-money-react" className="text-foreground font-semibold">
Series A Pre-Money Valuation
</label>
<span className="text-muted-foreground font-mono tabular-nums">
${(simPreMoney / 1000000).toFixed(1)}M
</span>
</div>
<div className="flex items-center gap-3">
<Input
id="sim-pre-money-react"
type="number"
step="1000000"
min="10000000"
value={simPreMoney}
onChange={(e) => setSimPreMoney(Number(e.target.value) || 0)}
className="font-mono text-base font-bold tabular-nums"
/>
</div>
<p className="text-muted-foreground text-xs">
Implies a share price of{' '}
<strong className="text-foreground font-mono tabular-nums">
${simNewSharePrice.toFixed(2)} / share
</strong>{' '}
(+{(((simNewSharePrice - currentSharePrice) / currentSharePrice) * 100).toFixed(1)}% step-up).
</p>
</div>
{/* New Investment Raised Input */}
<div className="bg-muted/30 border-border/80 space-y-2 rounded-lg border p-4">
<div className="flex flex-wrap items-center justify-between gap-x-2 text-xs">
<label htmlFor="sim-investment-react" className="text-foreground font-semibold">
Series A Capital Raised
</label>
<span className="text-muted-foreground font-mono tabular-nums">
${(simInvestment / 1000000).toFixed(1)}M
</span>
</div>
<div className="flex items-center gap-3">
<Input
id="sim-investment-react"
type="number"
step="1000000"
min="1000000"
value={simInvestment}
onChange={(e) => setSimInvestment(Number(e.target.value) || 0)}
className="font-mono text-base font-bold tabular-nums"
/>
</div>
<p className="text-muted-foreground text-xs">
Issues{' '}
<strong className="text-foreground font-mono tabular-nums">
{formatNumber(simNewSharesIssued)} new shares
</strong>{' '}
to Series A lead syndicate.
</p>
</div>
</div>
{/* Calculated Pro-Forma Summary Metrics */}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<div className="border-border/60 bg-muted/20 space-y-1 rounded-md border p-3">
<div className="text-muted-foreground text-xs">Post-Money Valuation</div>
<div className="text-foreground font-mono text-lg font-bold tabular-nums">
{formatCurrency(simPostMoney)}
</div>
<div className="text-muted-foreground text-xs">Pre-Money + Investment</div>
</div>
<div className="border-border/60 bg-muted/20 space-y-1 rounded-md border p-3">
<div className="text-muted-foreground text-xs">Series A Share Price</div>
<div className="text-foreground font-mono text-lg font-bold tabular-nums">
${simNewSharePrice.toFixed(2)}
</div>
<div className="text-success text-xs">
vs ${currentSharePrice.toFixed(2)} Seed (+
{(((simNewSharePrice - currentSharePrice) / currentSharePrice) * 100).toFixed(1)}%)
</div>
</div>
<div className="border-border/60 bg-muted/20 space-y-1 rounded-md border p-3">
<div className="text-muted-foreground text-xs">Total Diluted Shares</div>
<div className="text-foreground font-mono text-lg font-bold tabular-nums">
{formatNumber(simTotalPostShares)}
</div>
<div className="text-muted-foreground text-xs">+{formatNumber(simNewSharesIssued)} New Shares</div>
</div>
<div className="border-border/60 bg-muted/20 space-y-1 rounded-md border p-3">
<div className="text-muted-foreground text-xs">Investor Dilution Rate</div>
<div className="text-foreground text-warning text-warning font-mono text-lg font-bold tabular-nums">
{formatPercent(simNewInvestorPct)}
</div>
<div className="text-muted-foreground text-xs">Effective Round Dilution</div>
</div>
</div>
{/* Post-Round Dilution & Valuation Comparison Table */}
<div className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-x-2 text-xs">
<span className="text-foreground font-semibold">Post-Series A Ownership Comparison</span>
<span className="text-muted-foreground">Pro-Forma Stakeholder Impact</span>
</div>
{/* Visual Comparison Bar */}
<div className="space-y-1.5">
<div className="bg-muted/60 border-border/50 flex h-4 w-full gap-0.5 overflow-hidden rounded-full border p-0.5">
<div
style={{ width: `${simFounderPct}%` }}
className="bg-success h-full rounded-l-full transition-[width] duration-300"
title={`Founders: ${simFounderPct.toFixed(1)}%`}
/>
<div
style={{ width: `${simSeedPct}%` }}
className="bg-chart-2 h-full transition-[width] duration-300"
title={`Seed Investors: ${simSeedPct.toFixed(1)}%`}
/>
<div
style={{ width: `${simOptionPct}%` }}
className="bg-info h-full transition-[width] duration-300"
title={`Option Pool: ${simOptionPct.toFixed(1)}%`}
/>
<div
style={{ width: `${simNewInvestorPct}%` }}
className="bg-warning h-full rounded-r-full transition-[width] duration-300"
title={`Series A Investors: ${simNewInvestorPct.toFixed(1)}%`}
/>
</div>
</div>
{/* Stakeholder Comparative Grid */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
{/* Founders */}
<div className="border-border/80 bg-card space-y-2 rounded-lg border p-3 text-xs shadow-xs">
<div className="flex flex-wrap items-center justify-between gap-x-2">
<div className="flex items-center gap-1.5">
<span className="bg-success size-2 rounded-full" aria-hidden="true" />
<span className="text-foreground font-semibold">Founders</span>
</div>
<Badge variant="outline" className="border-success/30 text-success font-mono text-xs">
{formatPercent(simFounderPct)}
</Badge>
</div>
<div className="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2">
<span>Dilution:</span>
<span className="text-destructive text-destructive font-mono tabular-nums">
60.0% → {formatPercent(simFounderPct)} (-{(60 - simFounderPct).toFixed(1)}%)
</span>
</div>
<div className="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2">
<span>Equity Value:</span>
<span className="text-foreground font-mono font-semibold tabular-nums">
{formatCurrency(simFounderValue)}
</span>
</div>
<div className="text-success flex flex-wrap items-center justify-between gap-x-2 text-xs">
<span>Value Accretion:</span>
<span className="font-mono font-semibold tabular-nums">
+{formatCurrency(simFounderValue - 27000000)}
</span>
</div>
</div>
{/* Seed Investors */}
<div className="border-border/80 bg-card space-y-2 rounded-lg border p-3 text-xs shadow-xs">
<div className="flex flex-wrap items-center justify-between gap-x-2">
<div className="flex items-center gap-1.5">
<span className="bg-chart-2 size-2 rounded-full" aria-hidden="true" />
<span className="text-foreground font-semibold">Seed Investors</span>
</div>
<Badge variant="outline" className="border-chart-2/30 text-chart-2 font-mono text-xs">
{formatPercent(simSeedPct)}
</Badge>
</div>
<div className="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2">
<span>Dilution:</span>
<span className="text-destructive text-destructive font-mono tabular-nums">
25.0% → {formatPercent(simSeedPct)} (-{(25 - simSeedPct).toFixed(1)}%)
</span>
</div>
<div className="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2">
<span>Equity Value:</span>
<span className="text-foreground font-mono font-semibold tabular-nums">
{formatCurrency(simSeedValue)}
</span>
</div>
<div className="text-success flex flex-wrap items-center justify-between gap-x-2 text-xs">
<span>Value Accretion:</span>
<span className="font-mono font-semibold tabular-nums">
+{formatCurrency(simSeedValue - 11250000)}
</span>
</div>
</div>
{/* Option Pool */}
<div className="border-border/80 bg-card space-y-2 rounded-lg border p-3 text-xs shadow-xs">
<div className="flex flex-wrap items-center justify-between gap-x-2">
<div className="flex items-center gap-1.5">
<span className="bg-info size-2 rounded-full" aria-hidden="true" />
<span className="text-foreground font-semibold">Option Pool</span>
</div>
<Badge variant="outline" className="border-info/30 text-info font-mono text-xs">
{formatPercent(simOptionPct)}
</Badge>
</div>
<div className="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2">
<span>Dilution:</span>
<span className="text-destructive text-destructive font-mono tabular-nums">
15.0% → {formatPercent(simOptionPct)} (-{(15 - simOptionPct).toFixed(1)}%)
</span>
</div>
<div className="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2">
<span>Equity Value:</span>
<span className="text-foreground font-mono font-semibold tabular-nums">
{formatCurrency(simOptionValue)}
</span>
</div>
<div className="text-success flex flex-wrap items-center justify-between gap-x-2 text-xs">
<span>Value Accretion:</span>
<span className="font-mono font-semibold tabular-nums">
+{formatCurrency(simOptionValue - 6750000)}
</span>
</div>
</div>
{/* New Series A Investors */}
<div className="border-border/80 bg-card space-y-2 rounded-lg border p-3 text-xs shadow-xs">
<div className="flex flex-wrap items-center justify-between gap-x-2">
<div className="flex items-center gap-1.5">
<span className="bg-warning size-2 rounded-full" aria-hidden="true" />
<span className="text-foreground font-semibold">Series A Syndicate</span>
</div>
<Badge variant="outline" className="border-warning/30 text-warning font-mono text-xs">
New Investor
</Badge>
</div>
<div className="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2">
<span>New Ownership:</span>
<span className="text-warning text-warning font-mono font-bold tabular-nums">
{formatPercent(simNewInvestorPct)}
</span>
</div>
<div className="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2">
<span>Shares Purchased:</span>
<span className="text-foreground font-mono font-semibold tabular-nums">
{formatNumber(simNewSharesIssued)}
</span>
</div>
<div className="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2 text-xs">
<span>Capital Invested:</span>
<span className="text-foreground font-mono font-semibold tabular-nums">
{formatCurrency(simInvestment)}
</span>
</div>
</div>
</div>
</div>
{/* Insight Banner Callout */}
<div className="border-border/60 bg-muted/40 flex items-start gap-3 rounded-lg border p-3.5 text-xs">
<Info className="text-primary mt-0.5 size-4 shrink-0" aria-hidden="true" />
<div className="text-muted-foreground space-y-1">
<span className="text-foreground font-semibold">Round Dynamics Takeaway: </span>
Raising <span className="text-foreground font-mono font-medium">{formatCurrency(simInvestment)}</span> at
a <span className="text-foreground font-mono font-medium">{formatCurrency(simPreMoney)}</span> pre-money
valuation generates a{' '}
<span className="text-foreground font-mono font-medium">${simNewSharePrice.toFixed(2)}</span> share price.
Although existing founders experience{' '}
<span className="text-foreground font-mono font-medium">{(60 - simFounderPct).toFixed(1)}%</span>{' '}
dilution, their net equity value increases by{' '}
<span className="text-success font-mono font-bold">+{formatCurrency(simFounderValue - 27000000)}</span> (+
{(((simFounderValue - 27000000) / 27000000) * 100).toFixed(1)}%) due to the valuation step-up.
</div>
</div>
</CardContent>
</Card>
</div>
)
}
export default CapTableOwnershipSummary
Raw manifest:https://uipkge.dev/r/react/cap-table-ownership-summary.json