499 lines
19 KiB
TypeScript
499 lines
19 KiB
TypeScript
|
|
import { useState, useCallback } from 'react';
|
||
|
|
import { Route } from 'lucide-react';
|
||
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
||
|
|
import type { VesselData, TripEstimate, CurrencyCode } from '../types';
|
||
|
|
import { calculateTripCarbon, calculateCarbonFromFuel } from '../utils/carbonCalculator';
|
||
|
|
import { currencies, formatCurrency } from '../utils/currencies';
|
||
|
|
import { CurrencySelect } from './CurrencySelect';
|
||
|
|
|
||
|
|
interface Props {
|
||
|
|
vesselData: VesselData;
|
||
|
|
onOffsetClick?: (tons: number, monetaryAmount?: number) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function TripCalculator({ vesselData, onOffsetClick }: Props) {
|
||
|
|
const [calculationType, setCalculationType] = useState<'fuel' | 'distance' | 'custom'>('fuel');
|
||
|
|
const [distance, setDistance] = useState<string>('');
|
||
|
|
const [speed, setSpeed] = useState<string>('12');
|
||
|
|
const [fuelRate, setFuelRate] = useState<string>('100');
|
||
|
|
const [fuelAmount, setFuelAmount] = useState<string>('');
|
||
|
|
const [fuelUnit, setFuelUnit] = useState<'liters' | 'gallons'>('liters');
|
||
|
|
const [tripEstimate, setTripEstimate] = useState<TripEstimate | null>(null);
|
||
|
|
const [currency, setCurrency] = useState<CurrencyCode>('USD');
|
||
|
|
const [offsetPercentage, setOffsetPercentage] = useState<number>(100);
|
||
|
|
const [customPercentage, setCustomPercentage] = useState<string>('');
|
||
|
|
const [customAmount, setCustomAmount] = useState<string>('');
|
||
|
|
|
||
|
|
const handleCalculate = useCallback((e: React.FormEvent) => {
|
||
|
|
e.preventDefault();
|
||
|
|
if (calculationType === 'distance') {
|
||
|
|
const estimate = calculateTripCarbon(
|
||
|
|
vesselData,
|
||
|
|
Number(distance),
|
||
|
|
Number(speed),
|
||
|
|
Number(fuelRate)
|
||
|
|
);
|
||
|
|
setTripEstimate(estimate);
|
||
|
|
} else if (calculationType === 'fuel') {
|
||
|
|
const co2Emissions = calculateCarbonFromFuel(Number(fuelAmount), fuelUnit === 'gallons');
|
||
|
|
setTripEstimate({
|
||
|
|
distance: 0,
|
||
|
|
duration: 0,
|
||
|
|
fuelConsumption: Number(fuelAmount),
|
||
|
|
co2Emissions
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}, [calculationType, distance, speed, fuelRate, fuelAmount, fuelUnit, vesselData]);
|
||
|
|
|
||
|
|
const handleCustomPercentageChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||
|
|
const value = e.target.value;
|
||
|
|
if (value === '' || (Number(value) >= 0 && Number(value) <= 100)) {
|
||
|
|
setCustomPercentage(value);
|
||
|
|
if (value !== '') {
|
||
|
|
setOffsetPercentage(Number(value));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const handlePresetPercentage = useCallback((percentage: number) => {
|
||
|
|
setOffsetPercentage(percentage);
|
||
|
|
setCustomPercentage('');
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const calculateOffsetAmount = useCallback((emissions: number, percentage: number) => {
|
||
|
|
return (emissions * percentage) / 100;
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const handleCustomAmountChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||
|
|
const value = e.target.value;
|
||
|
|
if (value === '' || Number(value) >= 0) {
|
||
|
|
setCustomAmount(value);
|
||
|
|
}
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
// Animation variants
|
||
|
|
const fadeIn = {
|
||
|
|
hidden: { opacity: 0 },
|
||
|
|
visible: {
|
||
|
|
opacity: 1,
|
||
|
|
transition: { duration: 0.5 }
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const slideIn = {
|
||
|
|
hidden: { opacity: 0, y: 20 },
|
||
|
|
visible: {
|
||
|
|
opacity: 1,
|
||
|
|
y: 0,
|
||
|
|
transition: {
|
||
|
|
duration: 0.5,
|
||
|
|
ease: [0.25, 0.1, 0.25, 1.0]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<motion.div
|
||
|
|
className="bg-white rounded-lg shadow-xl p-6 max-w-2xl w-full mt-8"
|
||
|
|
initial={{ opacity: 0, y: 30 }}
|
||
|
|
animate={{ opacity: 1, y: 0 }}
|
||
|
|
transition={{
|
||
|
|
duration: 0.6,
|
||
|
|
ease: [0.22, 1, 0.36, 1]
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<div className="flex items-center justify-between mb-6">
|
||
|
|
<h2 className="text-2xl font-bold text-gray-800">Carbon Offset Calculator</h2>
|
||
|
|
<motion.div
|
||
|
|
initial={{ rotate: -10, opacity: 0 }}
|
||
|
|
animate={{ rotate: 0, opacity: 1 }}
|
||
|
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||
|
|
>
|
||
|
|
<Route className="text-blue-500" size={24} />
|
||
|
|
</motion.div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<motion.div
|
||
|
|
className="mb-6"
|
||
|
|
variants={fadeIn}
|
||
|
|
initial="hidden"
|
||
|
|
animate="visible"
|
||
|
|
>
|
||
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
|
|
Calculation Method
|
||
|
|
</label>
|
||
|
|
<div className="flex flex-wrap gap-3">
|
||
|
|
<motion.button
|
||
|
|
type="button"
|
||
|
|
onClick={() => setCalculationType('fuel')}
|
||
|
|
className={`px-4 py-2 rounded-lg transition-colors ${
|
||
|
|
calculationType === 'fuel'
|
||
|
|
? 'bg-blue-500 text-white'
|
||
|
|
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||
|
|
}`}
|
||
|
|
whileHover={{ scale: 1.05 }}
|
||
|
|
whileTap={{ scale: 0.95 }}
|
||
|
|
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
||
|
|
>
|
||
|
|
Fuel Based
|
||
|
|
</motion.button>
|
||
|
|
<motion.button
|
||
|
|
type="button"
|
||
|
|
onClick={() => setCalculationType('distance')}
|
||
|
|
className={`px-4 py-2 rounded-lg transition-colors ${
|
||
|
|
calculationType === 'distance'
|
||
|
|
? 'bg-blue-500 text-white'
|
||
|
|
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||
|
|
}`}
|
||
|
|
whileHover={{ scale: 1.05 }}
|
||
|
|
whileTap={{ scale: 0.95 }}
|
||
|
|
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
||
|
|
>
|
||
|
|
Distance Based
|
||
|
|
</motion.button>
|
||
|
|
<motion.button
|
||
|
|
type="button"
|
||
|
|
onClick={() => setCalculationType('custom')}
|
||
|
|
className={`px-4 py-2 rounded-lg transition-colors ${
|
||
|
|
calculationType === 'custom'
|
||
|
|
? 'bg-blue-500 text-white'
|
||
|
|
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||
|
|
}`}
|
||
|
|
whileHover={{ scale: 1.05 }}
|
||
|
|
whileTap={{ scale: 0.95 }}
|
||
|
|
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
||
|
|
>
|
||
|
|
Custom Amount
|
||
|
|
</motion.button>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
|
||
|
|
<AnimatePresence mode="wait">
|
||
|
|
{calculationType === 'custom' ? (
|
||
|
|
<motion.div
|
||
|
|
key="custom"
|
||
|
|
className="space-y-4"
|
||
|
|
initial={{ opacity: 0, height: 0 }}
|
||
|
|
animate={{ opacity: 1, height: "auto" }}
|
||
|
|
exit={{ opacity: 0, height: 0 }}
|
||
|
|
transition={{ duration: 0.3 }}
|
||
|
|
>
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
|
|
Select Currency
|
||
|
|
</label>
|
||
|
|
<div className="max-w-xs">
|
||
|
|
<CurrencySelect value={currency} onChange={setCurrency} />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
|
|
Enter Amount to Offset
|
||
|
|
</label>
|
||
|
|
<div className="relative rounded-md shadow-sm">
|
||
|
|
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||
|
|
<span className="text-gray-500 sm:text-sm">{currencies[currency].symbol}</span>
|
||
|
|
</div>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
value={customAmount}
|
||
|
|
onChange={handleCustomAmountChange}
|
||
|
|
placeholder="Enter amount"
|
||
|
|
min="0"
|
||
|
|
className="mt-1 block w-full rounded-md border-gray-300 pl-7 pr-12 focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
||
|
|
required
|
||
|
|
/>
|
||
|
|
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
||
|
|
<span className="text-gray-500 sm:text-sm">{currency}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{customAmount && Number(customAmount) > 0 && (
|
||
|
|
<motion.button
|
||
|
|
onClick={() => onOffsetClick?.(0, Number(customAmount))}
|
||
|
|
className="w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition-colors mt-6"
|
||
|
|
whileHover={{ scale: 1.03 }}
|
||
|
|
whileTap={{ scale: 0.97 }}
|
||
|
|
>
|
||
|
|
Offset Your Impact
|
||
|
|
</motion.button>
|
||
|
|
)}
|
||
|
|
</motion.div>
|
||
|
|
) : (
|
||
|
|
<motion.form
|
||
|
|
key="calculator"
|
||
|
|
onSubmit={handleCalculate}
|
||
|
|
className="space-y-4"
|
||
|
|
initial={{ opacity: 0 }}
|
||
|
|
animate={{ opacity: 1 }}
|
||
|
|
exit={{ opacity: 0 }}
|
||
|
|
transition={{ duration: 0.3 }}
|
||
|
|
>
|
||
|
|
{calculationType === 'fuel' && (
|
||
|
|
<motion.div
|
||
|
|
initial={{ opacity: 0, y: 10 }}
|
||
|
|
animate={{ opacity: 1, y: 0 }}
|
||
|
|
transition={{ duration: 0.3 }}
|
||
|
|
>
|
||
|
|
<label className="block text-sm font-medium text-gray-700">
|
||
|
|
Fuel Consumption
|
||
|
|
</label>
|
||
|
|
<div className="flex space-x-4">
|
||
|
|
<div className="flex-1">
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
min="1"
|
||
|
|
value={fuelAmount}
|
||
|
|
onChange={(e) => setFuelAmount(e.target.value)}
|
||
|
|
placeholder="Enter amount"
|
||
|
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200"
|
||
|
|
required
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<select
|
||
|
|
value={fuelUnit}
|
||
|
|
onChange={(e) => setFuelUnit(e.target.value as 'liters' | 'gallons')}
|
||
|
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200"
|
||
|
|
>
|
||
|
|
<option value="liters">Liters</option>
|
||
|
|
<option value="gallons">Gallons</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{calculationType === 'distance' && (
|
||
|
|
<>
|
||
|
|
<motion.div
|
||
|
|
initial={{ opacity: 0, y: 10 }}
|
||
|
|
animate={{ opacity: 1, y: 0 }}
|
||
|
|
transition={{ duration: 0.3, delay: 0.1 }}
|
||
|
|
>
|
||
|
|
<label className="block text-sm font-medium text-gray-700">
|
||
|
|
Distance (nautical miles)
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
min="1"
|
||
|
|
value={distance}
|
||
|
|
onChange={(e) => setDistance(e.target.value)}
|
||
|
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200"
|
||
|
|
required
|
||
|
|
/>
|
||
|
|
</motion.div>
|
||
|
|
|
||
|
|
<motion.div
|
||
|
|
initial={{ opacity: 0, y: 10 }}
|
||
|
|
animate={{ opacity: 1, y: 0 }}
|
||
|
|
transition={{ duration: 0.3, delay: 0.2 }}
|
||
|
|
>
|
||
|
|
<label className="block text-sm font-medium text-gray-700">
|
||
|
|
Average Speed (knots)
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
min="1"
|
||
|
|
max="50"
|
||
|
|
value={speed}
|
||
|
|
onChange={(e) => setSpeed(e.target.value)}
|
||
|
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200"
|
||
|
|
required
|
||
|
|
/>
|
||
|
|
</motion.div>
|
||
|
|
|
||
|
|
<motion.div
|
||
|
|
initial={{ opacity: 0, y: 10 }}
|
||
|
|
animate={{ opacity: 1, y: 0 }}
|
||
|
|
transition={{ duration: 0.3, delay: 0.3 }}
|
||
|
|
>
|
||
|
|
<label className="block text-sm font-medium text-gray-700">
|
||
|
|
Fuel Consumption Rate (liters per hour)
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
min="1"
|
||
|
|
step="1"
|
||
|
|
value={fuelRate}
|
||
|
|
onChange={(e) => setFuelRate(e.target.value)}
|
||
|
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200"
|
||
|
|
required
|
||
|
|
/>
|
||
|
|
<p className="mt-1 text-sm text-gray-500">
|
||
|
|
Typical range: 50 - 500 liters per hour for most yachts
|
||
|
|
</p>
|
||
|
|
</motion.div>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<motion.div
|
||
|
|
initial={{ opacity: 0, y: 10 }}
|
||
|
|
animate={{ opacity: 1, y: 0 }}
|
||
|
|
transition={{ duration: 0.3, delay: 0.4 }}
|
||
|
|
>
|
||
|
|
<label className="block text-sm font-medium text-gray-700">
|
||
|
|
Select Currency
|
||
|
|
</label>
|
||
|
|
<div className="max-w-xs">
|
||
|
|
<CurrencySelect value={currency} onChange={setCurrency} />
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
|
||
|
|
<motion.button
|
||
|
|
type="submit"
|
||
|
|
className="w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition-colors"
|
||
|
|
whileHover={{ scale: 1.03 }}
|
||
|
|
whileTap={{ scale: 0.97 }}
|
||
|
|
initial={{ opacity: 0, y: 10 }}
|
||
|
|
animate={{ opacity: 1, y: 0 }}
|
||
|
|
transition={{
|
||
|
|
duration: 0.3,
|
||
|
|
delay: 0.5,
|
||
|
|
type: "spring",
|
||
|
|
stiffness: 400,
|
||
|
|
damping: 17
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
Calculate Impact
|
||
|
|
</motion.button>
|
||
|
|
</motion.form>
|
||
|
|
)}
|
||
|
|
</AnimatePresence>
|
||
|
|
|
||
|
|
<AnimatePresence>
|
||
|
|
{tripEstimate && calculationType !== 'custom' && (
|
||
|
|
<motion.div
|
||
|
|
className="mt-6 space-y-6"
|
||
|
|
initial={{ opacity: 0, y: 20 }}
|
||
|
|
animate={{ opacity: 1, y: 0 }}
|
||
|
|
exit={{ opacity: 0, y: -20 }}
|
||
|
|
transition={{ duration: 0.5, ease: "easeOut" }}
|
||
|
|
>
|
||
|
|
<motion.div
|
||
|
|
className="grid grid-cols-2 gap-4"
|
||
|
|
initial={{ opacity: 0 }}
|
||
|
|
animate={{ opacity: 1 }}
|
||
|
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||
|
|
>
|
||
|
|
{calculationType === 'distance' && (
|
||
|
|
<motion.div
|
||
|
|
className="bg-gray-50 p-4 rounded-lg"
|
||
|
|
initial={{ opacity: 0, x: -20 }}
|
||
|
|
animate={{ opacity: 1, x: 0 }}
|
||
|
|
transition={{ duration: 0.5, delay: 0.3 }}
|
||
|
|
>
|
||
|
|
<p className="text-sm text-gray-600">Trip Duration</p>
|
||
|
|
<p className="text-xl font-bold text-gray-900">
|
||
|
|
{tripEstimate.duration.toFixed(1)} hours
|
||
|
|
</p>
|
||
|
|
</motion.div>
|
||
|
|
)}
|
||
|
|
<motion.div
|
||
|
|
className="bg-gray-50 p-4 rounded-lg"
|
||
|
|
initial={{ opacity: 0, x: calculationType === 'distance' ? 20 : 0 }}
|
||
|
|
animate={{ opacity: 1, x: 0 }}
|
||
|
|
transition={{ duration: 0.5, delay: 0.4 }}
|
||
|
|
>
|
||
|
|
<p className="text-sm text-gray-600">Fuel Consumption</p>
|
||
|
|
<p className="text-xl font-bold text-gray-900">
|
||
|
|
{tripEstimate.fuelConsumption.toLocaleString()} {fuelUnit}
|
||
|
|
</p>
|
||
|
|
</motion.div>
|
||
|
|
</motion.div>
|
||
|
|
|
||
|
|
<motion.div
|
||
|
|
initial={{ opacity: 0 }}
|
||
|
|
animate={{ opacity: 1 }}
|
||
|
|
transition={{ duration: 0.5, delay: 0.5 }}
|
||
|
|
>
|
||
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
|
|
Offset Percentage
|
||
|
|
</label>
|
||
|
|
<div className="flex flex-wrap gap-3 mb-3">
|
||
|
|
{[100, 75, 50, 25].map((percent, index) => (
|
||
|
|
<motion.button
|
||
|
|
key={percent}
|
||
|
|
type="button"
|
||
|
|
onClick={() => handlePresetPercentage(percent)}
|
||
|
|
className={`px-4 py-2 rounded-lg transition-colors ${
|
||
|
|
offsetPercentage === percent && customPercentage === ''
|
||
|
|
? 'bg-blue-500 text-white'
|
||
|
|
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||
|
|
}`}
|
||
|
|
whileHover={{ scale: 1.05 }}
|
||
|
|
whileTap={{ scale: 0.95 }}
|
||
|
|
initial={{ opacity: 0, y: 10 }}
|
||
|
|
animate={{ opacity: 1, y: 0 }}
|
||
|
|
transition={{
|
||
|
|
duration: 0.3,
|
||
|
|
delay: 0.6 + (index * 0.1),
|
||
|
|
type: "spring",
|
||
|
|
stiffness: 400,
|
||
|
|
damping: 17
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{percent}%
|
||
|
|
</motion.button>
|
||
|
|
))}
|
||
|
|
<motion.div
|
||
|
|
className="flex items-center space-x-2"
|
||
|
|
initial={{ opacity: 0, y: 10 }}
|
||
|
|
animate={{ opacity: 1, y: 0 }}
|
||
|
|
transition={{ duration: 0.3, delay: 1.0 }}
|
||
|
|
>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
value={customPercentage}
|
||
|
|
onChange={handleCustomPercentageChange}
|
||
|
|
placeholder="Custom %"
|
||
|
|
min="0"
|
||
|
|
max="100"
|
||
|
|
className="w-24 px-3 py-2 border rounded-lg focus:ring-blue-500 focus:border-blue-500"
|
||
|
|
/>
|
||
|
|
<span className="text-gray-600">%</span>
|
||
|
|
</motion.div>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
|
||
|
|
<motion.div
|
||
|
|
className="bg-blue-50 p-4 rounded-lg"
|
||
|
|
initial={{ opacity: 0, scale: 0.95 }}
|
||
|
|
animate={{ opacity: 1, scale: 1 }}
|
||
|
|
transition={{ duration: 0.5, delay: 1.1 }}
|
||
|
|
>
|
||
|
|
<p className="text-sm text-gray-600">Selected CO₂ Offset</p>
|
||
|
|
<p className="text-2xl font-bold text-blue-900">
|
||
|
|
{calculateOffsetAmount(tripEstimate.co2Emissions, offsetPercentage).toFixed(2)} tons
|
||
|
|
</p>
|
||
|
|
<p className="text-sm text-blue-600 mt-1">
|
||
|
|
{offsetPercentage}% of {tripEstimate.co2Emissions.toFixed(2)} tons
|
||
|
|
</p>
|
||
|
|
</motion.div>
|
||
|
|
|
||
|
|
<motion.button
|
||
|
|
onClick={() => onOffsetClick?.(calculateOffsetAmount(tripEstimate.co2Emissions, offsetPercentage))}
|
||
|
|
className="w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition-colors"
|
||
|
|
whileHover={{ scale: 1.03 }}
|
||
|
|
whileTap={{ scale: 0.97 }}
|
||
|
|
initial={{ opacity: 0, y: 10 }}
|
||
|
|
animate={{ opacity: 1, y: 0 }}
|
||
|
|
transition={{
|
||
|
|
duration: 0.5,
|
||
|
|
delay: 1.2,
|
||
|
|
type: "spring",
|
||
|
|
stiffness: 400,
|
||
|
|
damping: 17
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
Offset Your Impact
|
||
|
|
</motion.button>
|
||
|
|
</motion.div>
|
||
|
|
)}
|
||
|
|
</AnimatePresence>
|
||
|
|
</motion.div>
|
||
|
|
);
|
||
|
|
}
|