Files
breakpilot-core/pitch-deck/components/ui/RunwayGauge.tsx
Benjamin Boenisch f2a24d7341 feat: add pitch-deck service to core infrastructure
Migrated pitch-deck from breakpilot-pwa to breakpilot-core.
Container: bp-core-pitch-deck on port 3012.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 19:44:27 +01:00

134 lines
4.5 KiB
TypeScript

'use client'
import { motion } from 'framer-motion'
import { useEffect, useState } from 'react'
interface RunwayGaugeProps {
months: number
maxMonths?: number
size?: number
label?: string
}
export default function RunwayGauge({ months, maxMonths = 36, size = 140, label = 'Runway' }: RunwayGaugeProps) {
const [animatedAngle, setAnimatedAngle] = useState(0)
const clampedMonths = Math.min(months, maxMonths)
const targetAngle = (clampedMonths / maxMonths) * 270 - 135 // -135 to +135 degrees
useEffect(() => {
const timer = setTimeout(() => setAnimatedAngle(targetAngle), 100)
return () => clearTimeout(timer)
}, [targetAngle])
// Color based on runway
const getColor = () => {
if (months >= 18) return '#22c55e' // green
if (months >= 12) return '#eab308' // yellow
if (months >= 6) return '#f97316' // orange
return '#ef4444' // red
}
const color = getColor()
const cx = size / 2
const cy = size / 2
const radius = (size / 2) - 16
const needleLength = radius - 10
// Arc path for gauge background
const startAngle = -135
const endAngle = 135
const polarToCartesian = (cx: number, cy: number, r: number, deg: number) => {
const rad = (deg - 90) * Math.PI / 180
return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) }
}
const arcStart = polarToCartesian(cx, cy, radius, startAngle)
const arcEnd = polarToCartesian(cx, cy, radius, endAngle)
const arcPath = `M ${arcStart.x} ${arcStart.y} A ${radius} ${radius} 0 1 1 ${arcEnd.x} ${arcEnd.y}`
// Filled arc
const filledEnd = polarToCartesian(cx, cy, radius, Math.min(animatedAngle, endAngle))
const largeArc = (animatedAngle - startAngle) > 180 ? 1 : 0
const filledPath = `M ${arcStart.x} ${arcStart.y} A ${radius} ${radius} 0 ${largeArc} 1 ${filledEnd.x} ${filledEnd.y}`
// Needle endpoint
const needleRad = (animatedAngle - 90) * Math.PI / 180
const needleX = cx + needleLength * Math.cos(needleRad)
const needleY = cy + needleLength * Math.sin(needleRad)
const shouldPulse = months < 6
return (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="flex flex-col items-center"
>
<div className={`relative ${shouldPulse ? 'animate-pulse' : ''}`} style={{ width: size, height: size * 0.8 }}>
<svg width={size} height={size * 0.8} viewBox={`0 0 ${size} ${size * 0.8}`}>
{/* Background arc */}
<path d={arcPath} fill="none" stroke="rgba(255,255,255,0.1)" strokeWidth="8" strokeLinecap="round" />
{/* Filled arc */}
<motion.path
d={filledPath}
fill="none"
stroke={color}
strokeWidth="8"
strokeLinecap="round"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 1.5, ease: 'easeOut' }}
/>
{/* Tick marks */}
{[0, 6, 12, 18, 24, 30, 36].map((tick) => {
const tickAngle = (tick / maxMonths) * 270 - 135
const inner = polarToCartesian(cx, cy, radius - 12, tickAngle)
const outer = polarToCartesian(cx, cy, radius - 6, tickAngle)
return (
<g key={tick}>
<line x1={inner.x} y1={inner.y} x2={outer.x} y2={outer.y} stroke="rgba(255,255,255,0.3)" strokeWidth="1.5" />
<text
x={polarToCartesian(cx, cy, radius - 22, tickAngle).x}
y={polarToCartesian(cx, cy, radius - 22, tickAngle).y}
fill="rgba(255,255,255,0.3)"
fontSize="8"
textAnchor="middle"
dominantBaseline="central"
>
{tick}
</text>
</g>
)
})}
{/* Needle */}
<motion.line
x1={cx}
y1={cy}
x2={needleX}
y2={needleY}
stroke="white"
strokeWidth="2"
strokeLinecap="round"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
/>
{/* Center circle */}
<circle cx={cx} cy={cy} r="4" fill={color} />
<circle cx={cx} cy={cy} r="2" fill="white" />
</svg>
</div>
<div className="text-center -mt-2">
<p className="text-lg font-bold" style={{ color }}>{Math.round(months)}</p>
<p className="text-[10px] text-white/40 uppercase tracking-wider">{label}</p>
</div>
</motion.div>
)
}