11/* eslint-disable @next/next/no-img-element */
22import Link from "next/link" ;
3- import { useEffect , useState } from "react" ;
3+ import { useEffect , useRef , useState } from "react" ;
44import { MODEL_LABELS } from "../modelMeta" ;
55import type {
66 BenchData ,
@@ -45,6 +45,33 @@ function ViewSelector({
4545
4646type NavItem = { id : string ; label : string } ;
4747
48+ /** Returns 0 at top, 1 when fully collapsed. Smooth continuous value. */
49+ function getScrollProgress ( threshold : number ) {
50+ if ( typeof window === "undefined" ) return 0 ;
51+ return Math . min ( 1 , Math . max ( 0 , window . scrollY / threshold ) ) ;
52+ }
53+
54+ function useScrollProgress ( threshold = 80 ) {
55+ const [ progress , setProgress ] = useState ( ( ) => getScrollProgress ( threshold ) ) ;
56+ const rafRef = useRef ( 0 ) ;
57+
58+ useEffect ( ( ) => {
59+ const onScroll = ( ) => {
60+ cancelAnimationFrame ( rafRef . current ) ;
61+ rafRef . current = requestAnimationFrame ( ( ) => {
62+ setProgress ( getScrollProgress ( threshold ) ) ;
63+ } ) ;
64+ } ;
65+ window . addEventListener ( "scroll" , onScroll , { passive : true } ) ;
66+ return ( ) => {
67+ window . removeEventListener ( "scroll" , onScroll ) ;
68+ cancelAnimationFrame ( rafRef . current ) ;
69+ } ;
70+ } , [ threshold ] ) ;
71+
72+ return progress ;
73+ }
74+
4875export default function Hero ( {
4976 selectedView,
5077 onSelectView,
@@ -60,14 +87,8 @@ export default function Hero({
6087 navItems : readonly NavItem [ ] ;
6188 activeNav : string ;
6289} ) {
63- const [ scrolled , setScrolled ] = useState ( false ) ;
64-
65- useEffect ( ( ) => {
66- const onScroll = ( ) => setScrolled ( window . scrollY > 60 ) ;
67- onScroll ( ) ;
68- window . addEventListener ( "scroll" , onScroll , { passive : true } ) ;
69- return ( ) => window . removeEventListener ( "scroll" , onScroll ) ;
70- } , [ ] ) ;
90+ const progress = useScrollProgress ( 80 ) ;
91+ const scrolled = progress > 0.5 ;
7192
7293 const isGlobal = selectedView === "global" ;
7394 const benchData = isGlobal ? null : ( data as BenchData ) ;
@@ -96,72 +117,84 @@ export default function Hero({
96117 { value : `${ benchData ! . programStats . length } ` , label : "Outputs" } ,
97118 ] ;
98119
120+ // Continuous interpolation helpers
121+ const lerp = ( a : number , b : number ) => a + ( b - a ) * progress ;
122+ const expandedPadTop = lerp ( 40 , 8 ) ; // pt-10 → py-2
123+ const expandedPadBot = lerp ( 16 , 8 ) ;
124+ const titleSize = lerp ( 36 , 16 ) ; // text-4xl → text-base
125+ const taglineOpacity = 1 - progress ;
126+ const expandOpacity = 1 - Math . min ( 1 , progress * 2 ) ; // fade out faster
127+ const expandHeight = `${ ( 1 - progress ) * 140 } px` ;
128+ const navOpacity = Math . max ( 0 , ( progress - 0.3 ) / 0.7 ) ; // fade in after 30%
129+ const bgOpacity = progress ;
130+
99131 return (
100- < header
101- className = { `sticky top-0 z-40 transition-[background-color,border-color] duration-300 ${
102- scrolled
103- ? "bg-bg/90 backdrop-blur-md border-b border-border"
104- : "bg-transparent border-b border-transparent"
105- } `}
106- >
107- { /* Gradient glow — fades out when scrolled */ }
132+ < header className = "sticky top-0 z-40" >
133+ { /* Background — fades in */ }
134+ < div
135+ className = "absolute inset-0 border-b backdrop-blur-md"
136+ style = { {
137+ opacity : bgOpacity ,
138+ backgroundColor : `color-mix(in srgb, var(--color-bg) ${ Math . round ( bgOpacity * 90 ) } %, transparent)` ,
139+ borderColor : `color-mix(in srgb, var(--color-border) ${ Math . round ( bgOpacity * 100 ) } %, transparent)` ,
140+ } }
141+ />
142+
143+ { /* Gradient glow — fades out */ }
108144 < div
109- className = { `absolute inset-x-0 top-0 h-[280px] bg-[radial-gradient(circle_at_top,_color-mix(in_srgb,var(--color-primary)_13%,transparent),transparent_58%)] pointer-events-none transition-opacity duration-300 ${
110- scrolled ? "opacity-0" : "opacity-100"
111- } `}
145+ className = "absolute inset-x-0 top-0 h-[280px] bg-[radial-gradient(circle_at_top,_color-mix(in_srgb,var(--color-primary)_13%,transparent),transparent_58%)] pointer-events-none"
146+ style = { { opacity : 1 - progress } }
112147 />
113148
114149 < div className = "relative max-w-7xl mx-auto px-4 sm:px-6" >
115- { /* Top row: brand + view selector — always visible */ }
150+ { /* Top row: brand + nav + view selector */ }
116151 < div
117- className = { `flex items-center gap-3 transition-[padding] duration-300 ${
118- scrolled ? "py-2" : "pt-8 pb-4 sm:pt-10 sm:pb-4"
119- } `}
152+ className = "flex items-center gap-3"
153+ style = { {
154+ paddingTop : `${ expandedPadTop } px` ,
155+ paddingBottom : `${ expandedPadBot } px` ,
156+ } }
120157 >
121158 < Link
122159 href = "/"
123- className = "shrink-0 flex items-center gap-2.5 transition-colors hover:opacity-80"
160+ className = "shrink-0 flex items-center gap-2 hover:opacity-80"
124161 >
125162 < span
126- className = { `font-[family-name:var(--font-display)] tracking-tight text-text transition-[font-size] duration-300 ${
127- scrolled ? "text-base" : "text-3xl sm:text-4xl"
128- } `}
163+ className = "font-[family-name:var(--font-display)] tracking-tight text-text leading-none"
164+ style = { { fontSize : `${ titleSize } px` } }
129165 >
130166 PolicyBench
131167 </ span >
132- { /* "a PolicyEngine project " tagline — visible when expanded */ }
168+ { /* "by [PE logo] " tagline */ }
133169 < span
134- className = { `flex items-center gap-1.5 transition-all duration-300 overflow-hidden ${
135- scrolled
136- ? "opacity-0 max-w-0"
137- : "opacity-60 max-w-[200px]"
138- } `}
170+ className = "flex items-center gap-1.5 overflow-hidden"
171+ style = { { opacity : taglineOpacity * 0.6 , maxWidth : taglineOpacity > 0.05 ? "160px" : "0px" } }
139172 >
140- < span className = "text-text-muted text-sm whitespace-nowrap" > a </ span >
173+ < span className = "text-text-muted text-sm whitespace-nowrap" > by </ span >
141174 < img
142175 src = "/assets/policyengine-logo.svg"
143176 alt = "PolicyEngine"
144177 className = "h-3.5 w-auto shrink-0"
145178 />
146- < span className = "text-text-muted text-sm whitespace-nowrap" > project</ span >
147179 </ span >
148180 </ Link >
149181
150- { /* Nav tabs — slide in when scrolled */ }
182+ { /* Nav tabs — fade in as you scroll */ }
151183 < div
152- className = { `flex items-center gap-0 transition-all duration-300 overflow-hidden ${
153- scrolled
154- ? "opacity-100 max-w-[600px] ml-1"
155- : "opacity-0 max-w-0 ml-0"
156- } `}
184+ className = "flex items-center overflow-hidden"
185+ style = { {
186+ opacity : navOpacity ,
187+ maxWidth : navOpacity > 0.05 ? "600px" : "0px" ,
188+ marginLeft : navOpacity > 0.05 ? "4px" : "0px" ,
189+ } }
157190 >
158191 < div className = "h-4 w-px bg-border shrink-0 mx-2" />
159192 < div className = "flex min-w-max gap-0.5" >
160193 { navItems . map ( ( item ) => (
161194 < a
162195 key = { item . id }
163196 href = { `#${ item . id } ` }
164- className = { `px-2.5 py-2 text-[11px] font-medium tracking-wider uppercase transition-colors border-b-2 sm:px-3 ${
197+ className = { `px-2.5 py-2 text-[11px] font-medium tracking-wider uppercase border-b-2 sm:px-3 ${
165198 activeNav === item . id
166199 ? "border-primary text-primary"
167200 : "border-transparent text-text-secondary hover:text-text"
@@ -181,26 +214,31 @@ export default function Hero({
181214 compact = { scrolled }
182215 />
183216
184- { /* Paper link — only when scrolled */ }
217+ { /* Paper link — fades in with nav */ }
185218 < div
186- className = { `transition-all duration-300 overflow-hidden ${
187- scrolled ? "opacity-100 max-w-[80px]" : "opacity-0 max-w-0"
188- } `}
219+ className = "overflow-hidden"
220+ style = { {
221+ opacity : navOpacity ,
222+ maxWidth : navOpacity > 0.05 ? "80px" : "0px" ,
223+ } }
189224 >
190225 < Link
191226 href = "/paper"
192- className = "rounded-full border border-border bg-card px-3 py-1 text-[11px] font-medium uppercase tracking-wider text-text-secondary transition-colors hover:border-primary/40 hover:text-primary whitespace-nowrap"
227+ className = "rounded-full border border-border bg-card px-3 py-1 text-[11px] font-medium uppercase tracking-wider text-text-secondary hover:border-primary/40 hover:text-primary whitespace-nowrap"
193228 >
194229 Paper
195230 </ Link >
196231 </ div >
197232 </ div >
198233
199- { /* Expanded content: subtitle + stats — collapses on scroll */ }
234+ { /* Expanded content: subtitle + stats */ }
200235 < div
201- className = { `overflow-hidden transition-all duration-300 ease-out ${
202- scrolled ? "max-h-0 opacity-0 pb-0" : "max-h-40 opacity-100 pb-6 sm:pb-8"
203- } `}
236+ className = "overflow-hidden"
237+ style = { {
238+ maxHeight : expandHeight ,
239+ opacity : expandOpacity ,
240+ paddingBottom : expandOpacity > 0.05 ? `${ lerp ( 32 , 0 ) } px` : "0px" ,
241+ } }
204242 >
205243 < p className = "text-text-secondary text-sm sm:text-base max-w-xl leading-relaxed" >
206244 { subtitle } { " " }
@@ -240,10 +278,11 @@ export default function Hero({
240278 </ div >
241279 </ div >
242280
243- { /* Bottom border gradient — only when not scrolled */ }
244- { ! scrolled && (
245- < div className = "h-px bg-gradient-to-r from-transparent via-primary/25 to-transparent" />
246- ) }
281+ { /* Bottom border gradient — fades out */ }
282+ < div
283+ className = "h-px bg-gradient-to-r from-transparent via-primary/25 to-transparent"
284+ style = { { opacity : 1 - progress } }
285+ />
247286 </ header >
248287 ) ;
249288}
0 commit comments