1- import { useRef , useEffect , useState } from 'react'
1+ import { useRef , useEffect , useState , useCallback } from 'react'
22import { motion , AnimatePresence } from 'framer-motion'
3- import { Search , Loader2 } from 'lucide-react'
3+ import { Loader2 , Sparkles } from 'lucide-react'
44import { HeroSearch , type HeroSearchHandle } from './HeroSearch'
55import { useDemoSearch , DEMO_REPOS , type DemoRepo } from '@/hooks/useDemoSearch'
66import type { SearchResult } from '@/types'
@@ -11,21 +11,73 @@ interface Props {
1111
1212const PYTHON_REPOS = DEMO_REPOS . filter ( r => [ 'flask' , 'fastapi' ] . includes ( r . id ) )
1313
14+ const TYPING_QUERIES = [
15+ 'authentication middleware' ,
16+ 'database connection pool' ,
17+ 'error handling patterns' ,
18+ 'caching implementation' ,
19+ 'request validation' ,
20+ ]
21+
1422export function Hero ( { onResultsReady } : Props ) {
1523 const searchRef = useRef < HeroSearchHandle > ( null )
1624 const cardRef = useRef < HTMLDivElement > ( null )
1725 const { query, repo, results, loading, searchTime, setQuery, setRepo, search } = useDemoSearch ( false )
1826 const [ mousePos , setMousePos ] = useState ( { x : 0 , y : 0 } )
27+ const [ isTyping , setIsTyping ] = useState ( true )
28+ const [ typingIndex , setTypingIndex ] = useState ( 0 )
29+ const [ displayText , setDisplayText ] = useState ( '' )
30+ const typingTimeoutRef = useRef < ReturnType < typeof setTimeout > > ( )
1931
2032 useEffect ( ( ) => {
2133 if ( results . length ) onResultsReady ?.( results , query , repo . id , searchTime )
2234 } , [ results , query , repo . id , searchTime , onResultsReady ] )
2335
36+ // Typing animation
37+ useEffect ( ( ) => {
38+ if ( ! isTyping ) return
39+
40+ const currentQuery = TYPING_QUERIES [ typingIndex ]
41+ let charIndex = 0
42+
43+ const typeChar = ( ) => {
44+ if ( charIndex <= currentQuery . length ) {
45+ setDisplayText ( currentQuery . slice ( 0 , charIndex ) )
46+ setQuery ( currentQuery . slice ( 0 , charIndex ) )
47+ charIndex ++
48+ typingTimeoutRef . current = setTimeout ( typeChar , 60 + Math . random ( ) * 40 )
49+ } else {
50+ // Done typing, trigger search after a pause
51+ typingTimeoutRef . current = setTimeout ( ( ) => {
52+ search ( )
53+ // Wait then move to next query
54+ typingTimeoutRef . current = setTimeout ( ( ) => {
55+ setTypingIndex ( ( i ) => ( i + 1 ) % TYPING_QUERIES . length )
56+ setDisplayText ( '' )
57+ } , 4000 )
58+ } , 500 )
59+ }
60+ }
61+
62+ typingTimeoutRef . current = setTimeout ( typeChar , 1500 )
63+ return ( ) => {
64+ if ( typingTimeoutRef . current ) clearTimeout ( typingTimeoutRef . current )
65+ }
66+ } , [ isTyping , typingIndex , setQuery , search ] )
67+
68+ // Stop auto-typing when user interacts
69+ const handleUserInput = useCallback ( ( val : string ) => {
70+ setIsTyping ( false )
71+ if ( typingTimeoutRef . current ) clearTimeout ( typingTimeoutRef . current )
72+ setQuery ( val )
73+ } , [ setQuery ] )
74+
2475 useEffect ( ( ) => {
2576 const onKey = ( e : KeyboardEvent ) => {
2677 const tag = ( e . target as HTMLElement ) . tagName
2778 if ( e . key === '/' && tag !== 'INPUT' && tag !== 'TEXTAREA' ) {
2879 e . preventDefault ( )
80+ setIsTyping ( false )
2981 searchRef . current ?. focus ( )
3082 }
3183 }
@@ -47,11 +99,11 @@ export function Hero({ onResultsReady }: Props) {
4799
48100 return (
49101 < section className = "relative min-h-screen flex flex-col justify-center pt-20 pb-12 px-6 overflow-hidden" >
50- { /* Animated gradient orbs - Linear style */ }
102+ { /* Animated gradient orbs */ }
51103 < div className = "absolute inset-0 overflow-hidden pointer-events-none" >
52104 < motion . div
53105 className = "absolute top-1/4 left-1/4 w-[500px] h-[500px] rounded-full"
54- style = { { background : 'radial-gradient(circle, rgba(59,130,246 ,0.15) 0%, transparent 70%)' } }
106+ style = { { background : 'radial-gradient(circle, rgba(79,70,229 ,0.15) 0%, transparent 70%)' } }
55107 animate = { { x : [ 0 , 30 , 0 ] , y : [ 0 , - 20 , 0 ] , scale : [ 1 , 1.1 , 1 ] } }
56108 transition = { { duration : 8 , repeat : Infinity , ease : 'easeInOut' } }
57109 />
@@ -70,38 +122,52 @@ export function Hero({ onResultsReady }: Props) {
70122 </ div >
71123
72124 < div className = "relative max-w-3xl mx-auto w-full" >
125+ { /* Badge */ }
126+ < motion . div
127+ className = "flex justify-center mb-6"
128+ initial = { { opacity : 0 , y : 20 } }
129+ animate = { { opacity : 1 , y : 0 } }
130+ transition = { { duration : 0.5 } }
131+ >
132+ < div className = "inline-flex items-center gap-2 px-3 py-1.5 rounded-full border border-accent/30 bg-accent/5 text-sm" >
133+ < Sparkles className = "w-3.5 h-3.5 text-accent" />
134+ < span className = "text-accent font-medium" > Now in beta</ span >
135+ < span className = "text-muted-foreground" > • Free for open source</ span >
136+ </ div >
137+ </ motion . div >
138+
73139 { /* Headline */ }
74140 < motion . div
75141 className = "text-center mb-10"
76142 initial = { { opacity : 0 , y : 20 } }
77143 animate = { { opacity : 1 , y : 0 } }
78- transition = { { duration : 0.5 } }
144+ transition = { { duration : 0.5 , delay : 0.1 } }
79145 >
80- < h1 className = "text-4xl md:text-5xl lg:text-6xl font-bold text-white leading-[1.1] tracking-tight" >
81- Find code by meaning,
146+ < h1 className = "text-4xl md:text-5xl lg:text-6xl font-bold text-foreground leading-[1.1] tracking-tight" >
147+ Stop feeling lost in
82148 < br />
83- < span className = "bg-gradient-to-r from-blue-400 via-violet-400 to-cyan-400 bg-clip-text text-transparent" >
84- not by keywords .
149+ < span className = "bg-gradient-to-r from-accent via-violet-400 to-cyan-400 bg-clip-text text-transparent" >
150+ unfamiliar codebases .
85151 </ span >
86152 </ h1 >
87- < p className = "mt-5 text-lg text-zinc-400 max-w-lg mx-auto" >
88- Stop grep-ing through thousands of files .
89- < br />
90- Describe what you need and get the exact function.
153+ < p className = "mt-6 text-lg text-muted-foreground max-w-xl mx-auto leading-relaxed " >
154+ Describe what you're looking for in plain English .
155+ < br className = "hidden sm:block" />
156+ Get the exact function, class, or pattern—instantly .
91157 </ p >
92158 </ motion . div >
93159
94160 { /* Search */ }
95161 < motion . div
96162 initial = { { opacity : 0 , y : 20 } }
97163 animate = { { opacity : 1 , y : 0 } }
98- transition = { { duration : 0.5 , delay : 0.1 } }
164+ transition = { { duration : 0.5 , delay : 0.2 } }
99165 >
100166 < HeroSearch
101167 ref = { searchRef }
102168 value = { query }
103- onChange = { setQuery }
104- onSubmit = { ( ) => search ( ) }
169+ onChange = { handleUserInput }
170+ onSubmit = { ( ) => { setIsTyping ( false ) ; search ( ) } }
105171 searching = { loading }
106172 repoName = { repo . name }
107173 />
@@ -112,9 +178,9 @@ export function Hero({ onResultsReady }: Props) {
112178 className = "mt-4 flex items-center justify-center gap-2"
113179 initial = { { opacity : 0 } }
114180 animate = { { opacity : 1 } }
115- transition = { { duration : 0.4 , delay : 0.2 } }
181+ transition = { { duration : 0.4 , delay : 0.3 } }
116182 >
117- < span className = "text-xs text-zinc-600 " > Try on:</ span >
183+ < span className = "text-xs text-muted-foreground/60 " > Try on:</ span >
118184 { PYTHON_REPOS . map ( r => (
119185 < button
120186 key = { r . id }
@@ -123,8 +189,8 @@ export function Hero({ onResultsReady }: Props) {
123189 className = { `
124190 px-3 py-1.5 text-xs rounded-lg transition-all font-medium
125191 ${ repo . id === r . id
126- ? 'bg-white /10 text-white border border-white/10 '
127- : 'text-zinc-500 hover:text-zinc-300 hover:bg-white/5'
192+ ? 'bg-accent /10 text-accent border border-accent/20 '
193+ : 'text-muted-foreground hover:text-foreground hover:bg-white/5 dark: hover:bg-white/5'
128194 }
129195 ` }
130196 >
@@ -133,7 +199,7 @@ export function Hero({ onResultsReady }: Props) {
133199 ) ) }
134200 </ motion . div >
135201
136- { /* Result card - only shows when loading or has results */ }
202+ { /* Result card */ }
137203 < AnimatePresence >
138204 { ( loading || topResult ) && (
139205 < motion . div
@@ -152,12 +218,12 @@ export function Hero({ onResultsReady }: Props) {
152218 < div
153219 className = "absolute -inset-px rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none"
154220 style = { {
155- background : `radial-gradient(400px circle at ${ mousePos . x } px ${ mousePos . y } px, rgba(59,130,246 ,0.15), transparent 40%)`
221+ background : `radial-gradient(400px circle at ${ mousePos . x } px ${ mousePos . y } px, rgba(79,70,229 ,0.15), transparent 40%)`
156222 } }
157223 />
158224
159225 { /* Card */ }
160- < div className = "relative rounded-xl border border-white/[0.08] bg-zinc-900 /50 backdrop-blur-sm overflow-hidden" >
226+ < div className = "relative rounded-xl border border-white/[0.08] dark:border-white/[0.08] light:border-black/[0.08] bg-card /50 backdrop-blur-sm overflow-hidden" >
161227 < AnimatePresence mode = "wait" >
162228 { loading ? (
163229 < motion . div
@@ -168,13 +234,13 @@ export function Hero({ onResultsReady }: Props) {
168234 className = "p-6"
169235 >
170236 < div className = "flex items-center gap-3 mb-4" >
171- < Loader2 className = "w-4 h-4 animate-spin text-blue-400 " />
172- < span className = "text-sm text-zinc-500 " > Searching { repo . name } ...</ span >
237+ < Loader2 className = "w-4 h-4 animate-spin text-accent " />
238+ < span className = "text-sm text-muted-foreground " > Searching { repo . name } ...</ span >
173239 </ div >
174240 < div className = "space-y-3 animate-pulse" >
175- < div className = "h-5 w-48 bg-white/5 rounded" />
176- < div className = "h-3 w-32 bg-white/5 rounded" />
177- < div className = "h-24 bg-white/[0.02] rounded-lg mt-3" />
241+ < div className = "h-5 w-48 bg-white/5 dark:bg-white/5 light:bg-black/5 rounded" />
242+ < div className = "h-3 w-32 bg-white/5 dark:bg-white/5 light:bg-black/5 rounded" />
243+ < div className = "h-24 bg-white/[0.02] dark:bg-white/[0.02] light:bg-black/[0.02] rounded-lg mt-3" />
178244 </ div >
179245 </ motion . div >
180246 ) : topResult ? (
@@ -185,36 +251,36 @@ export function Hero({ onResultsReady }: Props) {
185251 exit = { { opacity : 0 } }
186252 >
187253 { /* Header */ }
188- < div className = "px-5 py-3 border-b border-white/[0.06] flex items-center justify-between bg-white/[0.02]" >
254+ < div className = "px-5 py-3 border-b border-white/[0.06] dark:border-white/[0.06] light:border-black/[0.06] flex items-center justify-between bg-white/[0.02] dark:bg-white/[0.02] light:bg-black /[0.02]" >
189255 < div className = "flex items-center gap-3" >
190256 < div className = "flex items-center gap-2" >
191257 < div className = "w-2 h-2 rounded-full bg-emerald-400 animate-pulse" />
192- < span className = "text-xs text-zinc-500 " > Found in { searchTime } ms</ span >
258+ < span className = "text-xs text-muted-foreground " > Found in { searchTime } ms</ span >
193259 </ div >
194260 < span className = "text-xs px-2 py-0.5 rounded-full bg-emerald-500/10 text-emerald-400 font-medium" >
195261 { Math . round ( topResult . score * 100 ) } % match
196262 </ span >
197263 </ div >
198- < span className = "text-xs text-zinc-600 " > { repo . name } </ span >
264+ < span className = "text-xs text-muted-foreground/60 " > { repo . name } </ span >
199265 </ div >
200266
201267 { /* Content */ }
202268 < div className = "p-5" >
203269 < div className = "flex items-start gap-3 mb-4" >
204270 < div className = "flex-1" >
205271 < div className = "flex items-center gap-2" >
206- < span className = "font-mono text-sm font-semibold text-white " > { topResult . name } </ span >
272+ < span className = "font-mono text-sm font-semibold text-foreground " > { topResult . name } </ span >
207273 < span className = "text-[10px] px-1.5 py-0.5 rounded bg-violet-500/10 text-violet-400 uppercase font-medium" >
208274 { topResult . type }
209275 </ span >
210276 </ div >
211- < div className = "text-xs text-zinc-600 font-mono mt-1" > { topResult . file_path } </ div >
277+ < div className = "text-xs text-muted-foreground/60 font-mono mt-1" > { topResult . file_path } </ div >
212278 </ div >
213279 </ div >
214280
215281 { /* Code preview */ }
216282 < div className = "relative rounded-lg overflow-hidden" >
217- < div className = "absolute inset-0 bg-gradient-to-br from-blue-500 /5 to-violet-500/5" />
283+ < div className = "absolute inset-0 bg-gradient-to-br from-accent /5 to-violet-500/5" />
218284 < pre className = "relative text-xs text-zinc-300 bg-black/40 p-4 overflow-x-auto font-mono leading-relaxed" >
219285 < code > { topResult . content ?. slice ( 0 , 250 ) } ...</ code >
220286 </ pre >
@@ -223,8 +289,8 @@ export function Hero({ onResultsReady }: Props) {
223289
224290 { /* Footer */ }
225291 { results . length > 1 && (
226- < div className = "px-5 py-3 border-t border-white/[0.06] bg-white/[0.01]" >
227- < span className = "text-xs text-zinc-600 " > +{ results . length - 1 } more results</ span >
292+ < div className = "px-5 py-3 border-t border-white/[0.06] dark:border-white/[0.06] light:border-black/[0.06] bg-white/[0.01] dark:bg-white/[0.01] light:bg-black /[0.01]" >
293+ < span className = "text-xs text-muted-foreground/60 " > +{ results . length - 1 } more results</ span >
228294 </ div >
229295 ) }
230296 </ motion . div >
@@ -238,19 +304,33 @@ export function Hero({ onResultsReady }: Props) {
238304
239305 { /* CTA */ }
240306 < motion . div
241- className = "mt-10 text- center"
307+ className = "mt-10 flex flex-col items- center gap-4 "
242308 initial = { { opacity : 0 } }
243309 animate = { { opacity : 1 } }
244- transition = { { delay : 0.4 } }
310+ transition = { { delay : 0.5 } }
245311 >
246- < a
247- href = "/signup"
248- className = "inline-flex items-center gap-2 px-6 py-3 text-sm font-medium text-zinc-300 rounded-lg border border-zinc-700 hover:border-zinc-500 hover:text-white hover:bg-white/5 transition-all"
249- >
250- Index your first repo free →
251- </ a >
252- < p className = "text-xs text-zinc-500 mt-4" >
253- Works with any Python repository • Now in beta
312+ < div className = "flex flex-col sm:flex-row items-center gap-3" >
313+ < a
314+ href = "/signup"
315+ className = "inline-flex items-center gap-2 px-6 py-3 text-sm font-medium text-white rounded-lg bg-accent hover:bg-accent/90 transition-all shadow-lg shadow-accent/20"
316+ >
317+ Index your first repo free
318+ < span className = "text-white/60" > →</ span >
319+ </ a >
320+ < a
321+ href = "https://github.com/OpenCodeIntel/opencodeintel"
322+ target = "_blank"
323+ rel = "noopener noreferrer"
324+ className = "inline-flex items-center gap-2 px-6 py-3 text-sm font-medium text-muted-foreground rounded-lg border border-white/10 dark:border-white/10 light:border-black/10 hover:border-white/20 dark:hover:border-white/20 hover:text-foreground hover:bg-white/5 dark:hover:bg-white/5 light:hover:bg-black/5 transition-all"
325+ >
326+ < svg viewBox = "0 0 24 24" className = "w-4 h-4 fill-current" aria-hidden = "true" >
327+ < path d = "M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
328+ </ svg >
329+ View on GitHub
330+ </ a >
331+ </ div >
332+ < p className = "text-xs text-muted-foreground/60" >
333+ Works with any Python repository • Self-host or cloud
254334 </ p >
255335 </ motion . div >
256336 </ div >
0 commit comments