@@ -52,8 +52,14 @@ const SLIDES = [
5252
5353const slideVariants = {
5454 enter : ( dir ) => ( { x : dir > 0 ? '100%' : '-100%' , opacity : 0 } ) ,
55- center : { x : 0 , opacity : 1 , transition : { duration : 0.55 , ease : [ 0.25 , 0.1 , 0.25 , 1 ] } } ,
56- exit : ( dir ) => ( { x : dir < 0 ? '100%' : '-100%' , opacity : 0 , transition : { duration : 0.45 } } ) ,
55+ center : { x : 0 , opacity : 1 , transition : { duration : 0.6 , ease : [ 0.25 , 0.1 , 0.25 , 1 ] } } ,
56+ exit : ( dir ) => ( { x : dir < 0 ? '100%' : '-100%' , opacity : 0 , transition : { duration : 0.5 } } ) ,
57+ } ;
58+
59+ const textVariants = {
60+ hidden : { opacity : 0 , y : 30 } ,
61+ visible : { opacity : 1 , y : 0 , transition : { duration : 0.6 , ease : 'easeOut' } } ,
62+ exit : { opacity : 0 , y : - 20 , transition : { duration : 0.3 } }
5763} ;
5864
5965export default function HeroBanner ( ) {
@@ -72,25 +78,44 @@ export default function HeroBanner() {
7278
7379 useEffect ( ( ) => {
7480 if ( paused ) return ;
75- const id = setInterval ( next , 4000 ) ;
81+ const id = setInterval ( next , 5000 ) ;
7682 return ( ) => clearInterval ( id ) ;
7783 } , [ next , paused ] ) ;
7884
85+ const handleDragEnd = ( e , { offset } ) => {
86+ const swipe = offset . x ;
87+ if ( swipe < - 50 ) {
88+ next ( ) ;
89+ } else if ( swipe > 50 ) {
90+ prev ( ) ;
91+ }
92+ } ;
93+
94+ const handleKeyDown = ( e ) => {
95+ if ( e . key === 'ArrowLeft' ) prev ( ) ;
96+ if ( e . key === 'ArrowRight' ) next ( ) ;
97+ } ;
98+
7999 const slide = SLIDES [ current ] ;
80100 const textAlign = slide . align === 'center' ? 'items-center text-center' : slide . align === 'right' ? 'items-end text-right' : 'items-start text-left' ;
81101
82102 return (
83103 < section
84104 id = "hero-banner"
85- className = "relative w-full overflow-hidden h-[60vh] md:h-[75vh] lg:h-[85vh] min-h-[400px] max-h-[800px]"
105+ className = "relative w-full overflow-hidden h-[60vh] md:h-[75vh] lg:h-[85vh] min-h-[400px] max-h-[800px] bg-black focus-ring "
86106 onMouseEnter = { ( ) => setPaused ( true ) }
87107 onMouseLeave = { ( ) => setPaused ( false ) }
88- aria-label = "Hero banner"
108+ onFocus = { ( ) => setPaused ( true ) }
109+ onBlur = { ( ) => setPaused ( false ) }
110+ onKeyDown = { handleKeyDown }
111+ tabIndex = { 0 }
112+ aria-label = "Hero carousel"
113+ aria-roledescription = "carousel"
89114 >
90115 { /* Subtle pattern overlay */ }
91116 < div
92117 className = "absolute inset-0 z-[1] opacity-[0.06] pointer-events-none"
93- style = { { backgroundImage : `url(\ ${import.meta.env.BASE_URL}assets/illustrations/hero-pattern-bg.svg)` } }
118+ style = { { backgroundImage : `url(${ import . meta. env . BASE_URL } assets/illustrations/hero-pattern-bg.svg)` } }
94119 aria-hidden = "true"
95120 />
96121
@@ -103,88 +128,118 @@ export default function HeroBanner() {
103128 initial = "enter"
104129 animate = "center"
105130 exit = "exit"
106- className = "absolute inset-0 z-[2]"
131+ drag = "x"
132+ dragConstraints = { { left : 0 , right : 0 } }
133+ dragElastic = { 0.8 }
134+ onDragEnd = { handleDragEnd }
135+ className = "absolute inset-0 z-[2] cursor-grab active:cursor-grabbing"
136+ role = "group"
137+ aria-roledescription = "slide"
138+ aria-label = { `Slide ${ current + 1 } of ${ total } ` }
107139 >
108- < img
140+ < motion . img
109141 src = { slide . image }
110- alt = { slide . heading }
142+ alt = ""
111143 width = { 1440 }
112144 height = { 800 }
113145 loading = { current === 0 ? 'eager' : 'lazy' }
146+ fetchpriority = { current === 0 ? 'high' : 'auto' }
114147 className = "h-full w-full object-cover object-[center_20%]"
148+ initial = { { scale : 1 } }
149+ animate = { { scale : 1.05 } }
150+ transition = { { duration : 6 , ease : 'linear' } }
115151 />
116152 { /* Gradient overlay */ }
117153 < div
118154 className = "absolute inset-0"
119155 style = { {
120156 background : slide . align === 'right'
121- ? 'linear-gradient(to left, rgba(0,0,0,0.7 ) 0%, rgba(0,0,0,0.2) 60 %, transparent 100%)'
157+ ? 'linear-gradient(to left, rgba(0,0,0,0.8 ) 0%, rgba(0,0,0,0.3) 50 %, transparent 100%)'
122158 : slide . align === 'center'
123- ? 'linear-gradient(to top, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.2 ) 50%, transparent 100%)'
124- : 'linear-gradient(to right, rgba(0,0,0,0.7 ) 0%, rgba(0,0,0,0.2) 60 %, transparent 100%)'
159+ ? 'linear-gradient(to top, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.3 ) 50%, transparent 100%)'
160+ : 'linear-gradient(to right, rgba(0,0,0,0.8 ) 0%, rgba(0,0,0,0.3) 50 %, transparent 100%)'
125161 } }
126162 />
127- < div className = "absolute inset-0 bg-black/10" />
128163 </ motion . div >
129164 </ AnimatePresence >
130165
131166 { /* Text overlay */ }
132- < div className = { `absolute inset-0 z-[3] flex flex-col justify-center px-8 md:px-20 gap-4 ${ textAlign } ` } >
167+ < div className = { `absolute inset-0 z-[3] flex flex-col justify-center px-8 md:px-20 gap-4 pointer-events-none ${ textAlign } ` } >
133168 < AnimatePresence mode = "wait" >
134169 < motion . div
135170 key = { slide . id }
136- initial = { { opacity : 0 , y : 22 } }
137- animate = { { opacity : 1 , y : 0 , transition : { delay : 0.25 , duration : 0.4 } } }
138- exit = { { opacity : 0 , y : - 10 , transition : { duration : 0.2 } } }
139- className = { `flex flex-col gap-3 ${ textAlign } ` }
171+ variants = { textVariants }
172+ initial = "hidden"
173+ animate = "visible"
174+ exit = "exit"
175+ className = { `flex flex-col gap-4 pointer-events-auto max-w-2xl ${ textAlign } ` }
176+ aria-live = "polite"
140177 >
141- < h1
142- className = "text-4xl font-normal leading-tight text-white md:text-6xl"
143- style = { { fontFamily : slide . font || 'var(--font-heading)' , textShadow : '0 2px 12px rgba(0,0,0,0.6)' } }
178+ < motion . h1
179+ className = "text-4xl font-normal leading-tight text-white md:text-6xl lg:text-7xl drop-shadow-xl"
180+ style = { { fontFamily : slide . font || 'var(--font-heading)' } }
181+ initial = { { opacity : 0 , y : 20 } }
182+ animate = { { opacity : 1 , y : 0 } }
183+ transition = { { delay : 0.2 , duration : 0.5 } }
144184 >
145185 { slide . heading }
146- </ h1 >
147- < p className = "max-w-lg text-base text-white/90 md:text-lg" style = { { fontFamily : 'var(--font-body)' , textShadow : '0 1px 6px rgba(0,0,0,0.6)' } } >
186+ </ motion . h1 >
187+ < motion . p
188+ className = "text-base text-white/90 md:text-xl drop-shadow-md"
189+ style = { { fontFamily : 'var(--font-body)' } }
190+ initial = { { opacity : 0 , y : 20 } }
191+ animate = { { opacity : 1 , y : 0 } }
192+ transition = { { delay : 0.3 , duration : 0.5 } }
193+ >
148194 { slide . subheading }
149- </ p >
150- < Link
151- to = { slide . ctaLink }
152- className = "btn-glow mt-4 inline-flex w-fit items-center rounded-full px-8 py-3.5 text-[15px] font-bold tracking-wide uppercase text-white shadow-lg"
153- style = { { background : 'var(--color-primary)' , fontFamily : 'var(--font-syne)' } }
195+ </ motion . p >
196+ < motion . div
197+ initial = { { opacity : 0 , y : 20 } }
198+ animate = { { opacity : 1 , y : 0 } }
199+ transition = { { delay : 0.4 , duration : 0.5 } }
154200 >
155- { slide . cta }
156- </ Link >
201+ < Link
202+ to = { slide . ctaLink }
203+ className = "btn-glow mt-4 inline-flex w-fit items-center rounded-full px-8 py-3.5 text-[15px] font-bold tracking-wide uppercase text-white shadow-lg focus-ring"
204+ style = { { background : 'var(--color-primary)' , fontFamily : 'var(--font-syne)' } }
205+ >
206+ { slide . cta }
207+ </ Link >
208+ </ motion . div >
157209 </ motion . div >
158210 </ AnimatePresence >
159211 </ div >
160212
161- { /* Arrows */ }
162- < button
163- onClick = { prev }
164- aria-label = "Previous slide"
165- className = "absolute left-4 top-1/2 z-[4] -translate-y-1/2 flex h-10 w-10 items-center justify-center rounded-full bg-black/30 text-white backdrop-blur-sm transition hover:bg-black/50"
166- >
167- < FiChevronLeft size = { 22 } />
168- </ button >
169- < button
170- onClick = { next }
171- aria-label = "Next slide"
172- className = "absolute right-4 top-1/2 z-[4] -translate-y-1/2 flex h-10 w-10 items-center justify-center rounded-full bg-black/30 text-white backdrop-blur-sm transition hover:bg-black/50"
173- >
174- < FiChevronRight size = { 22 } />
175- </ button >
213+ { /* Controls */ }
214+ < div className = "absolute inset-0 z-[4] pointer-events-none flex items-center justify-between px-4 md:px-8" >
215+ < button
216+ onClick = { prev }
217+ aria-label = "Previous slide"
218+ className = "pointer-events-auto flex h-12 w-12 items-center justify-center rounded-full bg-black/20 text-white backdrop-blur-md transition-all hover:bg-black/50 hover:scale-110 focus-ring"
219+ >
220+ < FiChevronLeft size = { 24 } />
221+ </ button >
222+ < button
223+ onClick = { next }
224+ aria-label = "Next slide"
225+ className = "pointer-events-auto flex h-12 w-12 items-center justify-center rounded-full bg-black/20 text-white backdrop-blur-md transition-all hover:bg-black/50 hover:scale-110 focus-ring"
226+ >
227+ < FiChevronRight size = { 24 } />
228+ </ button >
229+ </ div >
176230
177231 { /* Dots */ }
178- < div className = "absolute bottom-5 left-1/2 z-[4] flex -translate-x-1/2 gap-2 " >
232+ < div className = "absolute bottom-6 left-1/2 z-[4] flex -translate-x-1/2 gap-3 " >
179233 { SLIDES . map ( ( _ , i ) => (
180234 < button
181235 key = { i }
182236 onClick = { ( ) => goTo ( i , i > current ? 1 : - 1 ) }
183237 aria-label = { `Go to slide ${ i + 1 } ` }
184- className = "h-2 rounded-full transition-all duration-300"
238+ aria-current = { i === current }
239+ className = "relative h-2 rounded-full transition-all duration-500 focus-ring"
185240 style = { {
186- width : i === current ? '28px ' : '8px ' ,
187- background : i === current ? 'var(--color-primary)' : 'rgba(255,255,255,0.5 )' ,
241+ width : i === current ? '32px ' : '12px ' ,
242+ background : i === current ? 'var(--color-primary)' : 'rgba(255,255,255,0.4 )' ,
188243 } }
189244 />
190245 ) ) }
0 commit comments