Timeline Scrubber <mv-timeline>

동영상 편집기식 시간 눈금자: 스크럽하는 동안 타임코드를 보여 주는 프레임 단위로 정확한 재생 헤드, 호버 시 값 표시, 반 옥타브 단계의 확대/축소(버튼, 키, Ctrl/⌘ + 휠 또는 핀치), 마커 레인, 범위 바깥을 모두 가리는 in/out 핸들, 트랙 위의 클립과 개요 바를 제공합니다.

작동 방식

동영상 편집기식 시간 눈금자: 스크럽하는 동안 타임코드를 보여 주는 프레임 단위로 정확한 재생 헤드, 호버 시 값 표시, 반 옥타브 단계의 확대/축소(버튼, 키, Ctrl/⌘ + 휠 또는 핀치), 마커 레인, 범위 바깥을 모두 가리는 in/out 핸들, 트랙 위의 클립과 개요 바를 제공합니다. 모든 부분을 키보드로 조작할 수 있습니다. 폼과 연동됩니다(form-associated).

카테고리폼
유형Web Component (<mv-timeline>)
상태안정
Keywordssafe-rewrite, timeline, playhead, scrubber, timecode, video, zoom, markers, in-out, editor, form-associated, keyboard

When to use

  • A video, audio or animation editor needs a frame-accurate playhead with a timecode
  • Users set in and out points, jump between cuts and markers and zoom along a time ruler, by pointer or keyboard
  • Clips must be shown on tracks with play, pause and time events for syncing media

Avoid when

  • The need is a simple media progress bar with seeking → use Slider instead
  • Users browse the edit history of a form rather than media time → use Rewind instead
  • The page lists dated events such as an order history, a changelog or an activity log → use Activity Timeline instead

설치

node scripts/add.mjs timeline --out ./src/marvelous

Marvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["timeline"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/form.js, core/motion.js, core/observe.js, components/timeline/timeline.js, components/timeline/timeline.css.

사용법

기본 마크업입니다. 여기서 시작해 속성, data-*, CSS 변수로 커스터마이즈하세요:

<div id="tl-demo">
  <style>
    #tl-demo { display: grid; grid-template-columns: minmax(0, 1fr) 14rem; gap: .75rem; width: min(100%, 52rem); margin: 0 auto; }
    #tl-demo .tld-monitor { position: relative; aspect-ratio: 16 / 8; overflow: hidden; border-radius: var(--mv-radius-lg); background: oklch(0.16 0.02 270); border: 1px solid var(--mv-border); }
    #tl-demo .tld-sky { position: absolute; inset: 0; background: linear-gradient(180deg, oklch(0.35 0.12 280), oklch(0.62 0.16 30)); }
    #tl-demo .tld-sun { position: absolute; left: 50%; width: 5rem; height: 5rem; margin-left: -2.5rem; border-radius: 50%; background: radial-gradient(circle, oklch(0.95 0.1 90), oklch(0.8 0.17 60)); box-shadow: 0 0 60px oklch(0.85 0.15 70 / .6); }
    #tl-demo .tld-hills { position: absolute; inset: auto -10% -30% -10%; height: 60%; border-radius: 50% 50% 0 0; background: oklch(0.22 0.05 280); }
    #tl-demo .tld-title { position: absolute; left: 0; right: 0; top: 18%; text-align: center; color: #fff; font: 700 clamp(1rem, 3vw, 1.6rem)/1.1 var(--mv-font-sans); letter-spacing: -.01em; text-shadow: 0 2px 16px oklch(0 0 0 / .35); }
    #tl-demo .tld-title small { display: block; margin-top: .35rem; font: 500 .7rem var(--mv-font-mono); letter-spacing: .12em; text-transform: uppercase; opacity: .8; }
    #tl-demo .tld-tc { position: absolute; right: .6rem; bottom: .5rem; padding: .15rem .4rem; border-radius: 4px; background: oklch(0 0 0 / .5); color: #fff; font: 500 .65rem var(--mv-font-mono); }
    #tl-demo .tld-panel { display: grid; align-content: start; gap: .45rem; padding: .75rem; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-lg); background: var(--mv-surface); font-size: .72rem; color: var(--mv-fg-muted); }
    #tl-demo .tld-head { margin: 0 0 .15rem; font: 600 .7rem var(--mv-font-sans); letter-spacing: .06em; text-transform: uppercase; color: var(--mv-fg-subtle); }
    #tl-demo .tld-row { display: flex; justify-content: space-between; gap: .5rem; }
    #tl-demo .tld-row b { color: var(--mv-fg); font: 600 .72rem var(--mv-font-mono); font-variant-numeric: tabular-nums; }
    #tl-demo .tld-hint { margin: .3rem 0 0; line-height: 1.5; }
    #tl-demo kbd { font: 500 .62rem var(--mv-font-mono); padding: 0 .25rem; border-radius: 3px; background: var(--mv-bg-muted); border: 1px solid var(--mv-border); }
    #tl-demo mv-timeline { grid-column: 1 / -1; }
    @media (max-width: 640px) { #tl-demo { grid-template-columns: 1fr; } }
    /* The monitor is a picture of the frame: keep its colors under forced colors */
    #tl-demo .tld-monitor { forced-color-adjust: none; }
  </style>
  <div class="tld-monitor" aria-hidden="true">
    <div class="tld-sky" id="tl-demo-sky"></div>
    <div class="tld-sun" id="tl-demo-sun"></div>
    <div class="tld-hills"></div>
    <div class="tld-title" id="tl-demo-title">Sunrise<small>A film by Ines Okafor</small></div>
    <span class="tld-tc" id="tl-demo-tc">00:00:00</span>
  </div>
  <div class="tld-panel" role="group" aria-label="Inspector">
    <p class="tld-head">Inspector</p>
    <div class="tld-row"><span>Shot</span><b id="tl-demo-clip">-</b></div>
    <div class="tld-row"><span>Frame</span><b id="tl-demo-frame">0</b></div>
    <div class="tld-row"><span>Sun (Y)</span><b id="tl-demo-y">-</b></div>
    <div class="tld-row"><span>Title (opacity)</span><b id="tl-demo-op">-</b></div>
    <div class="tld-row"><span>Range</span><b id="tl-demo-range">-</b></div>
    <p class="tld-hint"><kbd>Space</kbd> play, <kbd>←</kbd>/<kbd>→</kbd> one frame (<kbd>Shift</kbd> ×10), <kbd>↑</kbd>/<kbd>↓</kbd> next cut or marker, <kbd>I</kbd>/<kbd>O</kbd> in/out, <kbd>+</kbd>/<kbd>−</kbd> or <kbd>Ctrl</kbd> + wheel to zoom.</p>
  </div>
  <mv-timeline id="tl-demo-timeline" name="time" label="Edit “Sunrise”" duration="24" fps="25" value="3.2" in="2" out="20" loop markers="6:Title, 12.5:Zenith, 18:Credits">
    <div data-clip data-start="0" data-end="9.5" data-track="0">Wide shot · dawn</div>
    <div data-clip data-start="9.5" data-end="16" data-track="0" style="--mv-timeline-clip: var(--mv-info)">Close-up</div>
    <div data-clip data-start="16" data-end="24" data-track="0" style="--mv-timeline-clip: var(--mv-success)">Credits</div>
    <div data-clip data-start="5" data-end="11" data-track="1" style="--mv-timeline-clip: var(--mv-warning)">Title</div>
  </mv-timeline>
</div>
<script type="module">
  const $ = (id) => document.getElementById(`tl-demo-${id}`);
  const tl = $("timeline");
  const ease = (x) => 1 - (1 - x) ** 3;
  const clamp01 = (x) => Math.min(1, Math.max(0, x));
  const render = () => {
    const t = tl.value;
    const rise = ease(clamp01(t / 12.5));
    const y = 75 - rise * 55;
    $("sun").style.top = `${y}%`;
    $("sky").style.filter = `brightness(${0.55 + rise * 0.6}) saturate(${0.8 + rise * 0.4})`;
    const op = clamp01((t - 5) / 1) * clamp01((11 - t) / 1);
    $("title").style.opacity = op;
    $("title").style.translate = `0 ${(1 - op) * 10}px`;
    $("tc").textContent = tl.timecode;
    $("frame").textContent = tl.frame;
    $("y").textContent = `${y.toFixed(1)}%`;
    $("op").textContent = `${Math.round(op * 100)}%`;
    const clip = [...tl.querySelectorAll("[data-clip][data-track='0']")].find((c) => t >= +c.dataset.start && t < +c.dataset.end);
    $("clip").textContent = clip?.textContent ?? "-";
    const r = tl.range;
    $("range").textContent = r ? `${r.in.toFixed(2)} → ${r.out.toFixed(2)} s` : "-";
  };
  for (const t of ["mv-input", "mv-timeupdate", "mv-change", "mv-range"]) tl.addEventListener(t, render);
  customElements.whenDefined("mv-timeline").then(() => requestAnimationFrame(render));
</script>

API

Attributes

Name유형DefaultDescription
valuenumber0Initial time in seconds (snapped to the frame). Restored on reset.
durationnumber60Total duration in seconds.
fpsnumber30Frames per second: playhead step and the “ff” of the timecode.
in / outnumberPlayback range (seconds). Draggable, focusable handles on the ruler; I and O keys; a clear button in the toolbar.
markersstring“6:Title, 12.5:Zenith”: time and label, shown in their own lane, clickable and reachable with the keyboard.
loopbooleanPlayback loops within the range (or the full duration).
ratenumber1Playback rate.
labelstringGroup name.
name / disabled / readonlyStandard behavior; readonly allows playback but not range editing.

Properties

Name유형Description
value / currentTimenumberCurrent time (s).
frame / timecodenumber / stringFrame number and “mm:ss:ff”.
playingbooleanWhether playback is running.
range{ in, out } | nullIn/out range.
zoomnumberPixels per second (read/write). Zoom moves on quarter steps of a ×2 ladder above “fit”, so a written value snaps to the nearest step.
stringsobjectOverrides for the visible and spoken labels (play, pause, playhead, setIn, setOut, clear, inPoint, outPoint, zoomIn, zoomOut, fit, markers, marker, frame, help, group).

Methods

NameDescription
play() / pause() / toggle()Playback. It keeps running with reduced motion (it is content, not decoration) and pauses while the tab is hidden.
setRange(in, out)Sets the range (or clears it with null).
timecode(seconds, fps)Module export: mm:ss:ff formatting.

Events

NameDescription
mv-inputWhile dragging or on a keyboard step. detail: { time, frame, timecode }.
mv-changeOn release, keyboard step, marker click and pause.
mv-timeupdateOn every frame during playback.
mv-play / mv-pausePlayback start and stop.
mv-range / mv-zoomRange changed ({ range }); zoom changed ({ zoom, ratio }, ratio = scale relative to fit).

Content structure

NameDescription
[data-clip]Clips positioned by data-start / data-end (s) on track data-track (0, 1…). Color via --mv-timeline-clip.

CSS classes

NameDescription
mv-timeline-toolbar / -ruler / -markers / -marker / -tracks / -clip / -playhead / -playhead-tip / -range / -range-handle / -hover / -scrollbar / -thumbGenerated parts.

CSS variables

NameDefaultDescription
--mv-timeline-playheadvar(--mv-accent)Playhead color.
--mv-timeline-markervar(--mv-warning)Marker color.
--mv-timeline-clipvar(--mv-accent)Clip color (set on the [data-clip] element).

Accessibility

Named role="group". The playhead is a focusable role="slider" (aria-valuetext “00:03:05, frame 80”) described by a hidden shortcut summary; its timecode tip appears on focus and while scrubbing. Keyboard: ←/→ one frame (Shift ×10), PageUp/PageDown ±1 s, ↑/↓ next or previous cut, marker or range bound, Home/End to the in/out points then the ends, Space or K to play/pause, I/O to set in/out, +/− to zoom and 0 to fit. The in and out handles are sliders too (arrows, Page keys, Home/End, Delete clears the range). Markers are one tab stop with arrow-key roving; Enter jumps to the marker. Toolbar buttons are labeled (play uses aria-pressed), labels are overridable through strings. Escape while dragging restores the starting time or range; Shift while dragging snaps to markers, cuts and range bounds. The hover readout needs a precise pointer and never hides the system cursor. Forced colors use system colors.

이 페이지는 AI로 번역되었습니다. 번역 문제 신고