@@ -22,7 +22,13 @@ import {
2222 scrollFadeAttributes ,
2323 scrollFadeXClass ,
2424} from '@sim/emcn'
25- import { AnimatePresence , motion , useReducedMotion } from 'framer-motion'
25+ import {
26+ AnimatePresence ,
27+ type AnimationPlaybackControls ,
28+ animate ,
29+ motion ,
30+ useReducedMotion ,
31+ } from 'framer-motion'
2632import { Plus , X } from '../../icons'
2733import { cn } from '../../lib/cn'
2834import { Button } from '../button/button'
@@ -32,6 +38,12 @@ import { TabStripAction } from './tab-strip-action'
3238const DRAG_EDGE_ZONE = 40
3339const DRAG_SCROLL_SPEED = 8
3440const TAB_TRANSITION = { duration : 0.1 , ease : [ 0.2 , 0 , 0 , 1 ] as const }
41+ /**
42+ * Fixed duration for bringing the active tab into view. Native smooth scrolling
43+ * takes longer the further it travels, so a crowded strip crawled across dozens
44+ * of tabs.
45+ */
46+ const REVEAL_SCROLL_TRANSITION = { duration : 0.2 , ease : TAB_TRANSITION . ease }
3547
3648/**
3749 * Width, not flex-basis: `flex-1` compiles to `flex: 1 1 0%`, and Tailwind emits
@@ -508,6 +520,7 @@ export function TabStrip({
508520 const dropTargetIndexRef = useRef < number | null > ( null )
509521 const autoScrollRafRef = useRef < number | null > ( null )
510522 const autoScrollDirectionRef = useRef ( 0 )
523+ const revealScrollRef = useRef < AnimationPlaybackControls | null > ( null )
511524 const focusedTabRef = useRef < {
512525 id : string
513526 element : HTMLButtonElement
@@ -537,6 +550,11 @@ export function TabStrip({
537550 setCanScrollRight ( node . scrollLeft < maxScrollLeft - 1 )
538551 } , [ ] )
539552
553+ const stopRevealScroll = useCallback ( ( ) => {
554+ revealScrollRef . current ?. stop ( )
555+ revealScrollRef . current = null
556+ } , [ ] )
557+
540558 const stopAutoScroll = useCallback ( ( ) => {
541559 if ( autoScrollRafRef . current !== null ) cancelAnimationFrame ( autoScrollRafRef . current )
542560 autoScrollRafRef . current = null
@@ -579,9 +597,20 @@ export function TabStrip({
579597 // gradient at a scroll extreme, so no margin is needed to clear one.
580598 const nextLeft = Math . max ( 0 , Math . min ( maxScrollLeft , target ) )
581599 if ( Math . abs ( nextLeft - node . scrollLeft ) < 1 ) return
582- const reduceMotion = window . matchMedia ?.( '(prefers-reduced-motion: reduce)' ) . matches ?? false
583- node . scrollTo ( { left : nextLeft , behavior : reduceMotion ? 'auto' : 'smooth' } )
584- } , [ activeRegularId , regularTabOrder ] )
600+ stopRevealScroll ( )
601+ if ( reduceMotion ) {
602+ node . scrollLeft = nextLeft
603+ return
604+ }
605+ revealScrollRef . current = animate ( node . scrollLeft , nextLeft , {
606+ ...REVEAL_SCROLL_TRANSITION ,
607+ onUpdate : ( left ) => {
608+ node . scrollLeft = left
609+ } ,
610+ } )
611+ } , [ activeRegularId , regularTabOrder , reduceMotion , stopRevealScroll ] )
612+
613+ useEffect ( ( ) => stopRevealScroll , [ stopRevealScroll ] )
585614
586615 useLayoutEffect ( ( ) => {
587616 revealActiveTab ( )
@@ -621,13 +650,14 @@ export function TabStrip({
621650 event . deltaY
622651 )
623652 if ( next === null ) return
653+ stopRevealScroll ( )
624654 node . scrollLeft = next
625655 updateOverflow ( )
626656 event . preventDefault ( )
627657 }
628658 strip . addEventListener ( 'wheel' , handleWheel , { passive : false } )
629659 return ( ) => strip . removeEventListener ( 'wheel' , handleWheel )
630- } , [ updateOverflow ] )
660+ } , [ stopRevealScroll , updateOverflow ] )
631661
632662 const handleDragStart = useCallback (
633663 ( event : ReactDragEvent < HTMLDivElement > , id : string ) => {
@@ -677,6 +707,7 @@ export function TabStrip({
677707 stopAutoScroll ( )
678708 if ( direction === 0 ) return
679709 autoScrollDirectionRef . current = direction
710+ stopRevealScroll ( )
680711 const tick = ( ) => {
681712 const before = node . scrollLeft
682713 node . scrollLeft += direction * DRAG_SCROLL_SPEED
@@ -690,7 +721,7 @@ export function TabStrip({
690721 }
691722 autoScrollRafRef . current = requestAnimationFrame ( tick )
692723 } ,
693- [ stopAutoScroll , tabs , updateOverflow ]
724+ [ stopAutoScroll , stopRevealScroll , tabs , updateOverflow ]
694725 )
695726
696727 const handleDragOver = useCallback (
0 commit comments