'use client'; import React, { useEffect, useRef, useState, useId } from 'react'; import { ComponentShareButton } from './ComponentShareButton'; interface BoldNumberProps { /** The number to display, e.g. "53%" or "2.5M€" or "-20%" */ value: string; /** Short description of what this number means */ label: string; /** Source attribution */ source?: string; /** Source URL */ sourceUrl?: string; className?: string; } /** * Premium hero number component — full-width, dark gradient, animated count-up. * Designed for shareable key statistics that stand out in blog posts. */ export const BoldNumber: React.FC = ({ value, label, source, sourceUrl, className = '', }) => { const ref = useRef(null); const [isVisible, setIsVisible] = useState(false); const [displayValue, setDisplayValue] = useState(''); const shareId = `boldnum-${useId().replace(/:/g, '')}`; // Extract numeric part for animation const numericMatch = value.match(/^([+-]?)(\d+(?:[.,]\d+)?)(.*)/); const prefix = numericMatch?.[1] ?? ''; const numStr = numericMatch?.[2] ?? ''; const suffix = numericMatch?.[3] ?? value; const targetNum = parseFloat(numStr.replace(',', '.')) || 0; const hasDecimals = numStr.includes('.') || numStr.includes(','); useEffect(() => { const el = ref.current; if (!el) return; const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setIsVisible(true); observer.disconnect(); } }, { threshold: 0.3 } ); observer.observe(el); return () => observer.disconnect(); }, []); useEffect(() => { if (!isVisible || !numStr) { setDisplayValue(value); return; } const duration = 1200; const steps = 40; const stepTime = duration / steps; let step = 0; const timer = setInterval(() => { step++; const progress = Math.min(step / steps, 1); // Ease out cubic const eased = 1 - Math.pow(1 - progress, 3); const current = targetNum * eased; const formatted = hasDecimals ? current.toFixed(1) : Math.round(current).toString(); setDisplayValue(`${prefix}${formatted}${suffix}`); if (step >= steps) { clearInterval(timer); setDisplayValue(value); } }, stepTime); return () => clearInterval(timer); }, [isVisible, value, prefix, suffix, targetNum, hasDecimals, numStr]); return (
{displayValue || value} {label} {source && ( {sourceUrl ? ( Quelle: {source} ↗ ) : ( `Quelle: ${source}` )} )}
); };