Menu

Change language

GitHub repo

Safe Note Calculator

blockfinance

Y Combinator Post-Money SAFE note valuation, conversion share price, effective discount, and cap table equity dilution simulator with interactive parameter controls and Series A financing sensitivity matrix.

Installation

$npx uipkge-ng@latest add safe-note-calculator
Manifest:https://uipkge.dev/r/angular/safe-note-calculator.jsonInstalls to:src/app/components/blocks/safe-note-calculator/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

PresetConfig
interface PresetConfig {
  id: RoundPreset
  label: string
  investment: number
  valuationCap: number
  hasDiscount: boolean
  discountRate: number
  shares: number
  seriesAPreMoney: number
  description: string
}

Files installed (2)

  • src/app/components/blocks/safe-note-calculator/safe-note-calculator.component.ts43.5 kB
    // Y Combinator Post-Money SAFE note valuation, conversion share price, effective discount,
    // and cap table equity dilution simulator with interactive parameter controls and a Series A
    // financing sensitivity matrix. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, DestroyRef, Input, computed, inject, signal } from '@angular/core'
    import {
      BadgePercent,
      Check,
      CheckCircle2,
      Copy,
      DollarSign,
      FileSpreadsheet,
      Layers,
      LucideAngularModule,
      Percent,
      PieChart,
      Scale,
      ShieldCheck,
      TrendingUp,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardDescriptionComponent,
      UiCardFooterComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiInputComponent } from '@/ui/input/input.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import { UiSliderComponent } from '@/ui/slider/slider.component'
    import { UiSwitchComponent } from '@/ui/switch/switch.component'
    import {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableFooterComponent,
      UiTableHeadComponent,
      UiTableHeaderComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    import { UiTabsComponent, UiTabsListComponent, UiTabsTriggerComponent } from '@/ui/tabs/tabs.component'
    
    export type RoundPreset = 'seed-standard' | 'preseed-safe' | 'uncapped-discount' | 'bridge-round'
    
    export interface PresetConfig {
      id: RoundPreset
      label: string
      investment: number
      valuationCap: number
      hasDiscount: boolean
      discountRate: number
      shares: number
      seriesAPreMoney: number
      description: string
    }
    
    export const PRESETS: Record<RoundPreset, PresetConfig> = {
      'seed-standard': {
        id: 'seed-standard',
        label: 'Standard Seed ($1M @ $15M)',
        investment: 1_000_000,
        valuationCap: 15_000_000,
        hasDiscount: true,
        discountRate: 20,
        shares: 10_000_000,
        seriesAPreMoney: 25_000_000,
        description: 'Standard YC Post-Money SAFE with $15M valuation cap and optional 20% discount clause.',
      },
      'preseed-safe': {
        id: 'preseed-safe',
        label: 'Pre-Seed ($500K @ $8M)',
        investment: 500_000,
        valuationCap: 8_000_000,
        hasDiscount: true,
        discountRate: 20,
        shares: 10_000_000,
        seriesAPreMoney: 18_000_000,
        description: 'Early-stage angel or accelerator round with $8M valuation cap.',
      },
      'uncapped-discount': {
        id: 'uncapped-discount',
        label: 'Uncapped (25% Disc)',
        investment: 750_000,
        valuationCap: 60_000_000,
        hasDiscount: true,
        discountRate: 25,
        shares: 10_000_000,
        seriesAPreMoney: 20_000_000,
        description: 'High valuation cap structure where the 25% discount governs share conversion price.',
      },
      'bridge-round': {
        id: 'bridge-round',
        label: 'Growth Bridge ($2M @ $25M)',
        investment: 2_000_000,
        valuationCap: 25_000_000,
        hasDiscount: true,
        discountRate: 15,
        shares: 10_000_000,
        seriesAPreMoney: 45_000_000,
        description: 'Late seed extension bridging into a larger institutional Series A equity round.',
      },
    }
    
    const INVESTMENT_MARKS: Record<number, string> = {
      100000: '$100K',
      1000000: '$1M',
      2500000: '$2.5M',
      5000000: '$5M',
    }
    
    const VALUATION_CAP_MARKS: Record<number, string> = {
      2000000: '$2M',
      15000000: '$15M',
      30000000: '$30M',
      50000000: '$50M',
    }
    
    const DISCOUNT_RATE_MARKS: Record<number, string> = {
      10: '10%',
      15: '15%',
      20: '20%',
      25: '25%',
      30: '30%',
    }
    
    const SERIES_A_MARKS: Record<number, string> = {
      5000000: '$5M',
      25000000: '$25M',
      50000000: '$50M',
      80000000: '$80M',
    }
    
    const SCENARIO_VALUATIONS = [10_000_000, 15_000_000, 20_000_000, 25_000_000, 40_000_000, 60_000_000]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-safe-note-calculator, [ui-safe-note-calculator]',
      standalone: true,
      // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardFooterComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiInputComponent,
        UiSeparatorComponent,
        UiSliderComponent,
        UiSwitchComponent,
        UiTableBodyComponent,
        UiTableCellComponent,
        UiTableComponent,
        UiTableFooterComponent,
        UiTableHeadComponent,
        UiTableHeaderComponent,
        UiTableRowComponent,
        UiTabsComponent,
        UiTabsListComponent,
        UiTabsTriggerComponent,
      ],
      template: `
        <div data-slot="safe-note-calculator" [class]="rootClass">
          <!-- Header -->
          <div class="flex flex-col items-center gap-4 text-center">
            <span ui-badge wrap variant="outline" class="gap-1.5 px-3 py-1 text-xs font-medium tracking-wider uppercase">
              <lucide-icon [img]="Scale" class="text-primary size-3.5" />
              YC Post-Money SAFE v1.1 · Valuation Cap + Discount
            </span>
            <div class="space-y-2">
              <h2 class="text-foreground text-2xl font-bold tracking-tight sm:text-3xl lg:text-4xl">
                Y Combinator Post-Money SAFE Calculator
              </h2>
              <p class="text-muted-foreground mx-auto max-w-2xl text-sm sm:text-base">
                Model founder dilution, investor ownership percentages, and conversion price per share.
              </p>
            </div>
    
            <!-- Preset Tabs -->
            <div class="mt-2 flex w-full justify-center">
              <ui-tabs [value]="selectedPreset()" (valueChange)="handlePresetChange($event)" class="w-full sm:w-auto">
                <ui-tabs-list variant="segmented" class="grid w-full grid-cols-2 sm:flex sm:w-auto">
                  <button ui-tabs-trigger value="seed-standard" class="gap-1.5">
                    <lucide-icon [img]="ShieldCheck" class="size-3.5" />
                    Standard Seed
                  </button>
                  <button ui-tabs-trigger value="preseed-safe" class="gap-1.5">
                    <lucide-icon [img]="TrendingUp" class="size-3.5" />
                    Pre-Seed SAFE
                  </button>
                  <button ui-tabs-trigger value="uncapped-discount" class="gap-1.5">
                    <lucide-icon [img]="BadgePercent" class="size-3.5" />
                    Uncapped Disc
                  </button>
                  <button ui-tabs-trigger value="bridge-round" class="gap-1.5">
                    <lucide-icon [img]="Layers" class="size-3.5" />
                    Growth Bridge
                  </button>
                </ui-tabs-list>
              </ui-tabs>
            </div>
          </div>
    
          <!-- 2-Column Calculator Layout -->
          <div class="grid grid-cols-1 gap-8 lg:grid-cols-12 lg:items-start">
            <!-- Left Input Card -->
            <div class="space-y-6 lg:col-span-7">
              <div ui-card>
                <div ui-card-header>
                  <h3 ui-card-title class="flex items-center gap-2">
                    <lucide-icon [img]="DollarSign" class="text-primary size-5" />
                    SAFE Terms & Round Inputs
                  </h3>
                  <p ui-card-description>
                    Configure investment size, valuation cap, optional discount rate, capitalization, and Series A targets.
                  </p>
                </div>
                <div ui-card-content class="space-y-7">
                  <!-- 1. Investment Amount -->
                  <div class="space-y-3.5">
                    <div class="flex items-center justify-between gap-4">
                      <div>
                        <span class="text-foreground text-sm font-medium">Investment Amount</span>
                        <p class="text-muted-foreground text-xs">Total cash invested into the SAFE.</p>
                      </div>
                      <div class="w-36 sm:w-44">
                        <ui-input
                          id="safe-investment-input"
                          type="number"
                          size="small"
                          [min]="25000"
                          [max]="10000000"
                          [step]="25000"
                          prefix="$"
                          [value]="investmentAmount()"
                          (valueChange)="investmentAmount.set(+$event || 0)"
                        />
                      </div>
                    </div>
                    <div class="pb-5">
                      <ui-slider
                        [value]="[investmentAmount()]"
                        (valueChange)="investmentAmount.set($event[0] ?? 0)"
                        [min]="100000"
                        [max]="5000000"
                        [step]="25000"
                        [marks]="investmentMarks"
                        [tooltip]="investTooltip"
                      />
                    </div>
                    <div class="flex flex-wrap items-center gap-1.5 pt-1">
                      <span class="text-muted-foreground text-xs">Quick select:</span>
                      @for (amt of quickInvestments; track amt) {
                        <button
                          ui-button
                          variant="outline"
                          size="sm"
                          class="h-6 px-2 text-xs"
                          (click)="investmentAmount.set(amt)"
                        >
                          {{ quickInvestLabel(amt) }}
                        </button>
                      }
                    </div>
                  </div>
    
                  <ui-separator />
    
                  <!-- 2. Post-Money Valuation Cap -->
                  <div class="space-y-3.5">
                    <div class="flex items-center justify-between gap-4">
                      <div>
                        <span class="text-foreground text-sm font-medium">Post-Money Valuation Cap</span>
                        <p class="text-muted-foreground text-xs">Maximum valuation ceiling for conversion.</p>
                      </div>
                      <div class="w-36 sm:w-44">
                        <ui-input
                          id="safe-cap-input"
                          type="number"
                          size="small"
                          [min]="500000"
                          [max]="100000000"
                          [step]="500000"
                          prefix="$"
                          [value]="valuationCap()"
                          (valueChange)="valuationCap.set(+$event || 0)"
                        />
                      </div>
                    </div>
                    <div class="pb-5">
                      <ui-slider
                        [value]="[valuationCap()]"
                        (valueChange)="valuationCap.set($event[0] ?? 0)"
                        [min]="2000000"
                        [max]="50000000"
                        [step]="500000"
                        [marks]="valuationCapMarks"
                        [tooltip]="currencyTooltip"
                      />
                    </div>
                    <div class="flex flex-wrap items-center gap-1.5 pt-1">
                      <span class="text-muted-foreground text-xs">Cap benchmarks:</span>
                      @for (cap of capBenchmarks; track cap) {
                        <button
                          ui-button
                          variant="outline"
                          size="sm"
                          class="h-6 px-2 text-xs"
                          (click)="valuationCap.set(cap)"
                        >
                          \${{ cap / 1000000 }}M
                        </button>
                      }
                    </div>
                  </div>
    
                  <ui-separator />
    
                  <!-- 3. Discount Rate Input & Toggle -->
                  <div class="space-y-3.5">
                    <div class="flex items-center justify-between gap-4">
                      <div class="space-y-1">
                        <div class="flex items-center gap-2">
                          <span class="text-foreground text-sm font-medium">Discount Rate Clause</span>
                          @if (hasDiscount()) {
                            <span
                              ui-badge
                              wrap
                              variant="outline"
                              class="border-success/20 bg-success/10 text-success text-xs font-normal"
                            >
                              Active
                            </span>
                          } @else {
                            <span ui-badge wrap variant="outline" class="text-muted-foreground text-xs font-normal">
                              Cap Only
                            </span>
                          }
                        </div>
                        <p class="text-muted-foreground text-xs">
                          Percentage discount on Series A price if cap is not exceeded.
                        </p>
                      </div>
                      <div class="flex flex-wrap items-center gap-3">
                        <ui-switch
                          [checked]="hasDiscount()"
                          (checkedChange)="hasDiscount.set($event)"
                          aria-label="Toggle discount clause"
                        />
                        @if (hasDiscount()) {
                          <div class="w-24 sm:w-28">
                            <ui-input
                              id="safe-discount-input"
                              type="number"
                              size="small"
                              [min]="0"
                              [max]="50"
                              [step]="1"
                              suffix="%"
                              [value]="discountRate()"
                              (valueChange)="discountRate.set(+$event || 0)"
                            />
                          </div>
                        }
                      </div>
                    </div>
    
                    @if (hasDiscount()) {
                      <div class="space-y-3 pt-1">
                        <div class="pb-5">
                          <ui-slider
                            [value]="[discountRate()]"
                            (valueChange)="discountRate.set($event[0] ?? 0)"
                            [min]="5"
                            [max]="35"
                            [step]="1"
                            [marks]="discountRateMarks"
                            [tooltip]="discountTooltip"
                          />
                        </div>
                        <div class="flex flex-wrap items-center gap-1.5 pt-1">
                          <span class="text-muted-foreground text-xs">Standard rates:</span>
                          @for (r of standardRates; track r) {
                            <button
                              ui-button
                              [variant]="discountRate() === r ? 'default' : 'outline'"
                              size="sm"
                              class="h-6 px-2 text-xs"
                              (click)="discountRate.set(r)"
                            >
                              {{ r }}%
                            </button>
                          }
                        </div>
                      </div>
                    }
                  </div>
    
                  <ui-separator />
    
                  <!-- 4. Company Existing Capitalization -->
                  <div class="space-y-3.5">
                    <div class="flex items-center justify-between gap-4">
                      <div>
                        <span class="text-foreground text-sm font-medium">Company Existing Capitalization</span>
                        <p class="text-muted-foreground text-xs">Total pre-conversion issued shares and option pool.</p>
                      </div>
                      <div class="w-36 sm:w-44">
                        <ui-input
                          id="safe-shares-input"
                          type="number"
                          size="small"
                          [min]="1000000"
                          [max]="100000000"
                          [step]="500000"
                          suffix="Shares"
                          [value]="companyCapitalization()"
                          (valueChange)="companyCapitalization.set(+$event || 0)"
                        />
                      </div>
                    </div>
                    <div class="flex flex-wrap items-center gap-1.5 pt-1">
                      <span class="text-muted-foreground text-xs">Common share counts:</span>
                      @for (count of shareCounts; track count) {
                        <button
                          ui-button
                          [variant]="companyCapitalization() === count ? 'default' : 'outline'"
                          size="sm"
                          class="h-6 px-2 text-xs"
                          (click)="companyCapitalization.set(count)"
                        >
                          {{ count / 1000000 }}M Shares
                        </button>
                      }
                    </div>
                  </div>
    
                  <ui-separator />
    
                  <!-- 5. Series A Pre-Money Target -->
                  <div class="space-y-3.5">
                    <div class="flex items-center justify-between gap-4">
                      <div>
                        <span class="text-foreground text-sm font-medium">Series A Pre-Money Target</span>
                        <p class="text-muted-foreground text-xs">Target qualified financing equity valuation.</p>
                      </div>
                      <div class="w-36 sm:w-44">
                        <ui-input
                          id="safe-series-a-input"
                          type="number"
                          size="small"
                          [min]="1000000"
                          [max]="200000000"
                          [step]="1000000"
                          prefix="$"
                          [value]="seriesAPreMoney()"
                          (valueChange)="seriesAPreMoney.set(+$event || 0)"
                        />
                      </div>
                    </div>
                    <div class="pb-5">
                      <ui-slider
                        [value]="[seriesAPreMoney()]"
                        (valueChange)="seriesAPreMoney.set($event[0] ?? 0)"
                        [min]="5000000"
                        [max]="80000000"
                        [step]="1000000"
                        [marks]="seriesAMarks"
                        [tooltip]="currencyTooltip"
                      />
                    </div>
                    <div class="flex flex-wrap items-center gap-1.5 pt-1">
                      <span class="text-muted-foreground text-xs">Series A benchmarks:</span>
                      @for (target of seriesABenchmarks; track target) {
                        <button
                          ui-button
                          variant="outline"
                          size="sm"
                          class="h-6 px-2 text-xs"
                          (click)="seriesAPreMoney.set(target)"
                        >
                          \${{ target / 1000000 }}M
                        </button>
                      }
                    </div>
                  </div>
                </div>
              </div>
            </div>
    
            <!-- Right Results Card (Sticky on desktop) -->
            <div class="lg:sticky lg:top-8 lg:col-span-5">
              <div ui-card class="border-border bg-card shadow-xs">
                <div ui-card-header class="pb-4">
                  <div class="flex items-center justify-between">
                    <span class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
                      Investor Ownership Stake
                    </span>
                    <span ui-badge variant="secondary" [class]="governingBadgeClass">
                      {{ calculations().governingMechanism === 'cap' ? 'Cap Governs' : 'Discount Governs' }}
                    </span>
                  </div>
                  <div class="mt-4">
                    <div class="flex items-baseline gap-2">
                      <span class="text-foreground text-3xl font-bold tracking-tight tabular-nums sm:text-4xl">
                        {{ fmtPercent(calculations().safeOwnershipPct) }}
                      </span>
                      <span class="text-muted-foreground text-sm font-medium">Post-SAFE Ownership</span>
                    </div>
                    <p class="text-muted-foreground mt-1 text-xs">
                      SAFE investor receives
                      <span class="text-foreground font-semibold tabular-nums">
                        {{ fmtNumber(calculations().safeShares) }}
                      </span>
                      shares at conversion.
                    </p>
                  </div>
                </div>
    
                <div ui-card-content class="space-y-5">
                  <ui-separator />
    
                  <!-- Key Economics Metrics Grid -->
                  <div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
                    <div class="bg-muted/40 rounded-lg border p-3">
                      <span class="text-muted-foreground block text-xs">SAFE Conversion Price</span>
                      <span class="text-foreground text-base font-bold tabular-nums">
                        {{ fmtCurrency(calculations().conversionPrice) }}
                      </span>
                      <span class="text-muted-foreground block text-xs tabular-nums">
                        vs Series A {{ fmtCurrency(calculations().seriesAPricePerShare) }}
                      </span>
                    </div>
    
                    <div class="bg-muted/40 rounded-lg border p-3">
                      <span class="text-muted-foreground block text-xs">Converted Shares Issued</span>
                      <span class="text-foreground text-base font-bold tabular-nums">
                        {{ fmtNumber(calculations().safeShares) }}
                      </span>
                      <span class="text-muted-foreground block text-xs">Equity units</span>
                    </div>
    
                    <div class="bg-muted/40 rounded-lg border p-3">
                      <span class="text-muted-foreground block text-xs">Effective Discount</span>
                      <span class="text-success text-success text-base font-bold tabular-nums">
                        {{ fmtPercent(calculations().effectiveDiscountPct) }}
                      </span>
                      <span class="text-muted-foreground block text-xs">Savings vs Series A</span>
                    </div>
    
                    <div class="bg-muted/40 rounded-lg border p-3">
                      <span class="text-muted-foreground block text-xs">Implied Series A Value</span>
                      <span class="text-foreground text-base font-bold tabular-nums">
                        {{ fmtCurrencyWhole(calculations().impliedSeriesAValue) }}
                      </span>
                      <span class="text-success text-success block text-xs tabular-nums">
                        +{{ fmtCurrencyWhole(calculations().valueGain) }} ({{
                          calculations().paperRoiMultiple.toFixed(2)
                        }}x)
                      </span>
                    </div>
                  </div>
    
                  <ui-separator />
    
                  <!-- Visual Equity Distribution Bar -->
                  <div class="space-y-2.5">
                    <div class="flex items-center justify-between text-xs font-medium">
                      <span class="text-foreground">Founders vs. SAFE Investor</span>
                      <span class="text-muted-foreground tabular-nums">
                        {{ fmtPercent(calculations().founderOwnershipPct) }} /
                        {{ fmtPercent(calculations().safeOwnershipPct) }}
                      </span>
                    </div>
                    <div class="bg-muted relative flex h-3 w-full overflow-hidden rounded-full shadow-inner">
                      <div class="bg-primary h-full transition-[width] duration-300" [style.width]="founderBarWidth"></div>
                      <div class="bg-success h-full transition-[width] duration-300" [style.width]="safeBarWidth"></div>
                    </div>
                    <div
                      class="text-muted-foreground flex flex-col gap-1.5 text-xs sm:flex-row sm:items-center sm:justify-between"
                    >
                      <div class="flex items-center gap-1.5">
                        <span class="bg-primary size-2 shrink-0 rounded-full"></span>
                        <span>
                          Founders: {{ fmtPercent(calculations().founderOwnershipPct) }} ({{
                            fmtNumber(calculations().founderShares)
                          }}
                          sh)
                        </span>
                      </div>
                      <div class="flex items-center gap-1.5">
                        <span class="bg-success size-2 shrink-0 rounded-full"></span>
                        <span>
                          SAFE: {{ fmtPercent(calculations().safeOwnershipPct) }} ({{
                            fmtNumber(calculations().safeShares)
                          }}
                          sh)
                        </span>
                      </div>
                    </div>
                  </div>
    
                  <ui-separator />
    
                  <!-- Post-Conversion Dilution Breakdown Table -->
                  <div class="space-y-2">
                    <span class="text-foreground text-xs font-semibold tracking-wider uppercase">
                      Post-Conversion Dilution Breakdown
                    </span>
                    <div class="overflow-hidden rounded-lg border">
                      <div class="overflow-x-auto">
                        <table ui-table density="compact">
                          <thead ui-table-header>
                            <tr ui-table-row>
                              <th ui-table-head class="text-xs">Stakeholder</th>
                              <th ui-table-head class="text-right text-xs">Ownership</th>
                              <th ui-table-head class="text-right text-xs">Shares</th>
                              <th ui-table-head class="text-right text-xs">Implied Value</th>
                            </tr>
                          </thead>
                          <tbody ui-table-body>
                            <tr ui-table-row>
                              <td ui-table-cell class="text-xs font-medium">Founders & Existing</td>
                              <td ui-table-cell class="text-right text-xs font-medium tabular-nums">
                                {{ fmtPercent(calculations().founderOwnershipPct) }}
                              </td>
                              <td ui-table-cell class="text-right text-xs tabular-nums">
                                {{ fmtNumber(calculations().founderShares) }}
                              </td>
                              <td ui-table-cell class="text-right text-xs tabular-nums">
                                {{ fmtCurrencyWhole(calculations().founderShares * calculations().seriesAPricePerShare) }}
                              </td>
                            </tr>
                            <tr ui-table-row>
                              <td ui-table-cell class="text-success text-xs font-medium">SAFE Seed Investors</td>
                              <td ui-table-cell class="text-success text-success text-right text-xs font-bold tabular-nums">
                                {{ fmtPercent(calculations().safeOwnershipPct) }}
                              </td>
                              <td ui-table-cell class="text-right text-xs font-semibold tabular-nums">
                                {{ fmtNumber(calculations().safeShares) }}
                              </td>
                              <td ui-table-cell class="text-right text-xs font-semibold tabular-nums">
                                {{ fmtCurrencyWhole(calculations().impliedSeriesAValue) }}
                              </td>
                            </tr>
                          </tbody>
                          <tfoot ui-table-footer>
                            <tr ui-table-row>
                              <td ui-table-cell class="text-xs font-bold">Total Post-SAFE</td>
                              <td ui-table-cell class="text-right text-xs font-bold tabular-nums">100.00%</td>
                              <td ui-table-cell class="text-right text-xs font-bold tabular-nums">
                                {{ fmtNumber(calculations().totalShares) }}
                              </td>
                              <td ui-table-cell class="text-right text-xs font-bold tabular-nums">
                                {{ fmtCurrencyWhole(seriesAPreMoney()) }}
                              </td>
                            </tr>
                          </tfoot>
                        </table>
                      </div>
                    </div>
                  </div>
    
                  <ui-separator />
    
                  <!-- Key SAFE Takeaways Checklist -->
                  <ul class="text-muted-foreground space-y-2 text-xs">
                    <li class="flex items-center gap-2">
                      <lucide-icon [img]="CheckCircle2" class="text-primary size-3.5 shrink-0" />
                      <span>Post-money cap fixes ownership before Series A round</span>
                    </li>
                    <li class="flex items-center gap-2">
                      <lucide-icon [img]="CheckCircle2" class="text-primary size-3.5 shrink-0" />
                      <span>Converts to Preferred Stock with investor liquidation preference</span>
                    </li>
                    <li class="flex items-center gap-2">
                      <lucide-icon [img]="CheckCircle2" class="text-primary size-3.5 shrink-0" />
                      <span>No debt maturity dates, no interest accrual compounding</span>
                    </li>
                  </ul>
                </div>
    
                <div ui-card-footer class="flex flex-col gap-2.5 pt-2">
                  <button ui-button class="w-full gap-2 font-semibold shadow-xs" size="lg" (click)="copySummary()">
                    @if (copied()) {
                      <lucide-icon [img]="Check" class="text-success size-4" />
                    } @else {
                      <lucide-icon [img]="Copy" class="size-4" />
                    }
                    {{ copied() ? 'Copied Summary to Clipboard!' : 'Copy Valuation Breakdown' }}
                  </button>
                  <button ui-button variant="outline" class="w-full gap-2" size="default">
                    <lucide-icon [img]="FileSpreadsheet" class="size-4" />
                    Export Cap Table Summary
                  </button>
                </div>
              </div>
            </div>
          </div>
    
          <!-- Bottom Section: Series A Valuation Sensitivity Table -->
          <div ui-card>
            <div ui-card-header class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
              <div>
                <h3 ui-card-title class="flex items-center gap-2">
                  <lucide-icon [img]="TrendingUp" class="text-primary size-5" />
                  Series A Qualified Financing Sensitivity Matrix
                </h3>
                <p ui-card-description>
                  Simulate how SAFE investor ownership percentage, share price, and conversion mechanism respond across
                  different next-round valuations.
                </p>
              </div>
            </div>
            <div ui-card-content>
              <div class="overflow-x-auto">
                <table ui-table density="cozy">
                  <thead ui-table-header>
                    <tr ui-table-row>
                      <th ui-table-head>Series A Valuation</th>
                      <th ui-table-head class="text-right">Series A Price</th>
                      <th ui-table-head class="text-right">SAFE Conversion Price</th>
                      <th ui-table-head>Governing Term</th>
                      <th ui-table-head class="text-right">Shares Issued</th>
                      <th ui-table-head class="text-right">SAFE Ownership</th>
                      <th ui-table-head class="text-right">Effective Savings</th>
                      <th ui-table-head class="text-right">Implied Stake Value</th>
                    </tr>
                  </thead>
                  <tbody ui-table-body>
                    @for (row of scenarioResults(); track row.valuation) {
                      <tr ui-table-row [class]="row.isCurrentTarget ? 'bg-primary/5 font-medium' : ''">
                        <td ui-table-cell class="font-medium">
                          <div class="flex items-center gap-2">
                            <span class="tabular-nums">{{ fmtCurrencyWhole(row.valuation) }}</span>
                            @if (row.isCurrentTarget) {
                              <span ui-badge wrap variant="default" class="h-5 px-1.5 text-xs"> Current Target </span>
                            }
                          </div>
                        </td>
                        <td ui-table-cell class="text-right tabular-nums">{{ fmtCurrency(row.seriesAPrice) }}</td>
                        <td ui-table-cell class="text-foreground text-right font-semibold tabular-nums">
                          {{ fmtCurrency(row.convPrice) }}
                        </td>
                        <td ui-table-cell>
                          <span ui-badge variant="outline" [class]="scenarioBadgeClass(row.mechanism)">
                            {{ row.mechanism }}
                          </span>
                        </td>
                        <td ui-table-cell class="text-right tabular-nums">{{ fmtNumber(row.issuedShares) }}</td>
                        <td ui-table-cell class="text-foreground text-right font-bold tabular-nums">
                          {{ fmtPercent(row.ownershipPct) }}
                        </td>
                        <td ui-table-cell class="text-success text-success text-right font-medium tabular-nums">
                          {{ fmtPercent(row.effectiveDiscount) }}
                        </td>
                        <td ui-table-cell class="text-right font-semibold tabular-nums">
                          {{ fmtCurrencyWhole(row.impliedValue) }}
                        </td>
                      </tr>
                    }
                  </tbody>
                </table>
              </div>
            </div>
          </div>
    
          <!-- Post-Money SAFE Legal & Governance Guide -->
          <div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
            <div ui-card class="p-4">
              <div class="flex items-start gap-3">
                <div class="bg-primary/10 text-primary mt-0.5 rounded-md p-2">
                  <lucide-icon [img]="ShieldCheck" class="size-4" />
                </div>
                <div class="space-y-1">
                  <h4 class="text-foreground text-sm font-semibold">Post-Money Cap Protection</h4>
                  <p class="text-muted-foreground text-xs leading-relaxed">
                    Acts as an ownership percentage guarantee for seed investors prior to the priced round, regardless of
                    subsequent SAFE issuances.
                  </p>
                </div>
              </div>
            </div>
    
            <div ui-card class="p-4">
              <div class="flex items-start gap-3">
                <div class="bg-success/10 text-success mt-0.5 rounded-md p-2">
                  <lucide-icon [img]="Percent" class="size-4" />
                </div>
                <div class="space-y-1">
                  <h4 class="text-foreground text-sm font-semibold">Discount Rate Safety Net</h4>
                  <p class="text-muted-foreground text-xs leading-relaxed">
                    Protects investors if Series A prices below the valuation cap, guaranteeing a 15%–25% discount off the
                    institutional share price.
                  </p>
                </div>
              </div>
            </div>
    
            <div ui-card class="p-4">
              <div class="flex items-start gap-3">
                <div class="bg-warning/10 text-warning mt-0.5 rounded-md p-2">
                  <lucide-icon [img]="PieChart" class="size-4" />
                </div>
                <div class="space-y-1">
                  <h4 class="text-foreground text-sm font-semibold">Founder Dilution Transparency</h4>
                  <p class="text-muted-foreground text-xs leading-relaxed">
                    YC v1.1 post-money SAFEs dilute only founders and common shareholders, eliminating pre-money SAFE
                    circular dilution surprises.
                  </p>
                </div>
              </div>
            </div>
          </div>
        </div>
      `,
    })
    export class UiSafeNoteCalculatorComponent {
      protected readonly BadgePercent = BadgePercent
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly Copy = Copy
      protected readonly DollarSign = DollarSign
      protected readonly FileSpreadsheet = FileSpreadsheet
      protected readonly Layers = Layers
      protected readonly Percent = Percent
      protected readonly PieChart = PieChart
      protected readonly Scale = Scale
      protected readonly ShieldCheck = ShieldCheck
      protected readonly TrendingUp = TrendingUp
    
      protected readonly investmentMarks = INVESTMENT_MARKS
      protected readonly valuationCapMarks = VALUATION_CAP_MARKS
      protected readonly discountRateMarks = DISCOUNT_RATE_MARKS
      protected readonly seriesAMarks = SERIES_A_MARKS
      protected readonly quickInvestments = [250000, 500000, 1000000, 1500000, 2000000, 3000000]
      protected readonly capBenchmarks = [5000000, 8000000, 12000000, 15000000, 20000000, 30000000]
      protected readonly standardRates = [10, 15, 20, 25, 30]
      protected readonly shareCounts = [5000000, 8000000, 10000000, 12000000, 15000000]
      protected readonly seriesABenchmarks = [15000000, 20000000, 25000000, 35000000, 50000000, 75000000]
    
      protected readonly investTooltip = (val: number): string => `$${val.toLocaleString('en-US')}`
      protected readonly currencyTooltip = (val: number): string => `$${val.toLocaleString('en-US')}`
      protected readonly discountTooltip = (val: number): string => `${val}% Discount`
    
      @Input('class') className?: string
    
      readonly selectedPreset = signal<RoundPreset>('seed-standard')
      readonly investmentAmount = signal(1_000_000)
      readonly valuationCap = signal(15_000_000)
      readonly hasDiscount = signal(true)
      readonly discountRate = signal(20)
      readonly companyCapitalization = signal(10_000_000)
      readonly seriesAPreMoney = signal(25_000_000)
      readonly copied = signal(false)
    
      private copyTimer: ReturnType<typeof setTimeout> | undefined
    
      constructor() {
        inject(DestroyRef).onDestroy(() => clearTimeout(this.copyTimer))
      }
    
      handlePresetChange(value: string): void {
        const presetId = value as RoundPreset
        this.selectedPreset.set(presetId)
        const preset = PRESETS[presetId]
        if (preset) {
          this.investmentAmount.set(preset.investment)
          this.valuationCap.set(preset.valuationCap)
          this.hasDiscount.set(preset.hasDiscount)
          this.discountRate.set(preset.discountRate)
          this.companyCapitalization.set(preset.shares)
          this.seriesAPreMoney.set(preset.seriesAPreMoney)
        }
      }
    
      // Core SAFE conversion economics
      readonly calculations = computed(() => {
        const inv = Math.max(1, this.investmentAmount())
        const cap = Math.max(1, this.valuationCap())
        const shares = Math.max(1, this.companyCapitalization())
        const seriesA = Math.max(1, this.seriesAPreMoney())
        const disc = this.hasDiscount() ? Math.max(0, Math.min(99, this.discountRate())) : 0
    
        // Series A unqualified price per share
        const seriesAPricePerShare = seriesA / shares
    
        // Cap conversion price per share
        const capPricePerShare = cap / shares
    
        // Discount conversion price per share
        const discountPricePerShare = this.hasDiscount() ? seriesAPricePerShare * (1 - disc / 100) : seriesAPricePerShare
    
        // In YC Post-Money SAFE, investor converts at the lower of Cap Price or Discount Price
        let conversionPrice = capPricePerShare
        let governingMechanism: 'cap' | 'discount' = 'cap'
    
        if (this.hasDiscount() && discountPricePerShare < capPricePerShare) {
          conversionPrice = discountPricePerShare
          governingMechanism = 'discount'
        } else {
          conversionPrice = capPricePerShare
          governingMechanism = 'cap'
        }
    
        // Converted shares issued
        const safeShares = conversionPrice > 0 ? Math.round(inv / conversionPrice) : 0
    
        // Equity percentage calculation
        const safeOwnershipPct = Math.min(100, Math.max(0, (safeShares / shares) * 100))
        const founderShares = Math.max(0, shares - safeShares)
        const founderOwnershipPct = Math.max(0, 100 - safeOwnershipPct)
    
        // Effective discount achieved vs Series A price
        const effectiveDiscountPct =
          seriesAPricePerShare > 0
            ? Math.max(0, ((seriesAPricePerShare - conversionPrice) / seriesAPricePerShare) * 100)
            : 0
    
        // Implied Value of SAFE stake at Series A price
        const impliedSeriesAValue = safeShares * seriesAPricePerShare
        const valueGain = Math.max(0, impliedSeriesAValue - inv)
        const paperRoiMultiple = inv > 0 ? impliedSeriesAValue / inv : 1
    
        return {
          seriesAPricePerShare,
          capPricePerShare,
          discountPricePerShare,
          conversionPrice,
          governingMechanism,
          safeShares,
          founderShares,
          totalShares: shares,
          safeOwnershipPct,
          founderOwnershipPct,
          effectiveDiscountPct,
          impliedSeriesAValue,
          valueGain,
          paperRoiMultiple,
        }
      })
    
      // Sensitivity scenarios across different Series A pre-money valuations
      readonly scenarioResults = computed(() => {
        const inv = Math.max(1, this.investmentAmount())
        const cap = Math.max(1, this.valuationCap())
        const shares = Math.max(1, this.companyCapitalization())
        const disc = this.hasDiscount() ? Math.max(0, Math.min(99, this.discountRate())) : 0
    
        return SCENARIO_VALUATIONS.map((scenarioValuation) => {
          const seriesAPrice = scenarioValuation / shares
          const capPrice = cap / shares
          const discountPrice = this.hasDiscount() ? seriesAPrice * (1 - disc / 100) : seriesAPrice
    
          let convPrice = capPrice
          let mechanism = 'Valuation Cap'
          if (this.hasDiscount() && discountPrice < capPrice) {
            convPrice = discountPrice
            mechanism = `Discount (${disc}%)`
          }
    
          const issuedShares = convPrice > 0 ? Math.round(inv / convPrice) : 0
          const ownershipPct = (issuedShares / shares) * 100
          const effectiveDiscount = seriesAPrice > 0 ? Math.max(0, ((seriesAPrice - convPrice) / seriesAPrice) * 100) : 0
          const impliedValue = issuedShares * seriesAPrice
          const isCurrentTarget = scenarioValuation === this.seriesAPreMoney()
    
          return {
            valuation: scenarioValuation,
            seriesAPrice,
            convPrice,
            mechanism,
            issuedShares,
            ownershipPct,
            effectiveDiscount,
            impliedValue,
            isCurrentTarget,
          }
        })
      })
    
      get rootClass(): string {
        return cn('mx-auto w-full max-w-6xl space-y-8 p-4 sm:p-6 lg:p-8', this.className)
      }
    
      get governingBadgeClass(): string {
        return cn(
          this.calculations().governingMechanism === 'cap'
            ? 'bg-primary/10 text-primary border-primary/20 text-xs font-semibold'
            : 'border-success/20 bg-success/10 text-success text-xs font-semibold',
        )
      }
    
      get founderBarWidth(): string {
        return `${this.calculations().founderOwnershipPct}%`
      }
    
      get safeBarWidth(): string {
        return `${this.calculations().safeOwnershipPct}%`
      }
    
      scenarioBadgeClass(mechanism: string): string {
        return cn(
          mechanism === 'Valuation Cap'
            ? 'border-primary/20 bg-primary/10 text-primary text-xs'
            : 'border-success/20 bg-success/10 text-success text-xs',
        )
      }
    
      quickInvestLabel(amt: number): string {
        return amt >= 1000000 ? `$${(amt / 1000000).toFixed(amt % 1000000 === 0 ? 0 : 1)}M` : `$${amt / 1000}K`
      }
    
      fmtCurrency(val: number): string {
        return new Intl.NumberFormat('en-US', {
          style: 'currency',
          currency: 'USD',
          minimumFractionDigits: 2,
          maximumFractionDigits: 2,
        }).format(val || 0)
      }
    
      fmtCurrencyWhole(val: number): string {
        return new Intl.NumberFormat('en-US', {
          style: 'currency',
          currency: 'USD',
          minimumFractionDigits: 0,
          maximumFractionDigits: 0,
        }).format(val || 0)
      }
    
      fmtPercent(val: number, decimals = 2): string {
        return `${(val || 0).toFixed(decimals)}%`
      }
    
      fmtNumber(val: number): string {
        return new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(val || 0)
      }
    
      copySummary(): void {
        const c = this.calculations()
        const text = `YC Post-Money SAFE Conversion Summary:
    - Investment Amount: ${this.fmtCurrency(this.investmentAmount())}
    - Valuation Cap: ${this.fmtCurrency(this.valuationCap())}
    - Series A Target: ${this.fmtCurrency(this.seriesAPreMoney())} Pre-Money
    - Investor Ownership: ${this.fmtPercent(c.safeOwnershipPct)} (${this.fmtNumber(c.safeShares)} shares)
    - Conversion Price: ${this.fmtCurrency(c.conversionPrice)} / share (vs Series A ${this.fmtCurrency(c.seriesAPricePerShare)})
    - Effective Discount: ${this.fmtPercent(c.effectiveDiscountPct)}
    - Governing Term: ${c.governingMechanism === 'cap' ? 'Valuation Cap' : 'Discount Rate'}`
    
        try {
          navigator.clipboard?.writeText(text)
        } catch {
          // Clipboard unavailable in some contexts; the copied state still confirms the action.
        }
        this.copied.set(true)
        clearTimeout(this.copyTimer)
        this.copyTimer = setTimeout(() => {
          this.copied.set(false)
        }, 2000)
      }
    }
    
  • src/app/components/blocks/safe-note-calculator/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/safe-note-calculator.json