1- 'use client'
2-
3- import { type PointerEvent , useEffect , useRef } from 'react'
41import { cn } from '@sim/emcn'
52import Image from 'next/image'
63import { LOGOS , MUTED_MARK } from '@/app/(landing)/components/logos'
@@ -13,113 +10,25 @@ import styles from '@/app/(landing)/components/navbar/components/nav-menu-chip/c
1310 * even: a copy is about 800px, so 16s is roughly 50px/s.
1411 */
1512const COPIES = [ 0 , 1 ] as const
13+ const TRACK_MOTION = styles . track
1614const EDGE_FADE =
1715 '[mask-image:linear-gradient(to_right,transparent,black_14%,black_86%,transparent)]'
1816const LABEL = 'Companies building and governing AI agents with Sim'
19- const KEYBOARD_STEP = 120
20-
21- interface MarqueeDrag {
22- pointerId : number
23- lastX : number
24- }
25-
26- function moveTrack ( track : HTMLDivElement | null , distance : number ) {
27- const animation = track ?. getAnimations ( ) [ 0 ]
28- const duration = animation ?. effect ?. getComputedTiming ( ) . duration
29- const width = ( track ?. offsetWidth ?? 0 ) / COPIES . length
30- if ( ! animation || typeof duration !== 'number' || ! duration || ! width ) return
31-
32- const time = Number ( animation . currentTime ?? 0 ) - ( distance / width ) * duration
33- animation . currentTime = ( ( time % duration ) + duration ) % duration
34- }
3517
3618/**
3719 * The customer wordmarks sliding in a muted band under a floating menu's
3820 * blocs - the homepage's shared logo set at its optical sizes, the way the
3921 * platform pages show them. The second copy of the row exists only for the
4022 * seamless loop and is hidden from assistive technology; a reduced-motion
41- * preference leaves the row still. Dragging, trackpad scrolling, and arrow
42- * keys scrub the same animation timeline, preserving its position when
43- * autoplay resumes.
23+ * preference leaves the row still.
4424 */
4525export function NavMenuLogoMarquee ( ) {
46- const viewportRef = useRef < HTMLDivElement > ( null )
47- const trackRef = useRef < HTMLDivElement > ( null )
48- const dragRef = useRef < MarqueeDrag | null > ( null )
49-
50- useEffect ( ( ) => {
51- const viewport = viewportRef . current
52- if ( ! viewport ) return
53-
54- const onWheel = ( event : WheelEvent ) => {
55- if ( event . ctrlKey || dragRef . current ) return
56- const delta = event . deltaX || ( event . shiftKey ? event . deltaY : 0 )
57- if ( ! delta || ( ! event . shiftKey && Math . abs ( event . deltaY ) > Math . abs ( event . deltaX ) ) ) return
58-
59- const unit =
60- event . deltaMode === WheelEvent . DOM_DELTA_LINE
61- ? 16
62- : event . deltaMode === WheelEvent . DOM_DELTA_PAGE
63- ? viewport . clientWidth
64- : 1
65- if ( event . cancelable ) event . preventDefault ( )
66- moveTrack ( trackRef . current , - delta * unit )
67- }
68-
69- /** Native wheel deltas include trackpad momentum; non-passive handling keeps horizontal swipes in the carousel. */
70- viewport . addEventListener ( 'wheel' , onWheel , { passive : false } )
71- return ( ) => viewport . removeEventListener ( 'wheel' , onWheel )
72- } , [ ] )
73-
74- const endDrag = ( event : PointerEvent < HTMLDivElement > ) => {
75- if ( dragRef . current ?. pointerId !== event . pointerId ) return
76- dragRef . current = null
77- delete event . currentTarget . dataset . dragging
78- if ( event . currentTarget . hasPointerCapture ( event . pointerId ) ) {
79- event . currentTarget . releasePointerCapture ( event . pointerId )
80- }
81- }
82-
8326 return (
84- < div
85- ref = { viewportRef }
86- role = 'region'
87- aria-label = 'Customer logos'
88- aria-description = 'Drag, scroll horizontally, or use the left and right arrow keys to browse customer logos.'
89- /** biome-ignore lint/a11y/noNoninteractiveTabindex: Keyboard users can scrub this carousel with the arrow keys. */
90- tabIndex = { 0 }
91- className = { cn (
92- 'cursor-grab touch-pan-y touch-pinch-zoom select-none overflow-hidden py-2 focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--text-secondary)] focus-visible:outline-offset-[-2px] data-[dragging]:cursor-grabbing' ,
93- styles . viewport ,
94- EDGE_FADE
95- ) }
96- onPointerDown = { ( event ) => {
97- if ( ! event . isPrimary || event . button !== 0 ) return
98- event . preventDefault ( )
99- dragRef . current = { pointerId : event . pointerId , lastX : event . clientX }
100- event . currentTarget . dataset . dragging = ''
101- event . currentTarget . setPointerCapture ( event . pointerId )
102- } }
103- onPointerMove = { ( event ) => {
104- const drag = dragRef . current
105- if ( ! drag || drag . pointerId !== event . pointerId ) return
106- moveTrack ( trackRef . current , event . clientX - drag . lastX )
107- drag . lastX = event . clientX
108- } }
109- onPointerUp = { endDrag }
110- onPointerCancel = { endDrag }
111- onLostPointerCapture = { endDrag }
112- onKeyDown = { ( event ) => {
113- if ( event . key !== 'ArrowLeft' && event . key !== 'ArrowRight' ) return
114- event . preventDefault ( )
115- moveTrack ( trackRef . current , event . key === 'ArrowLeft' ? KEYBOARD_STEP : - KEYBOARD_STEP )
116- } }
117- >
27+ < div className = { cn ( 'overflow-hidden py-2' , EDGE_FADE ) } >
11828 < div
119- ref = { trackRef }
12029 className = { cn (
12130 'flex w-max items-center will-change-transform [backface-visibility:hidden]' ,
122- styles . track
31+ TRACK_MOTION
12332 ) }
12433 >
12534 { COPIES . map ( ( copy ) => {
@@ -139,8 +48,7 @@ export function NavMenuLogoMarquee() {
13948 height = { logo . height }
14049 width = { Math . round ( logo . height * logo . aspect ) }
14150 loading = 'lazy'
142- draggable = { false }
143- className = { cn ( 'pointer-events-none' , MUTED_MARK ) }
51+ className = { MUTED_MARK }
14452 />
14553 </ li >
14654 ) ) }
0 commit comments