diff --git a/apps/web/src/components/editor/timeline-element.tsx b/apps/web/src/components/editor/timeline-element.tsx
index 211c4e9..0d25860 100644
--- a/apps/web/src/components/editor/timeline-element.tsx
+++ b/apps/web/src/components/editor/timeline-element.tsx
@@ -19,6 +19,10 @@ import AudioWaveform from "./audio-waveform";
import { toast } from "sonner";
import { TimelineElementProps, TrackType } from "@/types/timeline";
import { useTimelineElementResize } from "@/hooks/use-timeline-element-resize";
+import {
+ getTrackElementClasses,
+ TIMELINE_CONSTANTS,
+} from "@/lib/timeline-constants";
import {
DropdownMenu,
DropdownMenuContent,
@@ -67,7 +71,10 @@ export function TimelineElement({
const effectiveDuration =
element.duration - element.trimStart - element.trimEnd;
- const elementWidth = Math.max(80, effectiveDuration * 50 * zoomLevel);
+ const elementWidth = Math.max(
+ TIMELINE_CONSTANTS.ELEMENT_MIN_WIDTH,
+ effectiveDuration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel
+ );
// Use real-time position during drag, otherwise use stored position
const isBeingDragged = dragState.elementId === element.id;
@@ -77,19 +84,6 @@ export function TimelineElement({
: element.startTime;
const elementLeft = elementStartTime * 50 * zoomLevel;
- const getTrackColor = (type: TrackType) => {
- switch (type) {
- case "media":
- return "bg-blue-500/20 border-blue-500/30";
- case "text":
- return "bg-purple-500/20 border-purple-500/30";
- case "audio":
- return "bg-green-500/20 border-green-500/30";
- default:
- return "bg-gray-500/20 border-gray-500/30";
- }
- };
-
const handleDeleteElement = () => {
removeElementFromTrack(track.id, element.id);
setElementMenuOpen(false);
@@ -271,7 +265,7 @@ export function TimelineElement({
onMouseLeave={resizing ? handleResizeEnd : undefined}
>
t.id === track.id);
// Determine drop zone within the track (top 20px, middle 20px, bottom 20px)
@@ -619,7 +624,7 @@ export function TimelineTrackContent({
type: "text",
name: dragData.name || "Text",
content: dragData.content || "Default Text",
- duration: 5,
+ duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
startTime: snappedTime,
trimStart: 0,
trimEnd: 0,
diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx
index f98ab5e..6a94ed6 100644
--- a/apps/web/src/components/editor/timeline.tsx
+++ b/apps/web/src/components/editor/timeline.tsx
@@ -44,6 +44,10 @@ import {
} from "../ui/select";
import { TimelineTrackContent } from "./timeline-track";
import type { DragData } from "@/types/timeline";
+import {
+ getTrackLabelColor,
+ TIMELINE_CONSTANTS,
+} from "@/lib/timeline-constants";
export function Timeline() {
// Timeline shows all tracks (video, audio, effects) and their elements.
@@ -105,8 +109,8 @@ export function Timeline() {
// Dynamic timeline width calculation based on playhead position and duration
const dynamicTimelineWidth = Math.max(
- (duration || 0) * 50 * zoomLevel, // Base width from duration
- (currentTime + 30) * 50 * zoomLevel, // Width to show current time + 30 seconds buffer
+ (duration || 0) * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel, // Base width from duration
+ (currentTime + 30) * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel, // Width to show current time + 30 seconds buffer
timelineRef.current?.clientWidth || 1000 // Minimum width
);
@@ -236,10 +240,11 @@ export function Timeline() {
let newSelection: { trackId: string; elementId: string }[] = [];
tracks.forEach((track, trackIdx) => {
track.elements.forEach((element) => {
- const clipLeft = element.startTime * 50 * zoomLevel;
- const clipTop = trackIdx * 60;
- const clipBottom = clipTop + 60;
- const clipRight = clipLeft + 60;
+ const clipLeft =
+ element.startTime * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
+ const clipTop = trackIdx * TIMELINE_CONSTANTS.TRACK_HEIGHT;
+ const clipBottom = clipTop + TIMELINE_CONSTANTS.TRACK_HEIGHT;
+ const clipRight = clipLeft + TIMELINE_CONSTANTS.TRACK_HEIGHT;
if (
bx1 < clipRight &&
bx2 > clipLeft &&
@@ -334,7 +339,7 @@ export function Timeline() {
type: "text",
name: dragData.name || "Text",
content: dragData.content || "Default Text",
- duration: 5,
+ duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
startTime: 0,
trimStart: 0,
trimEnd: 0,
@@ -721,7 +726,8 @@ export function Timeline() {
"[data-radix-scroll-area-viewport]"
) as HTMLElement;
if (!rulerViewport || !tracksViewport) return;
- const playheadPx = playheadPosition * 50 * zoomLevel;
+ const playheadPx =
+ playheadPosition * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
const viewportWidth = rulerViewport.clientWidth;
const scrollMin = 0;
const scrollMax = rulerViewport.scrollWidth - viewportWidth;
@@ -792,7 +798,7 @@ export function Timeline() {
type: "media",
mediaId: "test",
name: "Test Clip",
- duration: 5,
+ duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
startTime: 0,
trimStart: 0,
trimEnd: 0,
@@ -927,7 +933,8 @@ export function Timeline() {
{(() => {
// Calculate appropriate time interval based on zoom level
const getTimeInterval = (zoom: number) => {
- const pixelsPerSecond = 50 * zoom;
+ const pixelsPerSecond =
+ TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoom;
if (pixelsPerSecond >= 200) return 0.1; // Every 0.1s when very zoomed in
if (pixelsPerSecond >= 100) return 0.5; // Every 0.5s when zoomed in
if (pixelsPerSecond >= 50) return 1; // Every 1s at normal zoom
@@ -955,7 +962,9 @@ export function Timeline() {
? "border-l border-muted-foreground/40"
: "border-l border-muted-foreground/20"
}`}
- style={{ left: `${time * 50 * zoomLevel}px` }}
+ style={{
+ left: `${time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel}px`,
+ }}
>
@@ -1014,13 +1025,7 @@ export function Timeline() {
>
{track.name}
@@ -1043,7 +1048,7 @@ export function Timeline() {
{
// If clicking empty area (not on a element), deselect all elements
@@ -1092,8 +1097,8 @@ export function Timeline() {
diff --git a/apps/web/src/lib/timeline-constants.ts b/apps/web/src/lib/timeline-constants.ts
new file mode 100644
index 0000000..fbbfe08
--- /dev/null
+++ b/apps/web/src/lib/timeline-constants.ts
@@ -0,0 +1,48 @@
+import type { TrackType } from "@/types/timeline";
+
+// Track color definitions
+export const TRACK_COLORS = {
+ media: {
+ solid: "bg-blue-500",
+ background: "bg-blue-500/20",
+ border: "border-blue-500/30",
+ },
+ text: {
+ solid: "bg-purple-500",
+ background: "bg-purple-500/20",
+ border: "border-purple-500/30",
+ },
+ audio: {
+ solid: "bg-green-500",
+ background: "bg-green-500/20",
+ border: "border-green-500/30",
+ },
+ default: {
+ solid: "bg-gray-500",
+ background: "bg-gray-500/20",
+ border: "border-gray-500/30",
+ },
+} as const;
+
+// Utility functions
+export function getTrackColors(type: TrackType) {
+ return TRACK_COLORS[type] || TRACK_COLORS.default;
+}
+
+export function getTrackElementClasses(type: TrackType) {
+ const colors = getTrackColors(type);
+ return `${colors.background} ${colors.border}`;
+}
+
+export function getTrackLabelColor(type: TrackType) {
+ return getTrackColors(type).solid;
+}
+
+// Other timeline constants
+export const TIMELINE_CONSTANTS = {
+ ELEMENT_MIN_WIDTH: 80,
+ PIXELS_PER_SECOND: 50,
+ TRACK_HEIGHT: 60,
+ DEFAULT_TEXT_DURATION: 5,
+ ZOOM_LEVELS: [0.25, 0.5, 1, 1.5, 2, 3, 4],
+} as const;