{/* Main heading */}
diff --git a/src/components/LandingPage/hero.tsx b/src/components/LandingPage/hero.tsx
index 5b11a7c27..ad8111848 100644
--- a/src/components/LandingPage/hero.tsx
+++ b/src/components/LandingPage/hero.tsx
@@ -1,11 +1,11 @@
import { ButterySmoothGlobalMoney, PeanutGuyGIF, Sparkle } from '@/assets'
import { Stack } from '@chakra-ui/react'
import { motion } from 'framer-motion'
-import { useEffect, useState } from 'react'
import { twMerge } from 'tailwind-merge'
import { CloudImages, HeroImages } from './imageAssets'
import Image from 'next/image'
import instantlySendReceive from '@/assets/illustrations/instantly-send-receive.svg'
+import { useResizeHandler, useScrollHandler, createButtonAnimation } from '@/hooks/useAnimations'
type CTAButton = {
label: string
@@ -22,29 +22,6 @@ type HeroProps = {
}
// Helper functions moved outside component for better performance
-const getInitialAnimation = (variant: 'primary' | 'secondary') => ({
- opacity: 0,
- translateY: 4,
- translateX: variant === 'primary' ? 0 : 4,
- rotate: 0.75,
-})
-
-const getAnimateAnimation = (variant: 'primary' | 'secondary', buttonVisible?: boolean, buttonScale?: number) => ({
- opacity: buttonVisible ? 1 : 0,
- translateY: buttonVisible ? 0 : 20,
- translateX: buttonVisible ? (variant === 'primary' ? 0 : 0) : 20,
- rotate: buttonVisible ? 0 : 1,
- scale: buttonScale || 1,
- pointerEvents: buttonVisible ? ('auto' as const) : ('none' as const),
-})
-
-const getHoverAnimation = (variant: 'primary' | 'secondary') => ({
- translateY: 6,
- translateX: variant === 'primary' ? 0 : 3,
- rotate: 0.75,
-})
-
-const transitionConfig = { type: 'spring', damping: 15 } as const
const getButtonContainerClasses = (variant: 'primary' | 'secondary') =>
`relative z-20 mt-8 md:mt-12 ${variant === 'primary' ? 'mx-auto w-fit' : 'right-[calc(50%-120px)]'}`
@@ -100,38 +77,34 @@ const renderArrows = (variant: 'primary' | 'secondary', arrowOpacity: number, bu
)
export function Hero({ heading, primaryCta, secondaryCta, buttonVisible, buttonScale = 1 }: HeroProps) {
- const [screenWidth, setScreenWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 1200)
- const [scrollY, setScrollY] = useState(0)
-
- useEffect(() => {
- const handleResize = () => {
- setScreenWidth(window.innerWidth)
- }
-
- const handleScroll = () => {
- setScrollY(window.scrollY)
- }
-
- handleResize()
- window.addEventListener('resize', handleResize)
- window.addEventListener('scroll', handleScroll)
+ const screenWidth = useResizeHandler()
+ const scrollY = useScrollHandler()
+
+ const primaryButtonAnimation = createButtonAnimation(
+ buttonVisible || false,
+ buttonScale,
+ { translateX: 0 },
+ { translateX: 0 }
+ )
- return () => {
- window.removeEventListener('resize', handleResize)
- window.removeEventListener('scroll', handleScroll)
- }
- }, [])
+ const secondaryButtonAnimation = createButtonAnimation(
+ buttonVisible || false,
+ buttonScale,
+ { translateX: 4 },
+ { translateX: 3 }
+ )
const renderCTAButton = (cta: CTAButton, variant: 'primary' | 'secondary') => {
const arrowOpacity = 1 // Always visible
+ const animation = variant === 'primary' ? primaryButtonAnimation : secondaryButtonAnimation
return (
{/* {renderSparkle(variant)} */}
@@ -161,7 +134,10 @@ export function Hero({ heading, primaryCta, secondaryCta, buttonVisible, buttonS
alt="Peanut Guy"
/>
-
+
{
- const handleResize = () => {
- setScreenWidth(window.innerWidth)
- }
+ const starAnimation1 = createStarAnimation(0.2, 5, { rotate: 22 }, { rotate: 22 })
+ const starAnimation2 = createStarAnimation(0.4, 5, { translateY: 28, translateX: -5, rotate: -17 }, { rotate: -17 })
+ const starAnimation3 = createStarAnimation(0.6, 5, { rotate: 22 }, { rotate: 22 })
+ const starAnimation4 = createStarAnimation(0.8, 5, { translateY: 15, translateX: -5, rotate: -7 }, { rotate: -7 })
+ const starAnimation5 = createStarAnimation(1.0, 5, { translateY: 25, translateX: -5, rotate: -5 }, { rotate: -5 })
- handleResize()
- window.addEventListener('resize', handleResize)
- return () => window.removeEventListener('resize', handleResize)
- }, [])
-
- const createCloudAnimation = (side: 'left' | 'right', top: string, width: number, speed: number) => {
- const vpWidth = screenWidth || 1080
- const totalDistance = vpWidth + width
-
- return {
- initial: { x: side === 'left' ? -width : vpWidth },
- animate: { x: side === 'left' ? vpWidth : -width },
- transition: {
- ease: 'linear',
- duration: totalDistance / speed,
- repeat: Infinity,
- },
- }
- }
+ const cloud1Animation = createCloudAnimation('left', 200, 35, screenWidth)
+ const cloud2Animation = createCloudAnimation('right', 220, 40, screenWidth)
return (
@@ -47,14 +32,14 @@ export function NoFees() {
alt="Floating Border Cloud"
className="absolute left-0"
style={{ top: '20%', width: 200 }}
- {...createCloudAnimation('left', '20%', 200, 35)}
+ {...cloud1Animation}
/>
@@ -63,52 +48,32 @@ export function NoFees() {
{/* Main stylized headline */}
diff --git a/src/components/LandingPage/sendInSeconds.tsx b/src/components/LandingPage/sendInSeconds.tsx
index 1e7a1c688..4c17f8939 100644
--- a/src/components/LandingPage/sendInSeconds.tsx
+++ b/src/components/LandingPage/sendInSeconds.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react'
+import React from 'react'
import { motion } from 'framer-motion'
import Image from 'next/image'
import borderCloud from '@/assets/illustrations/border-cloud.svg'
@@ -6,59 +6,27 @@ import exclamations from '@/assets/illustrations/exclamations.svg'
import payZeroFees from '@/assets/illustrations/pay-zero-fees.svg'
import mobileSendInSeconds from '@/assets/illustrations/mobile-send-in-seconds.svg'
import { Star, Sparkle } from '@/assets'
+import {
+ useResizeHandler,
+ createButtonAnimation,
+ createStarAnimation,
+ createCloudAnimation,
+} from '@/hooks/useAnimations'
export function SendInSeconds() {
- const [screenWidth, setScreenWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 1200)
+ const screenWidth = useResizeHandler()
- useEffect(() => {
- const handleResize = () => {
- setScreenWidth(window.innerWidth)
- }
+ const buttonAnimation = createButtonAnimation(true, 1)
- handleResize()
- window.addEventListener('resize', handleResize)
- return () => window.removeEventListener('resize', handleResize)
- }, [])
+ const starAnimation1 = createStarAnimation(0.2)
+ const starAnimation2 = createStarAnimation(0.4, 5, { translateY: 25, translateX: -5, rotate: -10 }, { rotate: -10 })
+ const starAnimation3 = createStarAnimation(0.6, 5, { translateY: 18, translateX: 5, rotate: -22 }, { rotate: -22 })
+ const starAnimation4 = createStarAnimation(0.8, 5, { translateY: 22, translateX: -5, rotate: 12 }, { rotate: 12 })
- const createCloudAnimation = (side: 'left' | 'right', width: number, speed: number) => {
- const vpWidth = screenWidth || 1080
- const totalDistance = vpWidth + width
-
- return {
- initial: { x: side === 'left' ? -width : vpWidth },
- animate: { x: side === 'left' ? vpWidth : -width },
- transition: {
- ease: 'linear',
- duration: totalDistance / speed,
- repeat: Infinity,
- },
- }
- }
-
- // Button helper functions adapted from hero.tsx
- const getInitialAnimation = () => ({
- opacity: 0,
- translateY: 4,
- translateX: 0,
- rotate: 0.75,
- })
-
- const getAnimateAnimation = (buttonVisible: boolean, buttonScale: number = 1) => ({
- opacity: buttonVisible ? 1 : 0,
- translateY: buttonVisible ? 0 : 20,
- translateX: buttonVisible ? 0 : 20,
- rotate: buttonVisible ? 0 : 1,
- scale: buttonScale,
- pointerEvents: buttonVisible ? ('auto' as const) : ('none' as const),
- })
-
- const getHoverAnimation = () => ({
- translateY: 6,
- translateX: 0,
- rotate: 0.75,
- })
-
- const transitionConfig = { type: 'spring', damping: 15 } as const
+ const cloud1Animation = createCloudAnimation('left', 320, 35, screenWidth)
+ const cloud2Animation = createCloudAnimation('right', 200, 40, screenWidth)
+ const cloud3Animation = createCloudAnimation('left', 180, 45, screenWidth)
+ const cloud4Animation = createCloudAnimation('right', 320, 30, screenWidth)
const getButtonClasses = () =>
`btn bg-white fill-n-1 text-n-1 hover:bg-white/90 px-9 md:px-11 py-4 md:py-10 text-lg md:text-2xl btn-shadow-primary-4`
@@ -114,28 +82,28 @@ export function SendInSeconds() {
alt="Floating Border Cloud"
className="absolute left-0"
style={{ top: '15%', width: 320 }}
- {...createCloudAnimation('left', 320, 35)}
+ {...cloud1Animation}
/>
@@ -143,52 +111,38 @@ export function SendInSeconds() {
- {/* Exclamations */}
-
+ {/* Exclamations - only show on screens larger than 1780px */}
+ {screenWidth > 1740 && (
+
+ )}
{/* Main content */}
@@ -224,10 +178,10 @@ export function SendInSeconds() {
TRY NOW
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
new file mode 100644
index 000000000..ae2595f26
--- /dev/null
+++ b/src/hooks/index.ts
@@ -0,0 +1,8 @@
+export {
+ useScrollHandler,
+ useResizeHandler,
+ createButtonAnimation,
+ createStarAnimation,
+ createCloudAnimation,
+ createAccordionAnimation,
+} from './useAnimations'
diff --git a/src/hooks/useAnimations.ts b/src/hooks/useAnimations.ts
new file mode 100644
index 000000000..1682fb1fa
--- /dev/null
+++ b/src/hooks/useAnimations.ts
@@ -0,0 +1,146 @@
+import { useEffect, useState, useMemo } from 'react'
+
+// Event handler hooks
+export function useScrollHandler(throttleMs = 16) {
+ const [scrollY, setScrollY] = useState(0)
+
+ useEffect(() => {
+ let timeoutId: NodeJS.Timeout | null = null
+
+ const handleScroll = () => {
+ if (timeoutId) return
+
+ timeoutId = setTimeout(() => {
+ setScrollY(window.scrollY)
+ timeoutId = null
+ }, throttleMs)
+ }
+
+ handleScroll()
+ window.addEventListener('scroll', handleScroll)
+
+ return () => {
+ window.removeEventListener('scroll', handleScroll)
+ if (timeoutId) {
+ clearTimeout(timeoutId)
+ }
+ }
+ }, [throttleMs])
+
+ return scrollY
+}
+
+export function useResizeHandler(throttleMs = 16) {
+ const [screenWidth, setScreenWidth] = useState(1080)
+
+ useEffect(() => {
+ let timeoutId: NodeJS.Timeout | null = null
+
+ const handleResize = () => {
+ if (timeoutId) return
+
+ timeoutId = setTimeout(() => {
+ setScreenWidth(window.innerWidth)
+ timeoutId = null
+ }, throttleMs)
+ }
+
+ handleResize()
+ window.addEventListener('resize', handleResize)
+
+ return () => {
+ window.removeEventListener('resize', handleResize)
+ if (timeoutId) {
+ clearTimeout(timeoutId)
+ }
+ }
+ }, [throttleMs])
+
+ return screenWidth
+}
+
+// Animation factory functions
+export function createButtonAnimation(buttonVisible: boolean, buttonScale = 1, customInitial = {}, customHover = {}) {
+ return {
+ initial: {
+ opacity: 0,
+ translateY: 4,
+ translateX: 0,
+ rotate: 0.75,
+ ...customInitial,
+ },
+ animate: {
+ opacity: buttonVisible ? 1 : 0,
+ translateY: buttonVisible ? 0 : 20,
+ translateX: buttonVisible ? 0 : 20,
+ rotate: buttonVisible ? 0 : 1,
+ scale: buttonScale,
+ pointerEvents: buttonVisible ? ('auto' as const) : ('none' as const),
+ },
+ hover: {
+ translateY: 6,
+ translateX: 0,
+ rotate: 0.75,
+ ...customHover,
+ },
+ transition: { type: 'spring', damping: 15 },
+ }
+}
+
+export function createStarAnimation(delay = 0.2, damping = 5, customInitial = {}, customAnimate = {}) {
+ return {
+ initial: {
+ opacity: 0,
+ translateY: 20,
+ translateX: 5,
+ rotate: 45,
+ ...customInitial,
+ },
+ whileInView: {
+ opacity: 1,
+ translateY: 0,
+ translateX: 0,
+ rotate: 45,
+ ...customAnimate,
+ },
+ transition: {
+ type: 'spring',
+ damping,
+ delay,
+ },
+ }
+}
+
+export function createCloudAnimation(side: 'left' | 'right', width: number, speed: number, screenWidth: number) {
+ const vpWidth = screenWidth || 1080
+ const totalDistance = vpWidth + width
+
+ return {
+ initial: { x: side === 'left' ? -width : vpWidth },
+ animate: { x: side === 'left' ? vpWidth : -width },
+ transition: {
+ ease: 'linear',
+ duration: totalDistance / speed,
+ repeat: Infinity,
+ },
+ }
+}
+
+export function createAccordionAnimation(isOpen: boolean, duration = 0.4, iconRotation = 180) {
+ return {
+ container: {
+ animate: { height: 'auto' },
+ transition: { duration },
+ },
+ icon: {
+ animate: { rotate: isOpen ? iconRotation : 0 },
+ transition: { duration: duration * 0.75, transformOrigin: 'center' },
+ },
+ content: {
+ initial: { height: 0, opacity: 0 },
+ animate: { height: 'auto', opacity: 1 },
+ exit: { height: 0, opacity: 0 },
+ transition: { duration: duration * 0.5 },
+ },
+ }
+}