refactor: decompose ClipBlock.tsx into focused sub-components#1052
refactor: decompose ClipBlock.tsx into focused sub-components#1052
Conversation
Closes #1026 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR refactors the timeline ClipBlock by extracting drag/hover/waveform-upgrade logic and several UI subcomponents, aiming to keep ClipBlock smaller and more focused while preserving its external API.
Changes:
- Extracted clip drag state machine into
useClipDragand hover/cursor handling intouseClipHover. - Extracted waveform peak upgrade effect into
useWaveformUpgrade. - Extracted UI pieces into
ClipFadeHandles,ClipDragGhost,ClipContextMenuContainer,ClipVersionNav, andclipPresentation.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/components/timeline/ClipBlock.tsx | Rewired to use new hooks/components; removed in-file drag/hover/context-menu/ghost logic. |
| src/components/timeline/useClipDrag.ts | New hook implementing clip drag/resize/slip/scissor/range-selection behavior. |
| src/components/timeline/useClipHover.ts | New hook for hover state and cursor management. |
| src/components/timeline/useWaveformUpgrade.ts | New hook for async waveform peak upgrade. |
| src/components/timeline/ClipFadeHandles.tsx | Extracted fade overlays + fade handle pointer/keyboard interactions. |
| src/components/timeline/ClipDragGhost.tsx | Extracted drag ghost portal rendering + lane highlighting. |
| src/components/timeline/ClipContextMenuContainer.tsx | Extracted context menu wiring/handlers assembly. |
| src/components/timeline/ClipVersionNav.tsx | Extracted version navigation UI + regenerate behavior. |
| src/components/timeline/clipPresentation.ts | Extracted clip color/style computation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| getClipContentOffset, | ||
| getClipSourceSpan, | ||
| } from '../../utils/clipAudio'; | ||
| import { ARRANGEMENT_EMPTY_TRACK_ID_PREFIX, parseArrangementEmptyTrackSlotIndex } from '../arrangement/trackSlotLayout'; |
There was a problem hiding this comment.
Unused import: ARRANGEMENT_EMPTY_TRACK_ID_PREFIX is imported but never used (only parseArrangementEmptyTrackSlotIndex is). Removing the unused symbol will keep the module focused and easier to scan.
| import { ARRANGEMENT_EMPTY_TRACK_ID_PREFIX, parseArrangementEmptyTrackSlotIndex } from '../arrangement/trackSlotLayout'; | |
| import { parseArrangementEmptyTrackSlotIndex } from '../arrangement/trackSlotLayout'; |
| const handleClick = useCallback((e: React.MouseEvent) => { | ||
| e.stopPropagation(); | ||
| if (dragRef.current || rangePreviewCommittedRef.current) { | ||
| if (rangePreviewCommittedRef.current) { | ||
| rangePreviewCommittedRef.current = false; | ||
| return; | ||
| } |
There was a problem hiding this comment.
handleClick no longer checks whether the last interaction was a drag (previously guarded by dragRef.current). After a move/resize drag ends with mouseup over the clip, a click can still fire and this handler will select/seek unexpectedly. Consider destructuring dragRef from useClipDrag and restoring the drag guard (or otherwise suppressing click after a drag).
| const handleDoubleClick = useCallback((e: React.MouseEvent) => { | ||
| e.stopPropagation(); | ||
| if (dragRef.current || rangePreviewCommittedRef.current) { | ||
| if (rangePreviewCommittedRef.current) { | ||
| rangePreviewCommittedRef.current = false; | ||
| return; | ||
| } |
There was a problem hiding this comment.
handleDoubleClick also lost the previous dragRef.current guard. If a drag gesture ends on the clip, a subsequent dblclick event can still be emitted and open the context menu / piano roll unexpectedly. Re-introduce the drag suppression here as well (e.g., via dragRef from useClipDrag).
| import type { Clip, Track } from '../../types/project'; | ||
| import { useUIStore } from '../../store/uiStore'; | ||
| import { useProjectStore } from '../../store/projectStore'; | ||
| import { useTransportStore } from '../../store/transportStore'; |
There was a problem hiding this comment.
Unused import: useTransportStore is imported but never referenced in this hook. Removing it will reduce noise and avoid confusion about transport state dependencies.
| import { useTransportStore } from '../../store/transportStore'; |
Summary
Closes #1026
Decomposes
ClipBlock.tsxfrom 1481 lines down to 486 lines (67% reduction) by extracting focused sub-components and hooks:useClipDrag.ts(566 lines) -- drag state machine (move, resize-left, resize-right, slip, scissor, range selection)useClipHover.ts(85 lines) -- cursor management and hover state trackinguseWaveformUpgrade.ts(72 lines) -- async waveform peak upgrade effectClipFadeHandles.tsx(185 lines) -- fade in/out handles, overlays, and keyboard/mouse interactionsClipDragGhost.tsx(153 lines) -- drag ghost portal rendering with cross-track highlightClipContextMenuContainer.tsx(161 lines) -- context menu store wiring and callback assemblyClipVersionNav.tsx(70 lines) -- version navigation buttonsclipPresentation.ts(41 lines) -- clip color/style computationPure refactor -- no behavior changes. Exported API of
ClipBlockis unchanged.Test plan
npx tsc --noEmit-- 0 type errorsnpm test -- --run-- all 3008 tests pass (319 test files)npm run build-- succeeds🤖 Generated with Claude Code