33// file COPYING or http://www.opensource.org/licenses/mit-license.php.
44
55"use client" ;
6- import { useRef , useState , useEffect } from "react" ;
6+ import { useRef , useState , useEffect , useCallback , useMemo } from "react" ;
77import ContentContainer from "../Atoms/ContentContainer" ;
88import Image from "next/image" ;
99import { cn } from "../../utils/cn" ;
10- import { motion } from "framer-motion" ;
10+ import { motion , AnimatePresence } from "framer-motion" ;
1111
1212const eCashFeatures = [
1313 {
@@ -37,11 +37,8 @@ const eCashFeatures = [
3737] ;
3838
3939export default function PoweringPayments ( ) {
40- const [ rawHoveredIndex , setRawHoveredIndex ] = useState < number | null > ( null ) ;
4140 const [ hoveredIndex , setHoveredIndex ] = useState < number | null > ( null ) ;
4241 const [ activeIndex , setActiveIndex ] = useState < number > ( 0 ) ;
43- const [ prevHoveredIndex , setPrevHoveredIndex ] = useState < number | null > ( null ) ;
44- const [ fadeKey , setFadeKey ] = useState < number > ( 0 ) ;
4542 const containerRef = useRef < HTMLDivElement > ( null ) ;
4643
4744 type FeatureCardProps = {
@@ -51,22 +48,49 @@ export default function PoweringPayments() {
5148 } ;
5249
5350 const FeatureCard = ( { name, index, onHover } : FeatureCardProps ) => {
51+ const isHovered = hoveredIndex === index ;
52+
5453 return (
55- < div
54+ < motion . div
5655 onMouseEnter = { ( ) => onHover ( index ) }
5756 onMouseLeave = { ( ) => onHover ( null ) }
58- className = "custom-box relative flex h-[160px] w-full max-w-[270px] cursor-pointer items-center rounded-xl bg-gradient-to-br from-[#0E0E21] to-[#19193B] p-8 hover:from-[#130D3F] hover:to-[#7316d1]"
57+ className = "custom-box relative flex h-[160px] w-full max-w-[270px] cursor-pointer items-center rounded-xl bg-gradient-to-br from-[#0E0E21] to-[#19193B] p-8"
58+ whileHover = { {
59+ background : "linear-gradient(135deg, #130D3F 0%, #7316d1 100%)" ,
60+ transition : { duration : 0.2 , ease : "easeOut" } ,
61+ } }
5962 >
60- { index === 2 && hoveredIndex === 2 && (
61- < div className = "absolute left-[270px] z-0 h-[3px] w-2/3 bg-[#a77ba8]" />
63+ { /* Connection lines with smooth animations */ }
64+ { index === 2 && (
65+ < motion . div
66+ className = "absolute left-[270px] z-0 h-[3px] w-2/3 bg-[#a77ba8]"
67+ initial = { { scaleX : 0 , opacity : 0 } }
68+ animate = { {
69+ scaleX : isHovered ? 1 : 0 ,
70+ opacity : isHovered ? 1 : 0 ,
71+ } }
72+ transition = { { duration : 0.3 , ease : "easeOut" } }
73+ style = { { transformOrigin : "left center" } }
74+ />
6275 ) }
63- { index === 5 && hoveredIndex === 5 && (
64- < div className = "absolute right-[270px] z-0 h-[3px] w-2/3 bg-[#a77ba8]" />
76+ { index === 5 && (
77+ < motion . div
78+ className = "absolute right-[270px] z-0 h-[3px] w-2/3 bg-[#a77ba8]"
79+ initial = { { scaleX : 0 , opacity : 0 } }
80+ animate = { {
81+ scaleX : isHovered ? 1 : 0 ,
82+ opacity : isHovered ? 1 : 0 ,
83+ } }
84+ transition = { { duration : 0.3 , ease : "easeOut" } }
85+ style = { { transformOrigin : "right center" } }
86+ />
6587 ) }
88+
6689 < div className = "flex flex-col items-start" >
6790 < span className = "text-secondaryText text-sm" > 0{ index } </ span >
6891 < h4 className = "text-lg font-bold leading-snug" > { name } </ h4 >
6992 </ div >
93+
7094 < div className = "absolute bottom-0 right-0 h-full w-[60%]" >
7195 < Image
7296 src = { `/${ name . toLowerCase ( ) } .png` }
@@ -75,51 +99,30 @@ export default function PoweringPayments() {
7599 className = "object-contain object-bottom"
76100 />
77101 </ div >
78- </ div >
102+ </ motion . div >
79103 ) ;
80104 } ;
81105
82- useEffect ( ( ) => {
106+ // Memoized scroll handler to prevent unnecessary re-renders
107+ const handleScroll = useCallback ( ( ) => {
83108 const el = containerRef . current ;
84109 if ( ! el ) return ;
85110
86- const handleScroll = ( ) => {
87- const child = el . children [ 0 ] as HTMLElement ;
88- const cardWidth = child . offsetWidth + 16 ; // gap-4 = 1rem = 16px
89- const index = Math . round ( el . scrollLeft / cardWidth ) ;
90- setActiveIndex ( index ) ;
91- } ;
92-
93- el . addEventListener ( "scroll" , handleScroll ) ;
94- return ( ) => el . removeEventListener ( "scroll" , handleScroll ) ;
111+ const child = el . children [ 0 ] as HTMLElement ;
112+ const cardWidth = child . offsetWidth + 16 ; // gap-4 = 1rem = 16px
113+ const index = Math . round ( el . scrollLeft / cardWidth ) ;
114+ setActiveIndex ( index ) ;
95115 } , [ ] ) ;
96116
97117 useEffect ( ( ) => {
98- if ( hoveredIndex !== prevHoveredIndex ) {
99- setFadeKey ( ( prev ) => prev + 1 ) ;
100- setPrevHoveredIndex ( hoveredIndex ) ;
101- }
102- } , [ hoveredIndex , prevHoveredIndex ] ) ;
103-
104- useEffect ( ( ) => {
105- let timeout : NodeJS . Timeout ;
106-
107- if ( rawHoveredIndex === null ) {
108- // Long delay when hovering off all tiles
109- timeout = setTimeout ( ( ) => {
110- setHoveredIndex ( null ) ;
111- } , 500 ) ;
112- } else {
113- // Short delay between switching tiles
114- timeout = setTimeout ( ( ) => {
115- setHoveredIndex ( rawHoveredIndex ) ;
116- } , 300 ) ;
117- }
118+ const el = containerRef . current ;
119+ if ( ! el ) return ;
118120
119- return ( ) => clearTimeout ( timeout ) ;
120- } , [ rawHoveredIndex ] ) ;
121+ el . addEventListener ( "scroll" , handleScroll , { passive : true } ) ;
122+ return ( ) => el . removeEventListener ( "scroll" , handleScroll ) ;
123+ } , [ handleScroll ] ) ;
121124
122- const scrollToIndex = ( index : number ) => {
125+ const scrollToIndex = useCallback ( ( index : number ) => {
123126 const el = containerRef . current ;
124127 if ( ! el ) return ;
125128
@@ -130,7 +133,51 @@ export default function PoweringPayments() {
130133 inline : "start" ,
131134 } ) ;
132135 }
133- } ;
136+ } , [ ] ) ;
137+
138+ // Memoized content to prevent unnecessary re-renders
139+ const centerContent = useMemo (
140+ ( ) => (
141+ < div className = "absolute flex h-full w-full items-center justify-center overflow-hidden" >
142+ < AnimatePresence mode = "wait" >
143+ { hoveredIndex !== null ? (
144+ < motion . div
145+ key = { `text-${ hoveredIndex } ` }
146+ initial = { { opacity : 0 , y : 20 } }
147+ animate = { { opacity : 1 , y : 0 } }
148+ exit = { { opacity : 0 , y : - 20 } }
149+ transition = { { duration : 0.3 , ease : "easeOut" } }
150+ className = "absolute inset-0 flex items-center justify-center"
151+ >
152+ < div >
153+ < p className = "bg-background/70 max-w-[330px] rounded border border-white/10 p-6 text-center backdrop-blur-sm" >
154+ { eCashFeatures [ hoveredIndex - 1 ] ?. text }
155+ </ p >
156+ </ div >
157+ </ motion . div >
158+ ) : (
159+ < motion . div
160+ key = "title"
161+ initial = { { opacity : 0 , y : 20 } }
162+ animate = { { opacity : 1 , y : 0 } }
163+ exit = { { opacity : 0 , y : - 20 } }
164+ transition = { { duration : 0.3 , ease : "easeOut" } }
165+ className = "absolute inset-0 flex items-center justify-center"
166+ >
167+ < div >
168+ < h2 className = "max-w-[330px] text-center" >
169+ Powering{ " " }
170+ < span className = "pink-gradient-text" > Internet-Scale</ span > { " " }
171+ Payments
172+ </ h2 >
173+ </ div >
174+ </ motion . div >
175+ ) }
176+ </ AnimatePresence >
177+ </ div >
178+ ) ,
179+ [ hoveredIndex ]
180+ ) ;
134181
135182 return (
136183 < ContentContainer className = "mb-30 max-w-[1400px]" >
@@ -147,60 +194,57 @@ export default function PoweringPayments() {
147194 key = { index }
148195 index = { index + 1 }
149196 name = { feature . name }
150- onHover = { setRawHoveredIndex }
197+ onHover = { setHoveredIndex }
151198 />
152199 ) ) }
153200 </ div >
201+
154202 < div className = "relative z-10 aspect-[550/721] w-full lg:h-[721px] lg:w-[550px] lg:shrink-0" >
155203 < Image
156204 src = "/powering-payments-bg.jpg"
157205 alt = "Powering Internet-Scale Payments"
158206 fill
159207 className = "object-contain"
160208 />
161- { hoveredIndex !== null && (
162- < Image
163- key = { hoveredIndex }
164- src = { `/overlay-${ hoveredIndex } .png` }
165- alt = "Powering Internet-Scale Payments"
166- fill
167- className = "hidden object-contain lg:inline-block"
168- />
169- ) }
170209
171- < div className = "absolute flex h-full w-full items-center justify-center overflow-hidden" >
172- < div
173- key = { fadeKey }
174- className = "animate-fade-in-up absolute inset-0 flex items-center justify-center opacity-0"
175- >
176- < div >
177- { hoveredIndex !== null ? (
178- < p className = "bg-background/70 max-w-[330px] rounded border border-white/10 p-6 text-center" >
179- { eCashFeatures [ hoveredIndex - 1 ] ?. text }
180- </ p >
181- ) : (
182- < h2 className = "max-w-[330px] text-center" >
183- Powering{ " " }
184- < span className = "pink-gradient-text" > Internet-Scale</ span > { " " }
185- Payments
186- </ h2 >
187- ) }
188- </ div >
189- </ div >
190- </ div >
210+ { /* Overlay image with smooth transitions */ }
211+ < AnimatePresence mode = "wait" >
212+ { hoveredIndex !== null && (
213+ < motion . div
214+ key = { `overlay-${ hoveredIndex } ` }
215+ initial = { { opacity : 0 } }
216+ animate = { { opacity : 1 } }
217+ exit = { { opacity : 0 } }
218+ transition = { { duration : 0.3 , ease : "easeOut" } }
219+ className = "absolute inset-0"
220+ >
221+ < Image
222+ src = { `/overlay-${ hoveredIndex } .png` }
223+ alt = "Powering Internet-Scale Payments"
224+ fill
225+ className = "hidden object-contain lg:inline-block"
226+ />
227+ </ motion . div >
228+ ) }
229+ </ AnimatePresence >
230+
231+ { centerContent }
191232 </ div >
233+
192234 < div className = "hidden grow basis-0 flex-col items-start justify-center gap-2 self-stretch lg:flex [&>*:nth-child(2)]:self-end" >
193235 { eCashFeatures . slice ( 3 , 6 ) . map ( ( feature , index ) => (
194236 < FeatureCard
195237 key = { index }
196238 index = { index + 4 }
197239 name = { feature . name }
198- onHover = { setRawHoveredIndex }
240+ onHover = { setHoveredIndex }
199241 />
200242 ) ) }
201243 </ div >
202244 </ motion . div >
245+
203246 < div className = "bg-background relative z-20 m-auto mt-[-50px] h-[20px] w-full max-w-[750px] rounded border border-b-0 border-x-[#211a36] border-t-[#211a36] lg:mt-0" />
247+
204248 < div className = "bg-background relative z-20 flex w-full items-center justify-between gap-4 px-2 lg:hidden" >
205249 { eCashFeatures . map ( ( feature , index ) => (
206250 < div
@@ -221,26 +265,36 @@ export default function PoweringPayments() {
221265 </ div >
222266 < span
223267 className = { cn (
224- "text-sm" ,
268+ "text-sm transition-opacity duration-200 " ,
225269 activeIndex === index ? "opacity-100" : "opacity-40"
226270 ) }
227271 >
228272 0{ index + 1 }
229273 </ span >
230274
231275 < div className = "relative mt-2 aspect-square w-[15px]" >
232- { activeIndex === index && (
233- < Image
234- src = "/arrow-up.png"
235- alt = "arrow"
236- fill
237- className = "object-contain"
238- />
239- ) }
276+ < AnimatePresence >
277+ { activeIndex === index && (
278+ < motion . div
279+ initial = { { opacity : 0 , scale : 0.8 } }
280+ animate = { { opacity : 1 , scale : 1 } }
281+ exit = { { opacity : 0 , scale : 0.8 } }
282+ transition = { { duration : 0.2 , ease : "easeOut" } }
283+ >
284+ < Image
285+ src = "/arrow-up.png"
286+ alt = "arrow"
287+ fill
288+ className = "object-contain"
289+ />
290+ </ motion . div >
291+ ) }
292+ </ AnimatePresence >
240293 </ div >
241294 </ div >
242295 ) ) }
243296 </ div >
297+
244298 < div
245299 className = "scrollx-container m-auto mt-3 flex w-[95%] snap-x snap-mandatory gap-4 overflow-x-auto scroll-smooth pb-3 lg:hidden"
246300 ref = { containerRef }
@@ -249,7 +303,7 @@ export default function PoweringPayments() {
249303 < div
250304 key = { index }
251305 className = { cn (
252- "w-[85%] shrink-0 snap-start transition-opacity " ,
306+ "w-[85%] shrink-0 snap-start transition-all duration-300 ease-out " ,
253307 activeIndex === index ? "opacity-100" : "opacity-40"
254308 ) }
255309 >
0 commit comments