From dced40cd975732c2c98f230a6c750af54ffdabd4 Mon Sep 17 00:00:00 2001
From: Mehek Jethani <mehek.jethani@gmail.com>
Date: Sat, 17 Jul 2021 15:04:27 -0400
Subject: basic trim ui complete - switching laptops

---
 src/client/views/nodes/AudioBox.tsx | 990 +++++++++++++++++++++++-------------
 1 file changed, 643 insertions(+), 347 deletions(-)

(limited to 'src/client/views/nodes/AudioBox.tsx')

diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index a2e36f12e..99960e934 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -1,6 +1,13 @@
 import React = require("react");
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import {
+  action,
+  computed,
+  IReactionDisposer,
+  observable,
+  reaction,
+  runInAction,
+} from "mobx";
 import { observer } from "mobx-react";
 import { DateField } from "../../../fields/DateField";
 import { Doc, DocListCast, Opt } from "../../../fields/Doc";
@@ -17,376 +24,665 @@ import { SnappingManager } from "../../util/SnappingManager";
 import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline";
 import { ContextMenu } from "../ContextMenu";
 import { ContextMenuProps } from "../ContextMenuItem";
-import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent";
+import {
+  ViewBoxAnnotatableComponent,
+  ViewBoxAnnotatableProps,
+} from "../DocComponent";
 import "./AudioBox.scss";
-import { FieldView, FieldViewProps } from './FieldView';
+import { FieldView, FieldViewProps } from "./FieldView";
 import { LinkDocPreview } from "./LinkDocPreview";
 declare class MediaRecorder {
-    constructor(e: any);  // whatever MediaRecorder has
+  constructor(e: any); // whatever MediaRecorder has
 }
 
 type AudioDocument = makeInterface<[typeof documentSchema]>;
 const AudioDocument = makeInterface(documentSchema);
 
 @observer
-export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps, AudioDocument>(AudioDocument) {
-    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); }
-    public static Enabled = false;
-    static playheadWidth = 30; // width of playhead
-    static heightPercent = 80; // height of timeline in percent of height of audioBox.
-    static Instance: AudioBox;
-
-    _disposers: { [name: string]: IReactionDisposer } = {};
-    _ele: HTMLAudioElement | null = null;
-    _stackedTimeline = React.createRef<CollectionStackedTimeline>();
-    _recorder: any;
-    _recordStart = 0;
-    _pauseStart = 0;
-    _pauseEnd = 0;
-    _pausedTime = 0;
-    _stream: MediaStream | undefined;
-    _start: number = 0;
-    _play: any = null;
-
-    @observable static _scrubTime = 0;
-    @observable _markerEnd: number = 0;
-    @observable _position: number = 0;
-    @observable _waveHeight: Opt<number> = this.layoutDoc._height;
-    @observable _paused: boolean = false;
-    @computed get mediaState(): undefined | "pendingRecording" | "recording" | "paused" | "playing" { return this.dataDoc.mediaState as (undefined | "pendingRecording" | "recording" | "paused" | "playing"); }
-    set mediaState(value) { this.dataDoc.mediaState = value; }
-    public static SetScrubTime = action((timeInMillisFrom1970: number) => { AudioBox._scrubTime = 0; AudioBox._scrubTime = timeInMillisFrom1970; });
-    @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); }
-    @computed get duration() { return NumCast(this.dataDoc[`${this.fieldKey}-duration`]); }
-    @computed get anchorDocs() { return DocListCast(this.dataDoc[this.annotationKey]); }
-    @computed get links() { return DocListCast(this.dataDoc.links); }
-    @computed get pauseTime() { return this._pauseEnd - this._pauseStart; } // total time paused to update the correct time
-    @computed get heightPercent() { return AudioBox.heightPercent; }
-
-    constructor(props: Readonly<ViewBoxAnnotatableProps & FieldViewProps>) {
-        super(props);
-        AudioBox.Instance = this;
-
-        if (this.duration === undefined) {
-            runInAction(() => this.Document[this.fieldKey + "-duration"] = this.Document.duration);
-        }
-    }
-
-    getLinkData(l: Doc) {
-        let la1 = l.anchor1 as Doc;
-        let la2 = l.anchor2 as Doc;
-        const linkTime = this._stackedTimeline.current?.anchorStart(la2) || this._stackedTimeline.current?.anchorStart(la1) || 0;
-        if (Doc.AreProtosEqual(la1, this.dataDoc)) {
-            la1 = l.anchor2 as Doc;
-            la2 = l.anchor1 as Doc;
-        }
-        return { la1, la2, linkTime };
-    }
-
-    getAnchor = () => {
-        return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey,
-            "_timecodeToShow" /* audioStart */, "_timecodeToHide" /* audioEnd */, this._ele?.currentTime ||
-            Cast(this.props.Document._currentTimecode, "number", null) || (this.mediaState === "recording" ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined))
-            || this.rootDoc;
+export class AudioBox extends ViewBoxAnnotatableComponent<
+  ViewBoxAnnotatableProps & FieldViewProps,
+  AudioDocument
+>(AudioDocument) {
+  public static LayoutString(fieldKey: string) {
+    return FieldView.LayoutString(AudioBox, fieldKey);
+  }
+  public static Enabled = false;
+  static playheadWidth = 30; // width of playhead
+  static heightPercent = 80; // height of timeline in percent of height of audioBox.
+  static Instance: AudioBox;
+
+  _disposers: { [name: string]: IReactionDisposer } = {};
+  _ele: HTMLAudioElement | null = null;
+  _stackedTimeline = React.createRef<CollectionStackedTimeline>();
+  _recorder: any;
+  _recordStart = 0;
+  _pauseStart = 0;
+  _pauseEnd = 0;
+  _pausedTime = 0;
+  _stream: MediaStream | undefined;
+  _start: number = 0;
+  _play: any = null;
+
+  @observable static _scrubTime = 0;
+  @observable _markerEnd: number = 0;
+  @observable _position: number = 0;
+  @observable _waveHeight: Opt<number> = this.layoutDoc._height;
+  @observable _paused: boolean = false;
+  @observable _trimming: boolean = false;
+  @observable _trimBounds: { start: number; end: number } = {
+    start: this.dataDoc.clipStart ? this.dataDoc.clipStart : 0,
+    end: this.dataDoc.clipEnd
+      ? this.dataDoc.clipEnd
+      : this.duration
+      ? this.duration
+      : undefined,
+  };
+  @computed get mediaState():
+    | undefined
+    | "pendingRecording"
+    | "recording"
+    | "paused"
+    | "playing" {
+    return this.dataDoc.mediaState as
+      | undefined
+      | "pendingRecording"
+      | "recording"
+      | "paused"
+      | "playing";
+  }
+  set mediaState(value) {
+    this.dataDoc.mediaState = value;
+  }
+  public static SetScrubTime = action((timeInMillisFrom1970: number) => {
+    AudioBox._scrubTime = 0;
+    AudioBox._scrubTime = timeInMillisFrom1970;
+  });
+  @computed get recordingStart() {
+    return Cast(
+      this.dataDoc[this.props.fieldKey + "-recordingStart"],
+      DateField
+    )?.date.getTime();
+  }
+  @computed get duration() {
+    return NumCast(this.dataDoc[`${this.fieldKey}-duration`]);
+  }
+  @computed get anchorDocs() {
+    return DocListCast(this.dataDoc[this.annotationKey]);
+  }
+  @computed get links() {
+    return DocListCast(this.dataDoc.links);
+  }
+  @computed get pauseTime() {
+    return this._pauseEnd - this._pauseStart;
+  } // total time paused to update the correct time
+  @computed get heightPercent() {
+    return AudioBox.heightPercent;
+  }
+
+  constructor(props: Readonly<ViewBoxAnnotatableProps & FieldViewProps>) {
+    super(props);
+    AudioBox.Instance = this;
+
+    if (this.duration === undefined) {
+      runInAction(
+        () =>
+          (this.Document[this.fieldKey + "-duration"] = this.Document.duration)
+      );
     }
-
-    componentWillUnmount() {
-        Object.values(this._disposers).forEach(disposer => disposer?.());
-        const ind = DocUtils.ActiveRecordings.indexOf(this);
-        ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
+  }
+
+  getLinkData(l: Doc) {
+    let la1 = l.anchor1 as Doc;
+    let la2 = l.anchor2 as Doc;
+    const linkTime =
+      this._stackedTimeline.current?.anchorStart(la2) ||
+      this._stackedTimeline.current?.anchorStart(la1) ||
+      0;
+    if (Doc.AreProtosEqual(la1, this.dataDoc)) {
+      la1 = l.anchor2 as Doc;
+      la2 = l.anchor1 as Doc;
     }
-
-    @action
-    componentDidMount() {
-        this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document.  this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
-
-        this.mediaState = this.path ? "paused" : undefined;
-
-        this._disposers.triggerAudio = reaction(
-            () => !LinkDocPreview.LinkInfo && this.props.renderDepth !== -1 ? NumCast(this.Document._triggerAudio, null) : undefined,
-            start => start !== undefined && setTimeout(() => {
-                this.playFrom(start);
-                setTimeout(() => {
-                    this.Document._currentTimecode = start;
-                    this.Document._triggerAudio = undefined;
-                }, 10);
-            }), // wait for mainCont and try again to play
-            { fireImmediately: true }
-        );
-
-        this._disposers.audioStop = reaction(
-            () => this.props.renderDepth !== -1 && !LinkDocPreview.LinkInfo ? Cast(this.Document._audioStop, "number", null) : undefined,
-            audioStop => audioStop !== undefined && setTimeout(() => {
-                this.Pause();
-                setTimeout(() => this.Document._audioStop = undefined, 10);
-            }), // wait for mainCont and try again to play
-            { fireImmediately: true }
+    return { la1, la2, linkTime };
+  }
+
+  getAnchor = () => {
+    return (
+      CollectionStackedTimeline.createAnchor(
+        this.rootDoc,
+        this.dataDoc,
+        this.annotationKey,
+        "_timecodeToShow" /* audioStart */,
+        "_timecodeToHide" /* audioEnd */,
+        this._ele?.currentTime ||
+          Cast(this.props.Document._currentTimecode, "number", null) ||
+          (this.mediaState === "recording"
+            ? (Date.now() - (this.recordingStart || 0)) / 1000
+            : undefined)
+      ) || this.rootDoc
+    );
+  };
+
+  componentWillUnmount() {
+    Object.values(this._disposers).forEach((disposer) => disposer?.());
+    const ind = DocUtils.ActiveRecordings.indexOf(this);
+    ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
+  }
+
+  @action
+  componentDidMount() {
+    this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document.  this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
+
+    this.mediaState = this.path ? "paused" : undefined;
+
+    this._disposers.triggerAudio = reaction(
+      () =>
+        !LinkDocPreview.LinkInfo && this.props.renderDepth !== -1
+          ? NumCast(this.Document._triggerAudio, null)
+          : undefined,
+      (start) =>
+        start !== undefined &&
+        setTimeout(() => {
+          this.playFrom(start);
+          setTimeout(() => {
+            this.Document._currentTimecode = start;
+            this.Document._triggerAudio = undefined;
+          }, 10);
+        }), // wait for mainCont and try again to play
+      { fireImmediately: true }
+    );
+
+    this._disposers.audioStop = reaction(
+      () =>
+        this.props.renderDepth !== -1 && !LinkDocPreview.LinkInfo
+          ? Cast(this.Document._audioStop, "number", null)
+          : undefined,
+      (audioStop) =>
+        audioStop !== undefined &&
+        setTimeout(() => {
+          this.Pause();
+          setTimeout(() => (this.Document._audioStop = undefined), 10);
+        }), // wait for mainCont and try again to play
+      { fireImmediately: true }
+    );
+  }
+
+  // for updating the timecode
+  timecodeChanged = () => {
+    const htmlEle = this._ele;
+    if (this.mediaState !== "recording" && htmlEle) {
+      htmlEle.duration &&
+        htmlEle.duration !== Infinity &&
+        runInAction(
+          () => (this.dataDoc[this.fieldKey + "-duration"] = htmlEle.duration)
         );
+      this.links
+        .map((l) => this.getLinkData(l))
+        .forEach(({ la1, la2, linkTime }) => {
+          if (
+            linkTime > NumCast(this.layoutDoc._currentTimecode) &&
+            linkTime < htmlEle.currentTime
+          ) {
+            Doc.linkFollowHighlight(la1);
+          }
+        });
+      this.layoutDoc._currentTimecode = htmlEle.currentTime;
     }
-
-    // for updating the timecode
-    timecodeChanged = () => {
-        const htmlEle = this._ele;
-        if (this.mediaState !== "recording" && htmlEle) {
-            htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc[this.fieldKey + "-duration"] = htmlEle.duration);
-            this.links.map(l => this.getLinkData(l)).forEach(({ la1, la2, linkTime }) => {
-                if (linkTime > NumCast(this.layoutDoc._currentTimecode) && linkTime < htmlEle.currentTime) {
-                    Doc.linkFollowHighlight(la1);
-                }
-            });
-            this.layoutDoc._currentTimecode = htmlEle.currentTime;
-        }
-    }
-
-    // pause play back
-    Pause = action(() => {
-        this._ele!.pause();
-        this.mediaState = "paused";
-    });
-
-    // play audio for documents created during recording
-    playFromTime = (absoluteTime: number) => {
-        this.recordingStart && this.playFrom((absoluteTime - this.recordingStart) / 1000);
-    }
-
-    // play back the audio from time
-    @action
-    playFrom = (seekTimeInSeconds: number, endTime: number = this.duration) => {
-        clearTimeout(this._play);
-        if (Number.isNaN(this._ele?.duration)) {
-            setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
-        } else if (this._ele && AudioBox.Enabled) {
-            if (seekTimeInSeconds < 0) {
-                if (seekTimeInSeconds > -1) {
-                    setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000);
-                } else {
-                    this.Pause();
-                }
-            } else if (seekTimeInSeconds <= this._ele.duration) {
-                this._ele.currentTime = seekTimeInSeconds;
-                this._ele.play();
-                runInAction(() => this.mediaState = "playing");
-                if (endTime !== this.duration) {
-                    this._play = setTimeout(() => this.Pause(), (endTime - seekTimeInSeconds) * 1000); // use setTimeout to play a specific duration
-                }
-            } else {
-                this.Pause();
-            }
+  };
+
+  // pause play back
+  Pause = action(() => {
+    this._ele!.pause();
+    this.mediaState = "paused";
+  });
+
+  // play audio for documents created during recording
+  playFromTime = (absoluteTime: number) => {
+    this.recordingStart &&
+      this.playFrom((absoluteTime - this.recordingStart) / 1000);
+  };
+
+  // play back the audio from time
+  @action
+  playFrom = (seekTimeInSeconds: number, endTime: number = this.duration) => {
+    clearTimeout(this._play);
+    if (Number.isNaN(this._ele?.duration)) {
+      setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
+    } else if (this._ele && AudioBox.Enabled) {
+      if (seekTimeInSeconds < 0) {
+        if (seekTimeInSeconds > -1) {
+          setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000);
+        } else {
+          this.Pause();
         }
-    }
-
-    // update the recording time
-    updateRecordTime = () => {
-        if (this.mediaState === "recording") {
-            setTimeout(this.updateRecordTime, 30);
-            if (this._paused) {
-                this._pausedTime += (new Date().getTime() - this._recordStart) / 1000;
-            } else {
-                this.layoutDoc._currentTimecode = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
-            }
+      } else if (seekTimeInSeconds <= this._ele.duration) {
+        this._ele.currentTime = seekTimeInSeconds;
+        this._ele.play();
+        runInAction(() => (this.mediaState = "playing"));
+        if (endTime !== this.duration) {
+          this._play = setTimeout(
+            () => this.Pause(),
+            (endTime - seekTimeInSeconds) * 1000
+          ); // use setTimeout to play a specific duration
         }
+      } else {
+        this.Pause();
+      }
     }
-
-    // starts recording
-    recordAudioAnnotation = async () => {
-        this._stream = await navigator.mediaDevices.getUserMedia({ audio: true });
-        this._recorder = new MediaRecorder(this._stream);
-        this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(new Date());
-        DocUtils.ActiveRecordings.push(this);
-        this._recorder.ondataavailable = async (e: any) => {
-            const [{ result }] = await Networking.UploadFilesToServer(e.data);
-            if (!(result instanceof Error)) {
-                this.props.Document[this.props.fieldKey] = new AudioField(Utils.prepend(result.accessPaths.agnostic.client));
-            }
-        };
-        this._recordStart = new Date().getTime();
-        runInAction(() => this.mediaState = "recording");
-        setTimeout(this.updateRecordTime, 0);
-        this._recorder.start();
-        setTimeout(() => this._recorder && this.stopRecording(), 60 * 60 * 1000); // stop after an hour
-    }
-
-    // context menu
-    specificContextMenu = (e: React.MouseEvent): void => {
-        const funcs: ContextMenuProps[] = [];
-        funcs.push({ description: (this.layoutDoc.hideAnchors ? "Don't hide" : "Hide") + " anchors", event: () => this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors, icon: "expand-arrows-alt" });
-        funcs.push({ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") + " play when link is selected", event: () => this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks, icon: "expand-arrows-alt" });
-        funcs.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors, icon: "expand-arrows-alt" });
-        ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
+  };
+
+  // update the recording time
+  updateRecordTime = () => {
+    if (this.mediaState === "recording") {
+      setTimeout(this.updateRecordTime, 30);
+      if (this._paused) {
+        this._pausedTime += (new Date().getTime() - this._recordStart) / 1000;
+      } else {
+        this.layoutDoc._currentTimecode =
+          (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
+      }
     }
-
-    // stops the recording
-    stopRecording = action(() => {
-        this._recorder.stop();
-        this._recorder = undefined;
-        this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
-        this.mediaState = "paused";
-        this._stream?.getAudioTracks()[0].stop();
-        const ind = DocUtils.ActiveRecordings.indexOf(this);
-        ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
+  };
+
+  // starts recording
+  recordAudioAnnotation = async () => {
+    this._stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+    this._recorder = new MediaRecorder(this._stream);
+    this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(
+      new Date()
+    );
+    DocUtils.ActiveRecordings.push(this);
+    this._recorder.ondataavailable = async (e: any) => {
+      const [{ result }] = await Networking.UploadFilesToServer(e.data);
+      if (!(result instanceof Error)) {
+        this.props.Document[this.props.fieldKey] = new AudioField(
+          Utils.prepend(result.accessPaths.agnostic.client)
+        );
+      }
+    };
+    this._recordStart = new Date().getTime();
+    runInAction(() => (this.mediaState = "recording"));
+    setTimeout(this.updateRecordTime, 0);
+    this._recorder.start();
+    setTimeout(() => this._recorder && this.stopRecording(), 60 * 60 * 1000); // stop after an hour
+  };
+
+  // context menu
+  specificContextMenu = (e: React.MouseEvent): void => {
+    const funcs: ContextMenuProps[] = [];
+    funcs.push({
+      description:
+        (this.layoutDoc.hideAnchors ? "Don't hide" : "Hide") + " anchors",
+      event: () => (this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors),
+      icon: "expand-arrows-alt",
     });
-
-    // button for starting and stopping the recording
-    recordClick = (e: React.MouseEvent) => {
-        if (e.button === 0 && !e.ctrlKey) {
-            this._recorder ? this.stopRecording() : this.recordAudioAnnotation();
-            e.stopPropagation();
-        }
-    }
-
-    // for play button
-    Play = (e?: any) => {
-        this.playFrom(this._ele!.paused ? this._ele!.currentTime : -1);
-        e?.stopPropagation?.();
-    }
-
-    // creates a text document for dictation
-    onFile = (e: any) => {
-        const newDoc = CurrentUserUtils.GetNewTextDoc("", NumCast(this.props.Document.x), NumCast(this.props.Document.y) + NumCast(this.props.Document._height) + 10,
-            NumCast(this.props.Document._width), 2 * NumCast(this.props.Document._height));
-        Doc.GetProto(newDoc).recordingSource = this.dataDoc;
-        Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`);
-        Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction("self.recordingSource.mediaState");
-        this.props.addDocument?.(newDoc);
-        e.stopPropagation();
-    }
-
-    // ref for updating time
-    setRef = (e: HTMLAudioElement | null) => {
-        e?.addEventListener("timeupdate", this.timecodeChanged);
-        e?.addEventListener("ended", this.Pause);
-        this._ele = e;
+    funcs.push({
+      description:
+        (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") +
+        " play when link is selected",
+      event: () =>
+        (this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc
+          .dontAutoPlayFollowedLinks),
+      icon: "expand-arrows-alt",
+    });
+    funcs.push({
+      description:
+        (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") +
+        " anchors onClick",
+      event: () =>
+        (this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors),
+      icon: "expand-arrows-alt",
+    });
+    ContextMenu.Instance?.addItem({
+      description: "Options...",
+      subitems: funcs,
+      icon: "asterisk",
+    });
+  };
+
+  // stops the recording
+  stopRecording = action(() => {
+    this._recorder.stop();
+    this._recorder = undefined;
+    this.dataDoc[this.fieldKey + "-duration"] =
+      (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
+    this.mediaState = "paused";
+    this._trimBounds.end = this.duration;
+    this.dataDoc.clipStart = 0;
+    this.dataDoc.clipEnd = this.duration;
+    this._stream?.getAudioTracks()[0].stop();
+    const ind = DocUtils.ActiveRecordings.indexOf(this);
+    ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
+  });
+
+  // button for starting and stopping the recording
+  recordClick = (e: React.MouseEvent) => {
+    if (e.button === 0 && !e.ctrlKey) {
+      this._recorder ? this.stopRecording() : this.recordAudioAnnotation();
+      e.stopPropagation();
     }
-
-    // returns the path of the audio file
-    @computed get path() {
-        const field = Cast(this.props.Document[this.props.fieldKey], AudioField);
-        const path = (field instanceof AudioField) ? field.url.href : "";
-        return path === nullAudio ? "" : path;
+  };
+
+  // for play button
+  Play = (e?: any) => {
+    if (this._trimming) {
+      let end = this._trimBounds.end;
+      let start =
+        this._ele!.currentTime > end ||
+        this._ele!.currentTime < this._trimBounds.start
+          ? -1
+          : this._trimBounds.start;
+      this.playFrom(this._ele!.paused ? this._ele!.currentTime : start, end);
+    } else {
+      this.playFrom(this._ele!.paused ? this._ele!.currentTime : -1);
     }
-
-    // returns the html audio element
-    @computed get audio() {
-        return <audio ref={this.setRef} className={`audiobox-control${this.isContentActive() ? "-interactive" : ""}`}>
-            <source src={this.path} type="audio/mpeg" />
-            Not supported.
-        </audio>;
+    e?.stopPropagation?.();
+  };
+
+  // creates a text document for dictation
+  onFile = (e: any) => {
+    const newDoc = CurrentUserUtils.GetNewTextDoc(
+      "",
+      NumCast(this.props.Document.x),
+      NumCast(this.props.Document.y) +
+        NumCast(this.props.Document._height) +
+        10,
+      NumCast(this.props.Document._width),
+      2 * NumCast(this.props.Document._height)
+    );
+    Doc.GetProto(newDoc).recordingSource = this.dataDoc;
+    Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(
+      `self.recordingSource["${this.props.fieldKey}-recordingStart"]`
+    );
+    Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction(
+      "self.recordingSource.mediaState"
+    );
+    this.props.addDocument?.(newDoc);
+    e.stopPropagation();
+  };
+
+  // ref for updating time
+  setRef = (e: HTMLAudioElement | null) => {
+    e?.addEventListener("timeupdate", this.timecodeChanged);
+    e?.addEventListener("ended", this.Pause);
+    this._ele = e;
+  };
+
+  // returns the path of the audio file
+  @computed get path() {
+    const field = Cast(this.props.Document[this.props.fieldKey], AudioField);
+    const path = field instanceof AudioField ? field.url.href : "";
+    return path === nullAudio ? "" : path;
+  }
+
+  // returns the html audio element
+  @computed get audio() {
+    return (
+      <audio
+        ref={this.setRef}
+        className={`audiobox-control${
+          this.isContentActive() ? "-interactive" : ""
+        }`}
+      >
+        <source src={this.path} type="audio/mpeg" />
+        Not supported.
+      </audio>
+    );
+  }
+
+  // pause the time during recording phase
+  @action
+  recordPause = (e: React.MouseEvent) => {
+    this._pauseStart = new Date().getTime();
+    this._paused = true;
+    this._recorder.pause();
+    e.stopPropagation();
+  };
+
+  // continue the recording
+  @action
+  recordPlay = (e: React.MouseEvent) => {
+    this._pauseEnd = new Date().getTime();
+    this._paused = false;
+    this._recorder.resume();
+    e.stopPropagation();
+  };
+
+  playing = () => this.mediaState === "playing";
+  playLink = (link: Doc) => {
+    const stack = this._stackedTimeline.current;
+    if (link.annotationOn === this.rootDoc) {
+      if (!this.layoutDoc.dontAutoPlayFollowedLinks)
+        this.playFrom(stack?.anchorStart(link) || 0, stack?.anchorEnd(link));
+      else
+        this._ele!.currentTime = this.layoutDoc._currentTimecode =
+          stack?.anchorStart(link) || 0;
+    } else {
+      this.links
+        .filter((l) => l.anchor1 === link || l.anchor2 === link)
+        .forEach((l) => {
+          const { la1, la2 } = this.getLinkData(l);
+          const startTime = stack?.anchorStart(la1) || stack?.anchorStart(la2);
+          const endTime = stack?.anchorEnd(la1) || stack?.anchorEnd(la2);
+          if (startTime !== undefined) {
+            if (!this.layoutDoc.dontAutoPlayFollowedLinks)
+              endTime
+                ? this.playFrom(startTime, endTime)
+                : this.playFrom(startTime);
+            else
+              this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime;
+          }
+        });
     }
+  };
 
-    // pause the time during recording phase
-    @action
-    recordPause = (e: React.MouseEvent) => {
-        this._pauseStart = new Date().getTime();
-        this._paused = true;
-        this._recorder.pause();
-        e.stopPropagation();
+  // shows trim controls
+  @action
+  startTrim = () => {
+    if (this.mediaState === "playing") {
+      this.Pause();
     }
-
-    // continue the recording
-    @action
-    recordPlay = (e: React.MouseEvent) => {
-        this._pauseEnd = new Date().getTime();
-        this._paused = false;
-        this._recorder.resume();
-        e.stopPropagation();
+    this._trimming = true;
+  };
+
+  // hides trim controls and displays new clip
+  @action
+  finishTrim = () => {
+    if (this.mediaState === "playing") {
+      this.Pause();
     }
-
-    playing = () => this.mediaState === "playing";
-    playLink = (link: Doc) => {
-        const stack = this._stackedTimeline.current;
-        if (link.annotationOn === this.rootDoc) {
-            if (!this.layoutDoc.dontAutoPlayFollowedLinks) this.playFrom(stack?.anchorStart(link) || 0, stack?.anchorEnd(link));
-            else this._ele!.currentTime = this.layoutDoc._currentTimecode = (stack?.anchorStart(link) || 0);
+    this.dataDoc.clipStart = this._trimBounds.start;
+    this.dataDoc.clipEnd = this._trimBounds.end;
+    this._trimming = false;
+  };
+
+  isActiveChild = () => this._isAnyChildContentActive;
+  timelineWhenChildContentsActiveChanged = (isActive: boolean) =>
+    this.props.whenChildContentsActiveChanged(
+      runInAction(() => (this._isAnyChildContentActive = isActive))
+    );
+  timelineScreenToLocal = () =>
+    this.props
+      .ScreenToLocalTransform()
+      .translate(
+        -AudioBox.playheadWidth,
+        (-(100 - this.heightPercent) / 200) * this.props.PanelHeight()
+      );
+  setAnchorTime = (time: number) =>
+    (this._ele!.currentTime = this.layoutDoc._currentTimecode = time);
+  timelineHeight = () =>
+    (((this.props.PanelHeight() * this.heightPercent) / 100) *
+      this.heightPercent) /
+    100; // panelHeight * heightPercent is player height.  * heightPercent is timeline height (as per css inline)
+  timelineWidth = () => this.props.PanelWidth() - AudioBox.playheadWidth;
+  @computed get renderTimeline() {
+    return (
+      <CollectionStackedTimeline
+        ref={this._stackedTimeline}
+        {...this.props}
+        fieldKey={this.annotationKey}
+        dictationKey={this.fieldKey + "-dictation"}
+        mediaPath={this.path}
+        renderDepth={this.props.renderDepth + 1}
+        startTag={"_timecodeToShow" /* audioStart */}
+        endTag={"_timecodeToHide" /* audioEnd */}
+        focus={DocUtils.DefaultFocus}
+        bringToFront={emptyFunction}
+        CollectionView={undefined}
+        duration={this.duration}
+        playFrom={this.playFrom}
+        setTime={this.setAnchorTime}
+        playing={this.playing}
+        whenChildContentsActiveChanged={
+          this.timelineWhenChildContentsActiveChanged
         }
-        else {
-            this.links.filter(l => l.anchor1 === link || l.anchor2 === link).forEach(l => {
-                const { la1, la2 } = this.getLinkData(l);
-                const startTime = stack?.anchorStart(la1) || stack?.anchorStart(la2);
-                const endTime = stack?.anchorEnd(la1) || stack?.anchorEnd(la2);
-                if (startTime !== undefined) {
-                    if (!this.layoutDoc.dontAutoPlayFollowedLinks) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime);
-                    else this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime;
-                }
-            });
+        removeDocument={this.removeDocument}
+        ScreenToLocalTransform={this.timelineScreenToLocal}
+        Play={this.Play}
+        Pause={this.Pause}
+        isContentActive={this.isContentActive}
+        playLink={this.playLink}
+        PanelWidth={this.timelineWidth}
+        PanelHeight={this.timelineHeight}
+        trimming={this._trimming}
+        trimBounds={this._trimBounds}
+      />
+    );
+  }
+
+  render() {
+    const interactive =
+      SnappingManager.GetIsDragging() || this.isContentActive()
+        ? "-interactive"
+        : "";
+    return (
+      <div
+        className="audiobox-container"
+        onContextMenu={this.specificContextMenu}
+        onClick={
+          !this.path && !this._recorder ? this.recordAudioAnnotation : undefined
         }
-    }
-
-    isActiveChild = () => this._isAnyChildContentActive;
-    timelineWhenChildContentsActiveChanged = (isActive: boolean) => this.props.whenChildContentsActiveChanged(runInAction(() => this._isAnyChildContentActive = isActive));
-    timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-AudioBox.playheadWidth, -(100 - this.heightPercent) / 200 * this.props.PanelHeight());
-    setAnchorTime = (time: number) => this._ele!.currentTime = this.layoutDoc._currentTimecode = time;
-    timelineHeight = () => this.props.PanelHeight() * this.heightPercent / 100 * this.heightPercent / 100; // panelHeight * heightPercent is player height.  * heightPercent is timeline height (as per css inline)
-    timelineWidth = () => this.props.PanelWidth() - AudioBox.playheadWidth;
-    @computed get renderTimeline() {
-        return <CollectionStackedTimeline ref={this._stackedTimeline} {...this.props}
-            fieldKey={this.annotationKey}
-            dictationKey={this.fieldKey + "-dictation"}
-            mediaPath={this.path}
-            renderDepth={this.props.renderDepth + 1}
-            startTag={"_timecodeToShow" /* audioStart */}
-            endTag={"_timecodeToHide" /* audioEnd */}
-            focus={DocUtils.DefaultFocus}
-            bringToFront={emptyFunction}
-            CollectionView={undefined}
-            duration={this.duration}
-            playFrom={this.playFrom}
-            setTime={this.setAnchorTime}
-            playing={this.playing}
-            whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged}
-            removeDocument={this.removeDocument}
-            ScreenToLocalTransform={this.timelineScreenToLocal}
-            Play={this.Play}
-            Pause={this.Pause}
-            isContentActive={this.isContentActive}
-            playLink={this.playLink}
-            PanelWidth={this.timelineWidth}
-            PanelHeight={this.timelineHeight}
-        />;
-    }
-
-    render() {
-        const interactive = SnappingManager.GetIsDragging() || this.isContentActive() ? "-interactive" : "";
-        return <div className="audiobox-container"
-            onContextMenu={this.specificContextMenu}
-            onClick={!this.path && !this._recorder ? this.recordAudioAnnotation : undefined}
-            style={{ pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined }}>
-            {!this.path ?
-                <div className="audiobox-buttons">
-                    <div className="audiobox-dictation" onClick={this.onFile}>
-                        <FontAwesomeIcon style={{ width: "30px", background: !this.layoutDoc.dontAutoPlayFollowedLinks ? "yellow" : "rgba(0,0,0,0)" }} icon="file-alt" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
-                    </div>
-                    {this.mediaState === "recording" || this.mediaState === "paused" ?
-                        <div className="recording" onClick={e => e.stopPropagation()}>
-                            <div className="buttons" onClick={this.recordClick}>
-                                <FontAwesomeIcon icon={"stop"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
-                            </div>
-                            <div className="buttons" onClick={this._paused ? this.recordPlay : this.recordPause}>
-                                <FontAwesomeIcon icon={this._paused ? "play" : "pause"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
-                            </div>
-                            <div className="time">{formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))}</div>
-                        </div>
-                        :
-                        <button className={`audiobox-record${interactive}`} style={{ backgroundColor: "black" }}>
-                            RECORD
-                        </button>}
-                </div> :
-                <div className="audiobox-controls" style={{ pointerEvents: this._isAnyChildContentActive || this.isContentActive() ? "all" : "none" }} >
-                    <div className="audiobox-dictation" />
-                    <div className="audiobox-player" style={{ height: `${AudioBox.heightPercent}%` }} >
-                        <div className="audiobox-playhead" style={{ width: AudioBox.playheadWidth }} title={this.mediaState === "paused" ? "play" : "pause"} onClick={this.Play}> <FontAwesomeIcon style={{ width: "100%", position: "absolute", left: "0px", top: "5px", borderWidth: "thin", borderColor: "white" }} icon={this.mediaState === "paused" ? "play" : "pause"} size={"1x"} /></div>
-                        <div className="audiobox-timeline" style={{ top: 0, height: `100%`, left: AudioBox.playheadWidth, width: `calc(100% - ${AudioBox.playheadWidth}px)`, background: "white" }}>
-                            {this.renderTimeline}
-                        </div>
-                        {this.audio}
-                        <div className="audioBox-current-time">
-                            {formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))}
-                        </div>
-                        <div className="audioBox-total-time">
-                            {formatTime(Math.round(this.duration))}
-                        </div>
-                    </div>
+        style={{
+          pointerEvents:
+            this.props.layerProvider?.(this.layoutDoc) === false
+              ? "none"
+              : undefined,
+        }}
+      >
+        {!this.path ? (
+          <div className="audiobox-buttons">
+            <div className="audiobox-dictation" onClick={this.onFile}>
+              <FontAwesomeIcon
+                style={{
+                  width: "30px",
+                  background: !this.layoutDoc.dontAutoPlayFollowedLinks
+                    ? "yellow"
+                    : "rgba(0,0,0,0)",
+                }}
+                icon="file-alt"
+                size={this.props.PanelHeight() < 36 ? "1x" : "2x"}
+              />
+            </div>
+            {this.mediaState === "recording" || this.mediaState === "paused" ? (
+              <div className="recording" onClick={(e) => e.stopPropagation()}>
+                <div className="buttons" onClick={this.recordClick}>
+                  <FontAwesomeIcon
+                    icon={"stop"}
+                    size={this.props.PanelHeight() < 36 ? "1x" : "2x"}
+                  />
                 </div>
-            }
-        </div>;
-    }
-}
\ No newline at end of file
+                <div
+                  className="buttons"
+                  onClick={this._paused ? this.recordPlay : this.recordPause}
+                >
+                  <FontAwesomeIcon
+                    icon={this._paused ? "play" : "pause"}
+                    size={this.props.PanelHeight() < 36 ? "1x" : "2x"}
+                  />
+                </div>
+                <div className="time">
+                  {formatTime(
+                    Math.round(NumCast(this.layoutDoc._currentTimecode))
+                  )}
+                </div>
+              </div>
+            ) : (
+              <button
+                className={`audiobox-record${interactive}`}
+                style={{ backgroundColor: "black" }}
+              >
+                RECORD
+              </button>
+            )}
+          </div>
+        ) : (
+          <div
+            className="audiobox-controls"
+            style={{
+              pointerEvents:
+                this._isAnyChildContentActive || this.isContentActive()
+                  ? "all"
+                  : "none",
+            }}
+          >
+            <div className="audiobox-dictation" />
+            <div
+              className="audiobox-player"
+              style={{ height: `${AudioBox.heightPercent}%` }}
+            >
+              <div
+                className="audiobox-playhead"
+                style={{
+                  width: AudioBox.playheadWidth,
+                  height: AudioBox.playheadWidth,
+                }}
+                title={this.mediaState === "paused" ? "play" : "pause"}
+                onClick={this.Play}
+              >
+                {" "}
+                <FontAwesomeIcon
+                  icon={this.mediaState === "paused" ? "play" : "pause"}
+                  size={"1x"}
+                />
+              </div>
+              <div
+                className="audiobox-playhead"
+                style={{
+                  width: AudioBox.playheadWidth,
+                  height: AudioBox.playheadWidth,
+                }}
+                title={this._trimming ? "finish" : "trim"}
+                onClick={this._trimming ? this.finishTrim : this.startTrim}
+              >
+                <FontAwesomeIcon
+                  icon={this._trimming ? "check" : "cut"}
+                  size={"1x"}
+                />
+              </div>
+              <div
+                className="audiobox-timeline"
+                style={{
+                  top: 0,
+                  height: `100%`,
+                  left: AudioBox.playheadWidth,
+                  width: `calc(100% - ${AudioBox.playheadWidth}px)`,
+                  background: "white",
+                }}
+              >
+                {this.renderTimeline}
+              </div>
+              {this.audio}
+              <div className="audioBox-current-time">
+                {formatTime(
+                  Math.round(NumCast(this.layoutDoc._currentTimecode))
+                )}
+              </div>
+              <div className="audioBox-total-time">
+                {formatTime(Math.round(this.duration))}
+              </div>
+            </div>
+          </div>
+        )}
+      </div>
+    );
+  }
+}
-- 
cgit v1.2.3-70-g09d2


From 8beb8fa42ba5f84bb13b5877560fc92ad3613e88 Mon Sep 17 00:00:00 2001
From: mehekj <mehek.jethani@gmail.com>
Date: Wed, 25 Aug 2021 21:34:40 -0400
Subject: basic audio trim complete

---
 src/client/documents/Documents.ts                  |    2 +-
 src/client/util/CurrentUserUtils.ts                |    2 +-
 src/client/views/AudioWaveform.tsx                 |  214 ++-
 src/client/views/DocumentDecorations.tsx           |   11 +-
 .../collections/CollectionStackedTimeline.scss     |   22 +-
 .../collections/CollectionStackedTimeline.tsx      | 1550 ++++++++++----------
 src/client/views/nodes/AudioBox.scss               |   54 +-
 src/client/views/nodes/AudioBox.tsx                | 1289 ++++++++--------
 src/client/views/nodes/DocumentView.tsx            |    1 +
 src/client/views/nodes/LabelBox.tsx                |   21 +-
 10 files changed, 1615 insertions(+), 1551 deletions(-)

(limited to 'src/client/views/nodes/AudioBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 72b52616a..fce5e76f5 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -356,7 +356,7 @@ export namespace Docs {
             }],
             [DocumentType.AUDIO, {
                 layout: { view: AudioBox, dataField: defaultDataKey },
-                options: { _height: 35, backgroundColor: "lightGray", links: ComputedField.MakeFunction("links(self)") as any }
+                options: { _height: 100, backgroundColor: "lightGray", links: ComputedField.MakeFunction("links(self)") as any }
             }],
             [DocumentType.PDF, {
                 layout: { view: PDFBox, dataField: defaultDataKey },
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 62fab1b23..bf768a401 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -443,7 +443,7 @@ export class CurrentUserUtils {
             (doc.emptyWall as Doc).videoWall = true;
         }
         if (doc.emptyAudio === undefined) {
-            doc.emptyAudio = Docs.Create.AudioDocument(nullAudio, { _width: 200, title: "audio recording", system: true, cloneFieldFilter: new List<string>(["system"]) });
+            doc.emptyAudio = Docs.Create.AudioDocument(nullAudio, { _width: 200, _height: 100, title: "audio recording", system: true, cloneFieldFilter: new List<string>(["system"]) });
             ((doc.emptyAudio as Doc).proto as Doc)["dragFactory-count"] = 0;
         }
         if (doc.emptyNote === undefined) {
diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx
index 313d0062e..f519f9ef0 100644
--- a/src/client/views/AudioWaveform.tsx
+++ b/src/client/views/AudioWaveform.tsx
@@ -9,132 +9,118 @@ import { listSpec } from "../../fields/Schema";
 import { Cast, NumCast } from "../../fields/Types";
 import { numberRange } from "../../Utils";
 import "./AudioWaveform.scss";
+import { Colors } from "./global/globalEnums";
 
 export interface AudioWaveformProps {
-  duration: number;
-  mediaPath: string;
-  dataDoc: Doc;
-  trimming: boolean;
-  PanelHeight: () => number;
+    duration: number;
+    mediaPath: string;
+    layoutDoc: Doc;
+    trimming: boolean;
+    PanelHeight: () => number;
 }
 
 @observer
 export class AudioWaveform extends React.Component<AudioWaveformProps> {
-  public static NUMBER_OF_BUCKETS = 100;
-  @computed get _waveHeight() {
-    return Math.max(50, this.props.PanelHeight());
-  }
-  componentDidMount() {
-    const audioBuckets = Cast(
-      this.props.dataDoc.audioBuckets,
-      listSpec("number"),
-      []
-    );
-    if (!audioBuckets.length) {
-      this.props.dataDoc.audioBuckets = new List<number>([0, 0]); /// "lock" to prevent other views from computing the same data
-      setTimeout(this.createWaveformBuckets);
+    public static NUMBER_OF_BUCKETS = 100;
+    @computed get _waveHeight() {
+        return Math.max(50, this.props.PanelHeight());
+    }
+    componentDidMount() {
+        const audioBuckets = Cast(
+            this.props.layoutDoc.audioBuckets,
+            listSpec("number"),
+            []
+        );
+        if (!audioBuckets.length) {
+            this.props.layoutDoc.audioBuckets = new List<number>([0, 0]); /// "lock" to prevent other views from computing the same data
+            setTimeout(this.createWaveformBuckets);
+        }
     }
 
-    // const trimBuckets = Cast(
-    //     this.props.dataDoc.trimBuckets,
-    //     listSpec("number"),
-    //     []
-    //   );
-    //   if (!trimBuckets.length) {
-    //     this.props.dataDoc.trimBuckets = new List<number>([0, 0]); /// "lock" to prevent other views from computing the same data
-    //     this.createTrimBuckets();
-    //   }
-  }
-  // decodes the audio file into peaks for generating the waveform
-  createWaveformBuckets = async () => {
-    axios({ url: this.props.mediaPath, responseType: "arraybuffer" }).then(
-      (response) => {
-        const context = new window.AudioContext();
-        context.decodeAudioData(
-          response.data,
-          action((buffer) => {
-            const decodedAudioData = buffer.getChannelData(0);
+    // decodes the audio file into peaks for generating the waveform
+    createWaveformBuckets = async () => {
+        axios({ url: this.props.mediaPath, responseType: "arraybuffer" }).then(
+            (response) => {
+                const context = new window.AudioContext();
+                context.decodeAudioData(
+                    response.data,
+                    action((buffer) => {
+                        const decodedAudioData = buffer.getChannelData(0);
 
-            const bucketDataSize = Math.floor(
-              decodedAudioData.length / AudioWaveform.NUMBER_OF_BUCKETS
-            );
-            const brange = Array.from(Array(bucketDataSize));
-            this.props.dataDoc.audioBuckets = new List<number>(
-              numberRange(AudioWaveform.NUMBER_OF_BUCKETS).map(
-                (i: number) =>
-                  brange.reduce(
-                    (p, x, j) =>
-                      Math.abs(
-                        Math.max(p, decodedAudioData[i * bucketDataSize + j])
-                      ),
-                    0
-                  ) / 2
-              )
-            );
-          })
+                        const bucketDataSize = Math.floor(
+                            decodedAudioData.length / AudioWaveform.NUMBER_OF_BUCKETS
+                        );
+                        const brange = Array.from(Array(bucketDataSize));
+                        this.props.layoutDoc.audioBuckets = new List<number>(
+                            numberRange(AudioWaveform.NUMBER_OF_BUCKETS).map(
+                                (i: number) =>
+                                    brange.reduce(
+                                        (p, x, j) =>
+                                            Math.abs(
+                                                Math.max(p, decodedAudioData[i * bucketDataSize + j])
+                                            ),
+                                        0
+                                    ) / 2
+                            )
+                        );
+                    })
+                );
+            }
         );
-      }
-    );
-  };
+    };
 
-  @action
-  createTrimBuckets = () => {
-    const audioBuckets = Cast(
-      this.props.dataDoc.audioBuckets,
-      listSpec("number"),
-      []
-    );
-
-    const start = Math.floor(
-      (NumCast(this.props.dataDoc.clipStart) / this.props.duration) * 100
-    );
-    const end = Math.floor(
-      (NumCast(this.props.dataDoc.clipEnd) / this.props.duration) * 100
-    );
-    return audioBuckets.slice(start, end);
-  };
+    @action
+    createTrimBuckets = () => {
+        const audioBuckets = Cast(
+            this.props.layoutDoc.audioBuckets,
+            listSpec("number"),
+            []
+        );
 
-  render() {
-    const audioBuckets = Cast(
-      this.props.dataDoc.audioBuckets,
-      listSpec("number"),
-      []
-    );
+        const start = Math.floor(
+            (NumCast(this.props.layoutDoc.clipStart) / this.props.duration) * 100
+        );
+        const end = Math.floor(
+            (NumCast(this.props.layoutDoc.clipEnd) / this.props.duration) * 100
+        );
+        return audioBuckets.slice(start, end);
+    };
 
-    const trimBuckets = Cast(
-      this.props.dataDoc.trimBuckets,
-      listSpec("number"),
-      []
-    );
+    render() {
+        const audioBuckets = Cast(
+            this.props.layoutDoc.audioBuckets,
+            listSpec("number"),
+            []
+        );
 
-    return (
-      <div className="audioWaveform">
-        {this.props.trimming ? (
-          <Waveform
-            color={"darkblue"}
-            height={this._waveHeight}
-            barWidth={0.1}
-            pos={this.props.duration}
-            duration={this.props.duration}
-            peaks={
-              audioBuckets.length === AudioWaveform.NUMBER_OF_BUCKETS
-                ? audioBuckets
-                : undefined
-            }
-            progressColor={"blue"}
-          />
-        ) : (
-          <Waveform
-            color={"darkblue"}
-            height={this._waveHeight}
-            barWidth={0.1}
-            pos={this.props.duration}
-            duration={this.props.duration}
-            peaks={this.createTrimBuckets()}
-            progressColor={"blue"}
-          />
-        )}
-      </div>
-    );
-  }
+        return (
+            <div className="audioWaveform">
+                {this.props.trimming || !this.props.layoutDoc.clipEnd ? (
+                    <Waveform
+                        color={Colors.MEDIUM_BLUE}
+                        height={this._waveHeight}
+                        barWidth={0.1}
+                        pos={this.props.duration}
+                        duration={this.props.duration}
+                        peaks={
+                            audioBuckets.length === AudioWaveform.NUMBER_OF_BUCKETS
+                                ? audioBuckets
+                                : undefined
+                        }
+                        progressColor={Colors.MEDIUM_BLUE}
+                    />
+                ) : (
+                    <Waveform
+                        color={Colors.MEDIUM_BLUE}
+                        height={this._waveHeight}
+                        barWidth={0.1}
+                        pos={this.props.duration}
+                        duration={this.props.duration}
+                        peaks={this.createTrimBuckets()}
+                        progressColor={Colors.MEDIUM_BLUE}
+                    />
+                )}
+            </div>
+        );
+    }
 }
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index d24ab974c..0424b4e63 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -389,10 +389,10 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b
         this._inkDragDocs.map(oldbds => ({ oldbds, inkPts: Cast(oldbds.doc.data, InkField)?.inkData || [] }))
             .forEach(({ oldbds: { doc, x, y, width, height }, inkPts }) => {
                 Doc.GetProto(doc).data = new InkField(inkPts.map(ipt =>  // (new x — oldx) + newWidth * (oldxpoint /oldWidth)
-                    ({
-                        X: (NumCast(doc.x) - x) + NumCast(doc.width) * ipt.X / width,
-                        Y: (NumCast(doc.y) - y) + NumCast(doc.height) * ipt.Y / height
-                    })));
+                ({
+                    X: (NumCast(doc.x) - x) + NumCast(doc.width) * ipt.X / width,
+                    Y: (NumCast(doc.y) - y) + NumCast(doc.height) * ipt.Y / height
+                })));
                 Doc.SetNativeWidth(doc, undefined);
                 Doc.SetNativeHeight(doc, undefined);
             });
@@ -402,6 +402,9 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b
     get selectionTitle(): string {
         if (SelectionManager.Views().length === 1) {
             const selected = SelectionManager.Views()[0];
+            if (selected.ComponentView?.getTitle?.()) {
+                return selected.ComponentView.getTitle();
+            }
             if (this._titleControlString.startsWith("=")) {
                 return ScriptField.MakeFunction(this._titleControlString.substring(1), { doc: Doc.name })!.script.run({ self: selected.rootDoc, this: selected.layoutDoc }, console.log).result?.toString() || "";
             }
diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss
index 92476e298..59c21210a 100644
--- a/src/client/views/collections/CollectionStackedTimeline.scss
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -1,9 +1,9 @@
+@import "../global/globalCssVariables.scss";
+
 .collectionStackedTimeline {
   position: absolute;
   width: 100%;
   height: 100%;
-  border: gray solid 1px;
-  border-radius: 3px;
   z-index: 1000;
   overflow: hidden;
   top: 0px;
@@ -11,19 +11,21 @@
   .collectionStackedTimeline-trim-shade {
     position: absolute;
     height: 100%;
-    background-color: black;
+    background-color: $dark-gray;
     opacity: 0.3;
   }
 
   .collectionStackedTimeline-trim-controls {
     height: 100%;
     position: absolute;
-    border: 2px solid yellow;
+    box-sizing: border-box;
+    border: 2px solid $medium-blue;
     display: flex;
     justify-content: space-between;
+    max-width: 100%;
 
     .collectionStackedTimeline-trim-handle {
-      background-color: yellow;
+      background-color: $medium-blue;
       height: 100%;
       width: 5px;
       cursor: ew-resize;
@@ -35,19 +37,19 @@
     width: 10px;
     top: 2.5%;
     height: 95%;
-    background: lightblue;
-    border-radius: 5px;
+    background: $light-blue;
+    border-radius: 3px;
     opacity: 0.3;
     z-index: 500;
     border-style: solid;
-    border-color: darkblue;
+    border-color: $medium-blue;
     border-width: 1px;
   }
 
   .collectionStackedTimeline-current {
     width: 1px;
     height: 100%;
-    background-color: red;
+    background-color: $pink;
     position: absolute;
     top: 0px;
     pointer-events: none;
@@ -64,7 +66,7 @@
 
     .collectionStackedTimeline-left-resizer,
     .collectionStackedTimeline-resizer {
-      background: dimgrey;
+      background: $medium-gray;
       position: absolute;
       top: 0;
       height: 100%;
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 230238ba8..51e05e278 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -1,11 +1,11 @@
 import React = require("react");
 import {
-  action,
-  computed,
-  IReactionDisposer,
-  observable,
-  reaction,
-  runInAction,
+    action,
+    computed,
+    IReactionDisposer,
+    observable,
+    reaction,
+    runInAction,
 } from "mobx";
 import { observer } from "mobx-react";
 import { computedFn } from "mobx-utils";
@@ -16,14 +16,14 @@ import { listSpec, makeInterface } from "../../../fields/Schema";
 import { ComputedField, ScriptField } from "../../../fields/ScriptField";
 import { Cast, NumCast } from "../../../fields/Types";
 import {
-  emptyFunction,
-  formatTime,
-  OmitKeys,
-  returnFalse,
-  returnOne,
-  setupMoveUpEvents,
-  StopEvent,
-  returnTrue,
+    emptyFunction,
+    formatTime,
+    OmitKeys,
+    returnFalse,
+    returnOne,
+    setupMoveUpEvents,
+    StopEvent,
+    returnTrue,
 } from "../../../Utils";
 import { Docs } from "../../documents/Documents";
 import { LinkManager } from "../../util/LinkManager";
@@ -35,798 +35,820 @@ import { AudioWaveform } from "../AudioWaveform";
 import { CollectionSubView } from "../collections/CollectionSubView";
 import { LightboxView } from "../LightboxView";
 import {
-  DocAfterFocusFunc,
-  DocFocusFunc,
-  DocumentView,
-  DocumentViewProps,
+    DocAfterFocusFunc,
+    DocFocusFunc,
+    DocumentView,
+    DocumentViewProps,
 } from "../nodes/DocumentView";
 import { LabelBox } from "../nodes/LabelBox";
 import "./CollectionStackedTimeline.scss";
+import { Colors } from "../global/globalEnums";
 
 type PanZoomDocument = makeInterface<[]>;
 const PanZoomDocument = makeInterface();
 export type CollectionStackedTimelineProps = {
-  duration: number;
-  Play: () => void;
-  Pause: () => void;
-  playLink: (linkDoc: Doc) => void;
-  playFrom: (seekTimeInSeconds: number, endTime?: number) => void;
-  playing: () => boolean;
-  setTime: (time: number) => void;
-  startTag: string;
-  endTag: string;
-  mediaPath: string;
-  dictationKey: string;
-  trimming: boolean;
-  trimBounds: { start: number; end: number };
+    duration: number;
+    Play: () => void;
+    Pause: () => void;
+    playLink: (linkDoc: Doc) => void;
+    playFrom: (seekTimeInSeconds: number, endTime?: number) => void;
+    playing: () => boolean;
+    setTime: (time: number) => void;
+    startTag: string;
+    endTag: string;
+    mediaPath: string;
+    dictationKey: string;
+    trimming: boolean;
+    trimStart: number;
+    trimEnd: number;
+    trimDuration: number;
+    setStartTrim: (newStart: number) => void;
+    setEndTrim: (newEnd: number) => void;
 };
 
 @observer
 export class CollectionStackedTimeline extends CollectionSubView<
-  PanZoomDocument,
-  CollectionStackedTimelineProps
+    PanZoomDocument,
+    CollectionStackedTimelineProps
 >(PanZoomDocument) {
-  @observable static SelectingRegion:
-    | CollectionStackedTimeline
-    | undefined = undefined;
-  static RangeScript: ScriptField;
-  static LabelScript: ScriptField;
-  static RangePlayScript: ScriptField;
-  static LabelPlayScript: ScriptField;
-
-  private _timeline: HTMLDivElement | null = null;
-  private _markerStart: number = 0;
-  @observable _markerEnd: number = 0;
-
-  get minLength() {
-    const rect = this._timeline?.getBoundingClientRect();
-    if (rect) {
-      return 0.05 * this.duration;
+    @observable static SelectingRegion: CollectionStackedTimeline | undefined =
+        undefined;
+    static RangeScript: ScriptField;
+    static LabelScript: ScriptField;
+    static RangePlayScript: ScriptField;
+    static LabelPlayScript: ScriptField;
+
+    private _timeline: HTMLDivElement | null = null;
+    private _markerStart: number = 0;
+    @observable _markerEnd: number = 0;
+
+    get minLength() {
+        const rect = this._timeline?.getBoundingClientRect();
+        if (rect) {
+            return 0.05 * this.duration;
+        }
+        return 0;
     }
-    return 0;
-  }
-
-  get trimStart() {
-    return this.props.trimBounds.start;
-  }
-
-  get trimEnd() {
-    return this.props.trimBounds.end;
-  }
-
-  set trimStart(start: number) {
-    this.props.trimBounds.start = start;
-  }
-
-  set trimEnd(end: number) {
-    this.props.trimBounds.end = end;
-  }
-
-  get duration() {
-    return this.props.duration;
-  }
-  @computed get currentTime() {
-    return NumCast(this.layoutDoc._currentTimecode);
-  }
-  @computed get selectionContainer() {
-    return CollectionStackedTimeline.SelectingRegion !== this ? null : (
-      <div
-        className="collectionStackedTimeline-selector"
-        style={{
-          left: `${
-            (Math.min(NumCast(this._markerStart), NumCast(this._markerEnd)) /
-              this.duration) *
-            100
-          }%`,
-          width: `${
-            (Math.abs(this._markerStart - this._markerEnd) / this.duration) *
-            100
-          }%`,
-        }}
-      />
-    );
-  }
-
-  constructor(props: any) {
-    super(props);
-    // onClick play scripts
-    CollectionStackedTimeline.RangeScript =
-      CollectionStackedTimeline.RangeScript ||
-      ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, {
-        self: Doc.name,
-        scriptContext: "any",
-        clientX: "number",
-      })!;
-    CollectionStackedTimeline.RangePlayScript =
-      CollectionStackedTimeline.RangePlayScript ||
-      ScriptField.MakeFunction(`scriptContext.playOnClick(this, clientX)`, {
-        self: Doc.name,
-        scriptContext: "any",
-        clientX: "number",
-      })!;
-  }
-
-  componentDidMount() {
-    document.addEventListener("keydown", this.keyEvents, true);
-  }
-  componentWillUnmount() {
-    document.removeEventListener("keydown", this.keyEvents, true);
-    if (CollectionStackedTimeline.SelectingRegion === this)
-      runInAction(
-        () => (CollectionStackedTimeline.SelectingRegion = undefined)
-      );
-  }
-
-  anchorStart = (anchor: Doc) =>
-    NumCast(anchor._timecodeToShow, NumCast(anchor[this.props.startTag]));
-  anchorEnd = (anchor: Doc, val: any = null) => {
-    const endVal = NumCast(anchor[this.props.endTag], val);
-    return NumCast(
-      anchor._timecodeToHide,
-      endVal === undefined ? null : endVal
-    );
-  };
-  toTimeline = (screen_delta: number, width: number) =>
-    Math.max(
-      0,
-      Math.min(this.duration, (screen_delta / width) * this.duration)
-    );
-  rangeClickScript = () => CollectionStackedTimeline.RangeScript;
-  rangePlayScript = () => CollectionStackedTimeline.RangePlayScript;
-
-  // for creating key anchors with key events
-  @action
-  keyEvents = (e: KeyboardEvent) => {
-    if (
-      !(e.target instanceof HTMLInputElement) &&
-      this.props.isSelected(true)
-    ) {
-      switch (e.key) {
-        case " ":
-          if (!CollectionStackedTimeline.SelectingRegion) {
-            this._markerStart = this._markerEnd = this.currentTime;
-            CollectionStackedTimeline.SelectingRegion = this;
-          } else {
-            CollectionStackedTimeline.createAnchor(
-              this.rootDoc,
-              this.dataDoc,
-              this.props.fieldKey,
-              this.props.startTag,
-              this.props.endTag,
-              this.currentTime
-            );
-            CollectionStackedTimeline.SelectingRegion = undefined;
-          }
-      }
+
+    get trimStart() {
+        return this.props.trimStart;
     }
-  };
-
-  getLinkData(l: Doc) {
-    let la1 = l.anchor1 as Doc;
-    let la2 = l.anchor2 as Doc;
-    const linkTime = NumCast(
-      la2[this.props.startTag],
-      NumCast(la1[this.props.startTag])
-    );
-    if (Doc.AreProtosEqual(la1, this.dataDoc)) {
-      la1 = l.anchor2 as Doc;
-      la2 = l.anchor1 as Doc;
+
+    get trimEnd() {
+        return this.props.trimEnd;
     }
-    return { la1, la2, linkTime };
-  }
-
-  // starting the drag event for anchor resizing
-  @action
-  onPointerDownTimeline = (e: React.PointerEvent): void => {
-    const rect = this._timeline?.getBoundingClientRect();
-    const clientX = e.clientX;
-    if (rect && this.props.isContentActive()) {
-      const wasPlaying = this.props.playing();
-      if (wasPlaying) this.props.Pause();
-      const wasSelecting = CollectionStackedTimeline.SelectingRegion === this;
-      setupMoveUpEvents(
-        this,
-        e,
-        action((e) => {
-          if (
-            !wasSelecting &&
-            CollectionStackedTimeline.SelectingRegion !== this
-          ) {
-            this._markerStart = this._markerEnd = this.toTimeline(
-              clientX - rect.x,
-              rect.width
-            );
-            CollectionStackedTimeline.SelectingRegion = this;
-          }
-          this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
-          return false;
-        }),
-        action((e, movement, isClick) => {
-          this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
-          if (this._markerEnd < this._markerStart) {
-            const tmp = this._markerStart;
-            this._markerStart = this._markerEnd;
-            this._markerEnd = tmp;
-          }
-          if (
-            !isClick &&
-            CollectionStackedTimeline.SelectingRegion === this &&
-            Math.abs(movement[0]) > 15 &&
-            !this.props.trimming
-          ) {
-            CollectionStackedTimeline.createAnchor(
-              this.rootDoc,
-              this.dataDoc,
-              this.props.fieldKey,
-              this.props.startTag,
-              this.props.endTag,
-              this._markerStart,
-              this._markerEnd
-            );
-          }
-          (!isClick || !wasSelecting) &&
-            (CollectionStackedTimeline.SelectingRegion = undefined);
-        }),
-        (e, doubleTap) => {
-          this.props.select(false);
-          e.shiftKey &&
-            CollectionStackedTimeline.createAnchor(
-              this.rootDoc,
-              this.dataDoc,
-              this.props.fieldKey,
-              this.props.startTag,
-              this.props.endTag,
-              this.currentTime
-            );
-          !wasPlaying && doubleTap && this.props.Play();
-        },
-        this.props.isSelected(true) || this.props.isContentActive(),
-        undefined,
-        () =>
-          !wasPlaying &&
-          this.props.setTime(((clientX - rect.x) / rect.width) * this.duration)
-      );
+
+    get duration() {
+        return this.props.duration;
+    }
+
+    @computed get currentTime() {
+        return NumCast(this.layoutDoc._currentTimecode);
+    }
+    @computed get selectionContainer() {
+        return CollectionStackedTimeline.SelectingRegion !== this ? null : (
+            <div
+                className="collectionStackedTimeline-selector"
+                style={{
+                    left: `${((Math.min(this._markerStart, this._markerEnd) - this.trimStart) / this.props.trimDuration) * 100}%`,
+                    width: `${(Math.abs(this._markerStart - this._markerEnd) / this.props.trimDuration) * 100}%`,
+                }}
+            />
+        );
     }
-  };
-
-  @action
-  startTrimLeft = (e: React.PointerEvent): void => {
-    document.addEventListener("pointermove", this.dragTrimLeft);
-    document.addEventListener("pointerup", this.endTrimLeft);
-  };
-
-  @action
-  startTrimRight = (e: React.PointerEvent): void => {
-    document.addEventListener("pointermove", this.dragTrimRight);
-    document.addEventListener("pointerup", this.endTrimRight);
-  };
-
-  @action
-  dragTrimLeft = (e: MouseEvent) => {
-    const rect = this._timeline?.getBoundingClientRect();
-    if (rect && this.props.isContentActive()) {
-      this.trimStart = Math.min(
-        Math.max(
-          this.trimStart + (e.movementX / rect.width) * this.duration,
-          0
-        ),
-        this.trimEnd - this.minLength
-      );
+
+    constructor(props: any) {
+        super(props);
+        // onClick play scripts
+        CollectionStackedTimeline.RangeScript =
+            CollectionStackedTimeline.RangeScript ||
+            ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, {
+                self: Doc.name,
+                scriptContext: "any",
+                clientX: "number",
+            })!;
+        CollectionStackedTimeline.RangePlayScript =
+            CollectionStackedTimeline.RangePlayScript ||
+            ScriptField.MakeFunction(`scriptContext.playOnClick(this, clientX)`, {
+                self: Doc.name,
+                scriptContext: "any",
+                clientX: "number",
+            })!;
     }
-  };
-
-  @action
-  dragTrimRight = (e: MouseEvent) => {
-    const rect = this._timeline?.getBoundingClientRect();
-    if (rect && this.props.isContentActive()) {
-      this.trimEnd = Math.max(
-        Math.min(
-          this.trimEnd + (e.movementX / rect.width) * this.duration,
-          this.duration
-        ),
-        this.trimStart + this.minLength
-      );
+
+    componentDidMount() {
+        document.addEventListener("keydown", this.keyEvents, true);
     }
-  };
-
-  endTrimLeft = () => {
-    document.removeEventListener("pointermove", this.dragTrimLeft);
-    document.removeEventListener("pointerup", this.endTrimLeft);
-  };
-
-  endTrimRight = () => {
-    document.removeEventListener("pointermove", this.dragTrimRight);
-    document.removeEventListener("pointerup", this.endTrimRight);
-  };
-
-  @undoBatch
-  @action
-  static createAnchor(
-    rootDoc: Doc,
-    dataDoc: Doc,
-    fieldKey: string,
-    startTag: string,
-    endTag: string,
-    anchorStartTime?: number,
-    anchorEndTime?: number
-  ) {
-    if (anchorStartTime === undefined) return rootDoc;
-    const anchor = Docs.Create.LabelDocument({
-      title: ComputedField.MakeFunction(
-        `"#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"])`
-      ) as any,
-      useLinkSmallAnchor: true,
-      hideLinkButton: true,
-      annotationOn: rootDoc,
-      _timelineLabel: true,
-    });
-    Doc.GetProto(anchor)[startTag] = anchorStartTime;
-    Doc.GetProto(anchor)[endTag] = anchorEndTime;
-    if (Cast(dataDoc[fieldKey], listSpec(Doc), null) !== undefined) {
-      Cast(dataDoc[fieldKey], listSpec(Doc), []).push(anchor);
-    } else {
-      dataDoc[fieldKey] = new List<Doc>([anchor]);
+    componentWillUnmount() {
+        document.removeEventListener("keydown", this.keyEvents, true);
+        if (CollectionStackedTimeline.SelectingRegion === this)
+            runInAction(
+                () => (CollectionStackedTimeline.SelectingRegion = undefined)
+            );
     }
-    return anchor;
-  }
-
-  @action
-  playOnClick = (anchorDoc: Doc, clientX: number) => {
-    const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25;
-    const endTime = this.anchorEnd(anchorDoc);
-    if (this.layoutDoc.autoPlayAnchors) {
-      if (this.props.playing()) this.props.Pause();
-      else this.props.playFrom(seekTimeInSeconds, endTime);
-    } else {
-      if (
-        seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) &&
-        endTime > NumCast(this.layoutDoc._currentTimecode)
-      ) {
-        if (!this.layoutDoc.autoPlayAnchors && this.props.playing()) {
-          this.props.Pause();
-        } else {
-          this.props.Play();
+
+    anchorStart = (anchor: Doc) =>
+        NumCast(anchor._timecodeToShow, NumCast(anchor[this.props.startTag]));
+    anchorEnd = (anchor: Doc, val: any = null) => {
+        const endVal = NumCast(anchor[this.props.endTag], val);
+        return NumCast(
+            anchor._timecodeToHide,
+            endVal === undefined ? null : endVal
+        );
+    };
+    toTimeline = (screen_delta: number, width: number) => {
+        return Math.max(
+            this.trimStart,
+            Math.min(this.trimEnd, (screen_delta / width) * this.props.trimDuration + this.trimStart))
+    }
+
+    rangeClickScript = () => CollectionStackedTimeline.RangeScript;
+    rangePlayScript = () => CollectionStackedTimeline.RangePlayScript;
+
+    // for creating key anchors with key events
+    @action
+    keyEvents = (e: KeyboardEvent) => {
+        if (
+            !(e.target instanceof HTMLInputElement) &&
+            this.props.isSelected(true)
+        ) {
+            switch (e.key) {
+                case " ":
+                    if (!CollectionStackedTimeline.SelectingRegion) {
+                        this._markerStart = this._markerEnd = this.currentTime;
+                        CollectionStackedTimeline.SelectingRegion = this;
+                    } else {
+                        CollectionStackedTimeline.createAnchor(
+                            this.rootDoc,
+                            this.dataDoc,
+                            this.props.fieldKey,
+                            this.props.startTag,
+                            this.props.endTag,
+                            this.currentTime
+                        );
+                        CollectionStackedTimeline.SelectingRegion = undefined;
+                    }
+            }
+        }
+    };
+
+    getLinkData(l: Doc) {
+        let la1 = l.anchor1 as Doc;
+        let la2 = l.anchor2 as Doc;
+        const linkTime = NumCast(
+            la2[this.props.startTag],
+            NumCast(la1[this.props.startTag])
+        );
+        if (Doc.AreProtosEqual(la1, this.dataDoc)) {
+            la1 = l.anchor2 as Doc;
+            la2 = l.anchor1 as Doc;
         }
-      } else {
-        this.props.playFrom(seekTimeInSeconds, endTime);
-      }
+        return { la1, la2, linkTime };
     }
-    return { select: true };
-  };
-
-  @action
-  clickAnchor = (anchorDoc: Doc, clientX: number) => {
-    if (anchorDoc.isLinkButton)
-      LinkManager.FollowLink(undefined, anchorDoc, this.props, false);
-    const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25;
-    const endTime = this.anchorEnd(anchorDoc);
-    if (
-      seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 &&
-      endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4
-    ) {
-      if (this.props.playing()) this.props.Pause();
-      else if (this.layoutDoc.autoPlayAnchors) this.props.Play();
-      else if (!this.layoutDoc.autoPlayAnchors) {
+
+    // starting the drag event for anchor resizing
+    @action
+    onPointerDownTimeline = (e: React.PointerEvent): void => {
         const rect = this._timeline?.getBoundingClientRect();
-        rect &&
-          this.props.setTime(this.toTimeline(clientX - rect.x, rect.width));
-      }
-    } else {
-      if (this.layoutDoc.autoPlayAnchors)
-        this.props.playFrom(seekTimeInSeconds, endTime);
-      else this.props.setTime(seekTimeInSeconds);
+        const clientX = e.clientX;
+        if (rect && this.props.isContentActive()) {
+            const wasPlaying = this.props.playing();
+            if (wasPlaying) this.props.Pause();
+            const wasSelecting = CollectionStackedTimeline.SelectingRegion === this;
+            setupMoveUpEvents(
+                this,
+                e,
+                action((e) => {
+                    if (
+                        !wasSelecting &&
+                        CollectionStackedTimeline.SelectingRegion !== this
+                    ) {
+                        this._markerStart = this._markerEnd = this.toTimeline(
+                            clientX - rect.x,
+                            rect.width
+                        );
+                        CollectionStackedTimeline.SelectingRegion = this;
+                    }
+                    this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
+                    return false;
+                }),
+                action((e, movement, isClick) => {
+                    this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
+                    if (this._markerEnd < this._markerStart) {
+                        const tmp = this._markerStart;
+                        this._markerStart = this._markerEnd;
+                        this._markerEnd = tmp;
+                    }
+                    if (
+                        !isClick &&
+                        CollectionStackedTimeline.SelectingRegion === this &&
+                        Math.abs(movement[0]) > 15 &&
+                        !this.props.trimming
+                    ) {
+                        CollectionStackedTimeline.createAnchor(
+                            this.rootDoc,
+                            this.dataDoc,
+                            this.props.fieldKey,
+                            this.props.startTag,
+                            this.props.endTag,
+                            this._markerStart,
+                            this._markerEnd
+                        );
+                    }
+                    (!isClick || !wasSelecting) &&
+                        (CollectionStackedTimeline.SelectingRegion = undefined);
+                }),
+                (e, doubleTap) => {
+                    this.props.select(false);
+                    e.shiftKey &&
+                        CollectionStackedTimeline.createAnchor(
+                            this.rootDoc,
+                            this.dataDoc,
+                            this.props.fieldKey,
+                            this.props.startTag,
+                            this.props.endTag,
+                            this.currentTime
+                        );
+                    !wasPlaying && doubleTap && this.props.Play();
+                },
+                this.props.isSelected(true) || this.props.isContentActive(),
+                undefined,
+                () => {
+                    !wasPlaying &&
+                        (this.props.trimming && this.duration ?
+                            this.props.setTime(((clientX - rect.x) / rect.width) * this.duration)
+                            :
+                            this.props.setTime(((clientX - rect.x) / rect.width) * this.props.trimDuration + this.trimStart)
+                        )
+                }
+            );
+        }
+
+    };
+
+    @action
+    trimLeft = (e: React.PointerEvent): void => {
+        const rect = this._timeline?.getBoundingClientRect();
+        const clientX = e.movementX;
+        setupMoveUpEvents(
+            this,
+            e,
+            action((e, [], []) => {
+                if (rect && this.props.isContentActive()) {
+                    this.props.setStartTrim(Math.min(
+                        Math.max(
+                            this.trimStart + (e.movementX / rect.width) * this.duration,
+                            0
+                        ),
+                        this.trimEnd - this.minLength
+                    ));
+                }
+                return false;
+            }),
+            emptyFunction,
+            action((e, doubleTap) => {
+                if (doubleTap) {
+                    this.props.setStartTrim(0);
+                }
+            })
+        );
+    };
+
+    @action
+    trimRight = (e: React.PointerEvent): void => {
+        const rect = this._timeline?.getBoundingClientRect();
+        const clientX = e.movementX;
+        setupMoveUpEvents(
+            this,
+            e,
+            action((e, [], []) => {
+                if (rect && this.props.isContentActive()) {
+                    this.props.setEndTrim(Math.max(
+                        Math.min(
+                            this.trimEnd + (e.movementX / rect.width) * this.duration,
+                            this.duration
+                        ),
+                        this.trimStart + this.minLength
+                    ));
+                }
+                return false;
+            }),
+            emptyFunction,
+            action((e, doubleTap) => {
+                if (doubleTap) {
+                    this.props.setEndTrim(this.duration);
+                }
+            })
+        );
+    };
+
+    @undoBatch
+    @action
+    static createAnchor(
+        rootDoc: Doc,
+        dataDoc: Doc,
+        fieldKey: string,
+        startTag: string,
+        endTag: string,
+        anchorStartTime?: number,
+        anchorEndTime?: number
+    ) {
+        if (anchorStartTime === undefined) return rootDoc;
+        const anchor = Docs.Create.LabelDocument({
+            title: ComputedField.MakeFunction(
+                `"#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"])`
+            ) as any,
+            useLinkSmallAnchor: true,
+            hideLinkButton: true,
+            annotationOn: rootDoc,
+            _timelineLabel: true,
+        });
+        Doc.GetProto(anchor)[startTag] = anchorStartTime;
+        Doc.GetProto(anchor)[endTag] = anchorEndTime;
+        if (Cast(dataDoc[fieldKey], listSpec(Doc), null) !== undefined) {
+            Cast(dataDoc[fieldKey], listSpec(Doc), []).push(anchor);
+        } else {
+            dataDoc[fieldKey] = new List<Doc>([anchor]);
+        }
+        return anchor;
     }
-    return { select: true };
-  };
-
-  // makes sure no anchors overlaps each other by setting the correct position and width
-  getLevel = (
-    m: Doc,
-    placed: { anchorStartTime: number; anchorEndTime: number; level: number }[]
-  ) => {
-    const timelineContentWidth = this.props.PanelWidth();
-    const x1 = this.anchorStart(m);
-    const x2 = this.anchorEnd(
-      m,
-      x1 + (10 / timelineContentWidth) * this.duration
-    );
-    let max = 0;
-    const overlappedLevels = new Set(
-      placed.map((p) => {
-        const y1 = p.anchorStartTime;
-        const y2 = p.anchorEndTime;
+
+    @action
+    playOnClick = (anchorDoc: Doc, clientX: number) => {
+        const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25;
+        const endTime = this.anchorEnd(anchorDoc);
+        if (this.layoutDoc.autoPlayAnchors) {
+            if (this.props.playing()) this.props.Pause();
+            else this.props.playFrom(seekTimeInSeconds, endTime);
+        } else {
+            if (
+                seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) &&
+                endTime > NumCast(this.layoutDoc._currentTimecode)
+            ) {
+                if (!this.layoutDoc.autoPlayAnchors && this.props.playing()) {
+                    this.props.Pause();
+                } else {
+                    this.props.Play();
+                }
+            } else {
+                this.props.playFrom(seekTimeInSeconds, endTime);
+            }
+        }
+        return { select: true };
+    };
+
+    @action
+    clickAnchor = (anchorDoc: Doc, clientX: number) => {
+        if (anchorDoc.isLinkButton)
+            LinkManager.FollowLink(undefined, anchorDoc, this.props, false);
+        const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25;
+        const endTime = this.anchorEnd(anchorDoc);
         if (
-          (x1 >= y1 && x1 <= y2) ||
-          (x2 >= y1 && x2 <= y2) ||
-          (y1 >= x1 && y1 <= x2) ||
-          (y2 >= x1 && y2 <= x2)
+            seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 &&
+            endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4
         ) {
-          max = Math.max(max, p.level);
-          return p.level;
+            if (this.props.playing()) this.props.Pause();
+            else if (this.layoutDoc.autoPlayAnchors) this.props.Play();
+            else if (!this.layoutDoc.autoPlayAnchors) {
+                const rect = this._timeline?.getBoundingClientRect();
+                rect &&
+                    this.props.setTime(this.toTimeline(clientX - rect.x, rect.width));
+            }
+        } else {
+            if (this.layoutDoc.autoPlayAnchors)
+                this.props.playFrom(seekTimeInSeconds, endTime);
+            else {
+                this.props.setTime(seekTimeInSeconds);
+            }
         }
-      })
-    );
-    let level = max + 1;
-    for (let j = max; j >= 0; j--) !overlappedLevels.has(j) && (level = j);
-
-    placed.push({ anchorStartTime: x1, anchorEndTime: x2, level });
-    return level;
-  };
-
-  dictationHeightPercent = 50;
-  dictationHeight = () =>
-    (this.props.PanelHeight() * (100 - this.dictationHeightPercent)) / 100;
-  timelineContentHeight = () =>
-    (this.props.PanelHeight() * this.dictationHeightPercent) / 100;
-  dictationScreenToLocalTransform = () =>
-    this.props
-      .ScreenToLocalTransform()
-      .translate(0, -this.timelineContentHeight());
-  @computed get renderDictation() {
-    const dictation = Cast(this.dataDoc[this.props.dictationKey], Doc, null);
-    return !dictation ? null : (
-      <div
-        style={{
-          position: "absolute",
-          height: "100%",
-          top: this.timelineContentHeight(),
-          background: "tan",
-        }}
-      >
-        <DocumentView
-          {...OmitKeys(this.props, [
-            "NativeWidth",
-            "NativeHeight",
-            "setContentView",
-          ]).omit}
-          Document={dictation}
-          PanelHeight={this.dictationHeight}
-          isAnnotationOverlay={true}
-          isDocumentActive={returnFalse}
-          select={emptyFunction}
-          scaling={returnOne}
-          xMargin={25}
-          yMargin={10}
-          ScreenToLocalTransform={this.dictationScreenToLocalTransform}
-          whenChildContentsActiveChanged={emptyFunction}
-          removeDocument={returnFalse}
-          moveDocument={returnFalse}
-          addDocument={returnFalse}
-          CollectionView={undefined}
-          renderDepth={this.props.renderDepth + 1}
-        ></DocumentView>
-      </div>
-    );
-  }
-  @computed get renderAudioWaveform() {
-    return !this.props.mediaPath ? null : (
-      <div className="collectionStackedTimeline-waveform">
-        <AudioWaveform
-          duration={this.duration}
-          mediaPath={this.props.mediaPath}
-          dataDoc={this.dataDoc}
-          PanelHeight={this.timelineContentHeight}
-          trimming={this.props.trimming}
-        />
-      </div>
-    );
-  }
-
-  currentTimecode = () => this.currentTime;
-  render() {
-    const timelineContentWidth = this.props.PanelWidth();
-    const overlaps: {
-      anchorStartTime: number;
-      anchorEndTime: number;
-      level: number;
-    }[] = [];
-    const drawAnchors = this.childDocs.map((anchor) => ({
-      level: this.getLevel(anchor, overlaps),
-      anchor,
-    }));
-    const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2;
-    const isActive =
-      this.props.isContentActive() || this.props.isSelected(false);
-    return (
-      <div
-        className="collectionStackedTimeline"
-        ref={(timeline: HTMLDivElement | null) => (this._timeline = timeline)}
-        onClick={(e) => isActive && StopEvent(e)}
-        onPointerDown={(e) => isActive && this.onPointerDownTimeline(e)}
-      >
-        {drawAnchors.map((d) => {
-          const start = this.anchorStart(d.anchor);
-          const end = this.anchorEnd(
-            d.anchor,
-            start + (10 / timelineContentWidth) * this.duration
-          );
-          const left = (start / this.duration) * timelineContentWidth;
-          const top = (d.level / maxLevel) * this.timelineContentHeight();
-          const timespan = end - start;
-          return this.props.Document.hideAnchors ? null : (
+        return { select: true };
+    };
+
+    // makes sure no anchors overlaps each other by setting the correct position and width
+    getLevel = (
+        m: Doc,
+        placed: { anchorStartTime: number; anchorEndTime: number; level: number }[]
+    ) => {
+        const timelineContentWidth = this.props.PanelWidth();
+        const x1 = this.anchorStart(m);
+        const x2 = this.anchorEnd(
+            m,
+            x1 + (10 / timelineContentWidth) * this.duration
+        );
+        let max = 0;
+        const overlappedLevels = new Set(
+            placed.map((p) => {
+                const y1 = p.anchorStartTime;
+                const y2 = p.anchorEndTime;
+                if (
+                    (x1 >= y1 && x1 <= y2) ||
+                    (x2 >= y1 && x2 <= y2) ||
+                    (y1 >= x1 && y1 <= x2) ||
+                    (y2 >= x1 && y2 <= x2)
+                ) {
+                    max = Math.max(max, p.level);
+                    return p.level;
+                }
+            })
+        );
+        let level = max + 1;
+        for (let j = max; j >= 0; j--) !overlappedLevels.has(j) && (level = j);
+
+        placed.push({ anchorStartTime: x1, anchorEndTime: x2, level });
+        return level;
+    };
+
+    dictationHeightPercent = 50;
+    dictationHeight = () =>
+        (this.props.PanelHeight() * (100 - this.dictationHeightPercent)) / 100;
+    timelineContentHeight = () =>
+        (this.props.PanelHeight() * this.dictationHeightPercent) / 100;
+    dictationScreenToLocalTransform = () =>
+        this.props
+            .ScreenToLocalTransform()
+            .translate(0, -this.timelineContentHeight());
+    @computed get renderDictation() {
+        const dictation = Cast(this.dataDoc[this.props.dictationKey], Doc, null);
+        return !dictation ? null : (
             <div
-              className={"collectionStackedTimeline-marker-timeline"}
-              key={d.anchor[Id]}
-              style={{
-                left,
-                top,
-                width: `${(timespan / this.duration) * timelineContentWidth}px`,
-                height: `${this.timelineContentHeight() / maxLevel}px`,
-              }}
-              onClick={(e) => {
-                this.props.playFrom(start, this.anchorEnd(d.anchor));
-                e.stopPropagation();
-              }}
+                style={{
+                    position: "absolute",
+                    height: "100%",
+                    top: this.timelineContentHeight(),
+                    background: Colors.LIGHT_BLUE,
+                }}
             >
-              <StackedTimelineAnchor
-                {...this.props}
-                mark={d.anchor}
-                rangeClickScript={this.rangeClickScript}
-                rangePlayScript={this.rangePlayScript}
-                left={left}
-                top={top}
-                width={(timelineContentWidth * timespan) / this.duration}
-                height={this.timelineContentHeight() / maxLevel}
-                toTimeline={this.toTimeline}
-                layoutDoc={this.layoutDoc}
-                currentTimecode={this.currentTimecode}
-                _timeline={this._timeline}
-                stackedTimeline={this}
-              />
+                <DocumentView
+                    {...OmitKeys(this.props, [
+                        "NativeWidth",
+                        "NativeHeight",
+                        "setContentView",
+                    ]).omit}
+                    Document={dictation}
+                    PanelHeight={this.dictationHeight}
+                    isAnnotationOverlay={true}
+                    isDocumentActive={returnFalse}
+                    select={emptyFunction}
+                    scaling={returnOne}
+                    xMargin={25}
+                    yMargin={10}
+                    ScreenToLocalTransform={this.dictationScreenToLocalTransform}
+                    whenChildContentsActiveChanged={emptyFunction}
+                    removeDocument={returnFalse}
+                    moveDocument={returnFalse}
+                    addDocument={returnFalse}
+                    CollectionView={undefined}
+                    renderDepth={this.props.renderDepth + 1}
+                ></DocumentView>
             </div>
-          );
-        })}
-        {!this.props.trimming && this.selectionContainer}
-        {this.renderAudioWaveform}
-        {this.renderDictation}
-
-        <div
-          className="collectionStackedTimeline-current"
-          style={{
-            left: this.props.trimming
-              ? `${
-                  (this.currentTime /
-                    (NumCast(this.dataDoc.clipEnd) -
-                      NumCast(this.dataDoc.clipStart))) *
-                  100
-                }%`
-              : `${(this.currentTime / this.duration) * 100}%`,
-          }}
-        />
-
-        {this.props.trimming && (
-          <div>
-            <div
-              className="collectionStackedTimeline-trim-shade"
-              style={{ width: `${(this.trimStart / this.duration) * 100}%` }}
-            ></div>
+        );
+    }
+    @computed get renderAudioWaveform() {
+        return !this.props.mediaPath ? null : (
+            <div className="collectionStackedTimeline-waveform">
+                <AudioWaveform
+                    duration={this.duration}
+                    mediaPath={this.props.mediaPath}
+                    layoutDoc={this.layoutDoc}
+                    PanelHeight={this.timelineContentHeight}
+                    trimming={this.props.trimming}
+                />
+            </div>
+        );
+    }
 
+    currentTimecode = () => this.currentTime;
+    render() {
+        const timelineContentWidth = this.props.PanelWidth();
+        const overlaps: {
+            anchorStartTime: number;
+            anchorEndTime: number;
+            level: number;
+        }[] = [];
+        const drawAnchors = this.childDocs.map((anchor) => ({
+            level: this.getLevel(anchor, overlaps),
+            anchor,
+        }));
+        const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2;
+        const isActive =
+            this.props.isContentActive() || this.props.isSelected(false);
+        return (
             <div
-              className="collectionStackedTimeline-trim-controls"
-              style={{
-                left: `${(this.trimStart / this.duration) * 100}%`,
-                width: `${
-                  ((this.trimEnd - this.trimStart) / this.duration) * 100
-                }%`,
-              }}
+                className="collectionStackedTimeline"
+                ref={(timeline: HTMLDivElement | null) => (this._timeline = timeline)}
+                onClick={(e) => isActive && StopEvent(e)}
+                onPointerDown={(e) => isActive && this.onPointerDownTimeline(e)}
             >
-              <div
-                className="collectionStackedTimeline-trim-handle"
-                onPointerDown={this.startTrimLeft}
-              ></div>
-              <div
-                className="collectionStackedTimeline-trim-handle"
-                onPointerDown={this.startTrimRight}
-              ></div>
+                {drawAnchors.map((d) => {
+
+                    const start = this.anchorStart(d.anchor);
+                    const end = this.anchorEnd(
+                        d.anchor,
+                        start + (10 / timelineContentWidth) * this.duration
+                    );
+                    const left = this.props.trimming ?
+                        (start / this.duration) * timelineContentWidth
+                        : (start - this.trimStart) / this.props.trimDuration * timelineContentWidth;
+                    const top = (d.level / maxLevel) * this.timelineContentHeight();
+                    const timespan = end - start;
+                    const width = (timespan / this.props.trimDuration) * timelineContentWidth;
+                    const height = this.timelineContentHeight() / maxLevel
+                    return this.props.Document.hideAnchors ? null : (
+                        <div
+                            className={"collectionStackedTimeline-marker-timeline"}
+                            key={d.anchor[Id]}
+                            style={{
+                                left,
+                                top,
+                                width: `${width}px`,
+                                height: `${height}px`,
+                            }}
+                            onClick={(e) => {
+                                this.props.playFrom(start, this.anchorEnd(d.anchor));
+                                e.stopPropagation();
+                            }}
+                        >
+                            <StackedTimelineAnchor
+                                {...this.props}
+                                mark={d.anchor}
+                                rangeClickScript={this.rangeClickScript}
+                                rangePlayScript={this.rangePlayScript}
+                                left={left}
+                                top={top}
+                                width={width}
+                                height={height}
+                                toTimeline={this.toTimeline}
+                                layoutDoc={this.layoutDoc}
+                                currentTimecode={this.currentTimecode}
+                                _timeline={this._timeline}
+                                stackedTimeline={this}
+                                trimStart={this.trimStart}
+                                trimEnd={this.trimEnd}
+                            />
+                        </div>
+                    );
+                })}
+                {!this.props.trimming && this.selectionContainer}
+                {this.renderAudioWaveform}
+                {this.renderDictation}
+
+                <div
+                    className="collectionStackedTimeline-current"
+                    style={{
+                        left: this.props.trimming
+                            ? `${(this.currentTime / this.duration) * 100}%`
+                            : `${(this.currentTime - this.trimStart) / (this.trimEnd - this.trimStart) * 100}%`,
+                    }}
+                />
+
+                {this.props.trimming && (
+                    <>
+                        <div
+                            className="collectionStackedTimeline-trim-shade"
+                            style={{ width: `${(this.trimStart / this.duration) * 100}%` }}
+                        ></div>
+
+                        <div
+                            className="collectionStackedTimeline-trim-controls"
+                            style={{
+                                left: `${(this.trimStart / this.duration) * 100}%`,
+                                width: `${((this.trimEnd - this.trimStart) / this.duration) * 100
+                                    }%`,
+                            }}
+                        >
+                            <div
+                                className="collectionStackedTimeline-trim-handle"
+                                onPointerDown={this.trimLeft}
+                            ></div>
+                            <div
+                                className="collectionStackedTimeline-trim-handle"
+                                onPointerDown={this.trimRight}
+                            ></div>
+                        </div>
+
+                        <div
+                            className="collectionStackedTimeline-trim-shade"
+                            style={{
+                                left: `${(this.trimEnd / this.duration) * 100}%`,
+                                width: `${((this.duration - this.trimEnd) / this.duration) * 100
+                                    }%`,
+                            }}
+                        ></div>
+                    </>
+                )}
             </div>
-
-            <div
-              className="collectionStackedTimeline-trim-shade"
-              style={{
-                left: `${(this.trimEnd / this.duration) * 100}%`,
-                width: `${
-                  ((this.duration - this.trimEnd) / this.duration) * 100
-                }%`,
-              }}
-            ></div>
-          </div>
-        )}
-      </div>
-    );
-  }
+        );
+    }
 }
 
 interface StackedTimelineAnchorProps {
-  mark: Doc;
-  rangeClickScript: () => ScriptField;
-  rangePlayScript: () => ScriptField;
-  left: number;
-  top: number;
-  width: number;
-  height: number;
-  toTimeline: (screen_delta: number, width: number) => number;
-  playLink: (linkDoc: Doc) => void;
-  setTime: (time: number) => void;
-  startTag: string;
-  endTag: string;
-  renderDepth: number;
-  layoutDoc: Doc;
-  ScreenToLocalTransform: () => Transform;
-  _timeline: HTMLDivElement | null;
-  focus: DocFocusFunc;
-  currentTimecode: () => number;
-  isSelected: (outsideReaction?: boolean) => boolean;
-  stackedTimeline: CollectionStackedTimeline;
+    mark: Doc;
+    rangeClickScript: () => ScriptField;
+    rangePlayScript: () => ScriptField;
+    left: number;
+    top: number;
+    width: number;
+    height: number;
+    toTimeline: (screen_delta: number, width: number) => number;
+    playLink: (linkDoc: Doc) => void;
+    setTime: (time: number) => void;
+    startTag: string;
+    endTag: string;
+    renderDepth: number;
+    layoutDoc: Doc;
+    ScreenToLocalTransform: () => Transform;
+    _timeline: HTMLDivElement | null;
+    focus: DocFocusFunc;
+    currentTimecode: () => number;
+    isSelected: (outsideReaction?: boolean) => boolean;
+    stackedTimeline: CollectionStackedTimeline;
+    trimStart: number;
+    trimEnd: number;
 }
 @observer
 class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> {
-  _lastTimecode: number;
-  _disposer: IReactionDisposer | undefined;
-  constructor(props: any) {
-    super(props);
-    this._lastTimecode = this.props.currentTimecode();
-  }
-  componentDidMount() {
-    this._disposer = reaction(
-      () => this.props.currentTimecode(),
-      (time) => {
-        const dictationDoc = Cast(
-          this.props.layoutDoc["data-dictation"],
-          Doc,
-          null
+    _lastTimecode: number;
+    _disposer: IReactionDisposer | undefined;
+    constructor(props: any) {
+        super(props);
+        this._lastTimecode = this.props.currentTimecode();
+    }
+    componentDidMount() {
+        this._disposer = reaction(
+            () => this.props.currentTimecode(),
+            (time) => {
+                const dictationDoc = Cast(
+                    this.props.layoutDoc["data-dictation"],
+                    Doc,
+                    null
+                );
+                const isDictation =
+                    dictationDoc &&
+                    DocListCast(this.props.mark.links).some(
+                        (link) =>
+                            Cast(link.anchor1, Doc, null)?.annotationOn === dictationDoc
+                    );
+                if (
+                    !LightboxView.LightboxDoc &&
+                    // bcz: when should links be followed?  we don't want to move away from the video to follow a link but we can open it in a sidebar/etc.  But we don't know that upfront.
+                    // for now, we won't follow any links when the lightbox is oepn to avoid "losing" the video.
+                    /*(isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this.props.layoutDoc))*/
+                    DocListCast(this.props.mark.links).length &&
+                    time > NumCast(this.props.mark[this.props.startTag]) &&
+                    time < NumCast(this.props.mark[this.props.endTag]) &&
+                    this._lastTimecode < NumCast(this.props.mark[this.props.startTag])
+                ) {
+                    LinkManager.FollowLink(
+                        undefined,
+                        this.props.mark,
+                        this.props as any as DocumentViewProps,
+                        false,
+                        true
+                    );
+                }
+                this._lastTimecode = time;
+            }
         );
-        const isDictation =
-          dictationDoc &&
-          DocListCast(this.props.mark.links).some(
-            (link) =>
-              Cast(link.anchor1, Doc, null)?.annotationOn === dictationDoc
-          );
-        if (
-          !LightboxView.LightboxDoc &&
-          // bcz: when should links be followed?  we don't want to move away from the video to follow a link but we can open it in a sidebar/etc.  But we don't know that upfront.
-          // for now, we won't follow any links when the lightbox is oepn to avoid "losing" the video.
-          /*(isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this.props.layoutDoc))*/
-          DocListCast(this.props.mark.links).length &&
-          time > NumCast(this.props.mark[this.props.startTag]) &&
-          time < NumCast(this.props.mark[this.props.endTag]) &&
-          this._lastTimecode < NumCast(this.props.mark[this.props.startTag])
-        ) {
-          LinkManager.FollowLink(
-            undefined,
-            this.props.mark,
-            (this.props as any) as DocumentViewProps,
-            false,
-            true
-          );
-        }
-        this._lastTimecode = time;
-      }
-    );
-  }
-  componentWillUnmount() {
-    this._disposer?.();
-  }
-  // starting the drag event for anchor resizing
-  onAnchorDown = (e: React.PointerEvent, anchor: Doc, left: boolean): void => {
-    this.props._timeline?.setPointerCapture(e.pointerId);
-    const newTime = (e: PointerEvent) => {
-      const rect = (e.target as any).getBoundingClientRect();
-      return this.props.toTimeline(e.clientX - rect.x, rect.width);
-    };
-    const changeAnchor = (anchor: Doc, left: boolean, time: number) => {
-      const timelineOnly =
-        Cast(anchor[this.props.startTag], "number", null) !== undefined;
-      if (timelineOnly)
-        Doc.SetInPlace(
-          anchor,
-          left ? this.props.startTag : this.props.endTag,
-          time,
-          true
+    }
+    componentWillUnmount() {
+        this._disposer?.();
+    }
+    // starting the drag event for anchor resizing
+    onAnchorDown = (e: React.PointerEvent, anchor: Doc, left: boolean): void => {
+        this.props._timeline?.setPointerCapture(e.pointerId);
+        const newTime = (e: PointerEvent) => {
+            const rect = (e.target as any).getBoundingClientRect();
+            return this.props.toTimeline(e.clientX - rect.x, rect.width);
+        };
+        const changeAnchor = (anchor: Doc, left: boolean, time: number) => {
+            const timelineOnly =
+                Cast(anchor[this.props.startTag], "number", null) !== undefined;
+            if (timelineOnly)
+                Doc.SetInPlace(
+                    anchor,
+                    left ? this.props.startTag : this.props.endTag,
+                    time,
+                    true
+                );
+            else
+                left
+                    ? (anchor._timecodeToShow = time)
+                    : (anchor._timecodeToHide = time);
+            return false;
+        };
+        setupMoveUpEvents(
+            this,
+            e,
+            (e) => changeAnchor(anchor, left, newTime(e)),
+            (e) => {
+                this.props.setTime(newTime(e));
+                this.props._timeline?.releasePointerCapture(e.pointerId);
+            },
+            emptyFunction
         );
-      else
-        left
-          ? (anchor._timecodeToShow = time)
-          : (anchor._timecodeToHide = time);
-      return false;
     };
-    setupMoveUpEvents(
-      this,
-      e,
-      (e) => changeAnchor(anchor, left, newTime(e)),
-      (e) => {
-        this.props.setTime(newTime(e));
-        this.props._timeline?.releasePointerCapture(e.pointerId);
-      },
-      emptyFunction
-    );
-  };
-  renderInner = computedFn(function (
-    this: StackedTimelineAnchor,
-    mark: Doc,
-    script: undefined | (() => ScriptField),
-    doublescript: undefined | (() => ScriptField),
-    x: number,
-    y: number,
-    width: number,
-    height: number
-  ) {
-    const anchor = observable({ view: undefined as any });
-    const focusFunc = (
-      doc: Doc,
-      willZoom?: boolean,
-      scale?: number,
-      afterFocus?: DocAfterFocusFunc,
-      docTransform?: Transform
-    ) => {
-      this.props.playLink(mark);
-      this.props.focus(doc, { willZoom, scale, afterFocus, docTransform });
-    };
-    return {
-      anchor,
-      view: (
-        <DocumentView
-          key="view"
-          {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
-          ref={action((r: DocumentView | null) => (anchor.view = r))}
-          Document={mark}
-          DataDoc={undefined}
-          renderDepth={this.props.renderDepth + 1}
-          LayoutTemplate={undefined}
-          LayoutTemplateString={LabelBox.LayoutString("data")}
-          isDocumentActive={returnFalse}
-          PanelWidth={() => width}
-          PanelHeight={() => height}
-          ScreenToLocalTransform={() =>
-            this.props.ScreenToLocalTransform().translate(-x, -y)
-          }
-          focus={focusFunc}
-          rootSelected={returnFalse}
-          onClick={script}
-          onDoubleClick={
-            this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript
-          }
-          ignoreAutoHeight={false}
-          hideResizeHandles={true}
-          bringToFront={emptyFunction}
-          scriptContext={this.props.stackedTimeline}
-        />
-      ),
-    };
-  });
-  render() {
-    const inner = this.renderInner(
-      this.props.mark,
-      this.props.rangeClickScript,
-      this.props.rangePlayScript,
-      this.props.left,
-      this.props.top,
-      this.props.width,
-      this.props.height
-    );
-    return (
-      <>
-        {inner.view}
-        {!inner.anchor.view ||
-        !SelectionManager.IsSelected(inner.anchor.view) ? null : (
-          <>
-            <div
-              key="left"
-              className="collectionStackedTimeline-left-resizer"
-              onPointerDown={(e) => this.onAnchorDown(e, this.props.mark, true)}
-            />
-            <div
-              key="right"
-              className="collectionStackedTimeline-resizer"
-              onPointerDown={(e) =>
-                this.onAnchorDown(e, this.props.mark, false)
-              }
-            />
-          </>
-        )}
-      </>
-    );
-  }
+
+    @action
+    computeTitle = () => {
+        const start = Math.max(NumCast(this.props.mark[this.props.startTag]), this.props.trimStart) - this.props.trimStart;
+        const end = Math.min(NumCast(this.props.mark[this.props.endTag]), this.props.trimEnd) - this.props.trimStart;
+        return `#${formatTime(start)}-${formatTime(end)}`
+    }
+
+    renderInner = computedFn(function (
+        this: StackedTimelineAnchor,
+        mark: Doc,
+        script: undefined | (() => ScriptField),
+        doublescript: undefined | (() => ScriptField),
+        x: number,
+        y: number,
+        width: number,
+        height: number
+    ) {
+        const anchor = observable({ view: undefined as any });
+        const focusFunc = (
+            doc: Doc,
+            willZoom?: boolean,
+            scale?: number,
+            afterFocus?: DocAfterFocusFunc,
+            docTransform?: Transform
+        ) => {
+            this.props.playLink(mark);
+            this.props.focus(doc, { willZoom, scale, afterFocus, docTransform });
+        };
+        return {
+            anchor,
+            view: (
+                <DocumentView
+                    key="view"
+                    {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
+                    ref={action((r: DocumentView | null) => (anchor.view = r))}
+                    Document={mark}
+                    DataDoc={undefined}
+                    renderDepth={this.props.renderDepth + 1}
+                    LayoutTemplate={undefined}
+                    LayoutTemplateString={LabelBox.LayoutStringWithTitle(LabelBox, "data", this.computeTitle())}
+                    isDocumentActive={returnFalse}
+                    PanelWidth={() => width}
+                    PanelHeight={() => height}
+                    ScreenToLocalTransform={() =>
+                        this.props.ScreenToLocalTransform().translate(-x, -y)
+                    }
+                    focus={focusFunc}
+                    rootSelected={returnFalse}
+                    onClick={script}
+                    onDoubleClick={
+                        this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript
+                    }
+                    ignoreAutoHeight={false}
+                    hideResizeHandles={true}
+                    bringToFront={emptyFunction}
+                    scriptContext={this.props.stackedTimeline}
+                />
+            ),
+        };
+    });
+
+    render() {
+        const inner = this.renderInner(
+            this.props.mark,
+            this.props.rangeClickScript,
+            this.props.rangePlayScript,
+            this.props.left,
+            this.props.top,
+            this.props.width,
+            this.props.height
+        );
+        return (
+            <>
+                {inner.view}
+                {!inner.anchor.view ||
+                    !SelectionManager.IsSelected(inner.anchor.view) ? null : (
+                    <>
+                        <div
+                            key="left"
+                            className="collectionStackedTimeline-left-resizer"
+                            onPointerDown={(e) => this.onAnchorDown(e, this.props.mark, true)}
+                        />
+                        <div
+                            key="right"
+                            className="collectionStackedTimeline-resizer"
+                            onPointerDown={(e) =>
+                                this.onAnchorDown(e, this.props.mark, false)
+                            }
+                        />
+                    </>
+                )}
+            </>
+        );
+    }
 }
 Scripting.addGlobal(function formatToTime(time: number): any {
-  return formatTime(time);
+    return formatTime(time);
+});
+Scripting.addGlobal(function min(num1: number, num2: number): number {
+    return Math.min(num1, num2);
 });
+Scripting.addGlobal(function max(num1: number, num2: number): number {
+    return Math.max(num1, num2);
+});
\ No newline at end of file
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index 0fb0dc70e..ac2b19fd6 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -1,3 +1,6 @@
+@import "../global/globalCssVariables.scss";
+
+
 .audiobox-container,
 .audiobox-container-interactive {
   width: 100%;
@@ -19,10 +22,11 @@
       height: 100%;
       align-items: center;
       display: inherit;
-      background: dimgray;
+      background: $medium-gray;
       left: 0px;
+      color: $dark-gray;
       &:hover {
-        color: white;
+        color: $black;
         cursor: pointer;
       }
     }
@@ -61,13 +65,13 @@
     position: relative;
     padding-right: 5px;
     display: flex;
-    background-color: red;
+    background-color: $medium-blue;
 
     .time {
       position: relative;
       height: 100%;
       width: 100%;
-      font-size: 20;
+      font-size: $large-header;
       text-align: center;
       top: 5;
     }
@@ -77,9 +81,11 @@
       margin-top: auto;
       margin-bottom: auto;
       width: 25px;
+      width: 25px;
       padding: 5px;
+      color: $dark-gray;
       &:hover {
-        background-color: crimson;
+        color: $black;
       }
     }
   }
@@ -89,16 +95,15 @@
     height: 100%;
     position: relative;
     display: flex;
-    padding-left: 2px;
-    background: black;
+    background: $dark-gray;
 
     .audiobox-dictation {
       position: absolute;
-      width: 32px;
+      width: 40px;
       height: 100%;
       align-items: center;
       display: inherit;
-      background: dimgray;
+      background: $medium-gray;
       left: 0px;
     }
 
@@ -110,22 +115,23 @@
       padding-right: 5px;
       display: flex;
       flex-direction: column;
+      justify-content: center;
 
-      .audiobox-playhead {
+      .audiobox-buttons {
         position: relative;
         margin-top: auto;
         margin-bottom: auto;
-        margin-right: 2px;
-        height: 25px;
+        width: 30px;
+        height: 30px;
         border-radius: 50%;
-        background-color: black;
-        color: white;
+        background-color: $dark-gray;
+        color: $white;
         display: flex;
         align-items: center;
         justify-content: center;
+        left: 5px;
         &:hover {
-          background-color: grey;
-          color: lightgrey;
+          background-color: $black;
         }
 
         svg {
@@ -141,30 +147,28 @@
         margin-top: auto;
         margin-bottom: auto;
         width: 25px;
-        padding: 2px;
         align-items: center;
         display: inherit;
-        background: dimgray;
+        background: $medium-gray;
       }
 
       .audiobox-timeline {
         position: absolute;
         width: 100%;
-        border: gray solid 1px;
-        border-radius: 3px;
         z-index: 1000;
         overflow: hidden;
+        border-right: 5px solid black;
       }
 
       .audioBox-total-time,
       .audioBox-current-time {
         position: absolute;
-        font-size: 8;
+        font-size: $small-text;
         top: 100%;
-        color: white;
+        color: $white;
       }
       .audioBox-current-time {
-        left: 30px;
+        left: 42px;
       }
 
       .audioBox-total-time {
@@ -189,12 +193,12 @@
     font-size: 3em;
   }
 
-  .audiobox-container .audiobox-controls .audiobox-player .audiobox-playhead,
+  .audiobox-container .audiobox-controls .audiobox-player .audiobox-buttons,
   .audiobox-container .audiobox-controls .audiobox-player .audiobox-dictation,
   .audiobox-container-interactive
     .audiobox-controls
     .audiobox-player
-    .audiobox-playhead {
+    .audiobox-buttons {
     width: 70px;
   }
 }
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 99960e934..bcc8e042a 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -1,12 +1,12 @@
 import React = require("react");
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import {
-  action,
-  computed,
-  IReactionDisposer,
-  observable,
-  reaction,
-  runInAction,
+    action,
+    computed,
+    IReactionDisposer,
+    observable,
+    reaction,
+    runInAction,
 } from "mobx";
 import { observer } from "mobx-react";
 import { DateField } from "../../../fields/DateField";
@@ -25,14 +25,17 @@ import { CollectionStackedTimeline } from "../collections/CollectionStackedTimel
 import { ContextMenu } from "../ContextMenu";
 import { ContextMenuProps } from "../ContextMenuItem";
 import {
-  ViewBoxAnnotatableComponent,
-  ViewBoxAnnotatableProps,
+    ViewBoxAnnotatableComponent,
+    ViewBoxAnnotatableProps,
 } from "../DocComponent";
 import "./AudioBox.scss";
 import { FieldView, FieldViewProps } from "./FieldView";
 import { LinkDocPreview } from "./LinkDocPreview";
+import { faLessThan } from "@fortawesome/free-solid-svg-icons";
+import { Colors } from "../global/globalEnums";
+
 declare class MediaRecorder {
-  constructor(e: any); // whatever MediaRecorder has
+    constructor(e: any); // whatever MediaRecorder has
 }
 
 type AudioDocument = makeInterface<[typeof documentSchema]>;
@@ -40,649 +43,677 @@ const AudioDocument = makeInterface(documentSchema);
 
 @observer
 export class AudioBox extends ViewBoxAnnotatableComponent<
-  ViewBoxAnnotatableProps & FieldViewProps,
-  AudioDocument
+    ViewBoxAnnotatableProps & FieldViewProps,
+    AudioDocument
 >(AudioDocument) {
-  public static LayoutString(fieldKey: string) {
-    return FieldView.LayoutString(AudioBox, fieldKey);
-  }
-  public static Enabled = false;
-  static playheadWidth = 30; // width of playhead
-  static heightPercent = 80; // height of timeline in percent of height of audioBox.
-  static Instance: AudioBox;
-
-  _disposers: { [name: string]: IReactionDisposer } = {};
-  _ele: HTMLAudioElement | null = null;
-  _stackedTimeline = React.createRef<CollectionStackedTimeline>();
-  _recorder: any;
-  _recordStart = 0;
-  _pauseStart = 0;
-  _pauseEnd = 0;
-  _pausedTime = 0;
-  _stream: MediaStream | undefined;
-  _start: number = 0;
-  _play: any = null;
-
-  @observable static _scrubTime = 0;
-  @observable _markerEnd: number = 0;
-  @observable _position: number = 0;
-  @observable _waveHeight: Opt<number> = this.layoutDoc._height;
-  @observable _paused: boolean = false;
-  @observable _trimming: boolean = false;
-  @observable _trimBounds: { start: number; end: number } = {
-    start: this.dataDoc.clipStart ? this.dataDoc.clipStart : 0,
-    end: this.dataDoc.clipEnd
-      ? this.dataDoc.clipEnd
-      : this.duration
-      ? this.duration
-      : undefined,
-  };
-  @computed get mediaState():
-    | undefined
-    | "pendingRecording"
-    | "recording"
-    | "paused"
-    | "playing" {
-    return this.dataDoc.mediaState as
-      | undefined
-      | "pendingRecording"
-      | "recording"
-      | "paused"
-      | "playing";
-  }
-  set mediaState(value) {
-    this.dataDoc.mediaState = value;
-  }
-  public static SetScrubTime = action((timeInMillisFrom1970: number) => {
-    AudioBox._scrubTime = 0;
-    AudioBox._scrubTime = timeInMillisFrom1970;
-  });
-  @computed get recordingStart() {
-    return Cast(
-      this.dataDoc[this.props.fieldKey + "-recordingStart"],
-      DateField
-    )?.date.getTime();
-  }
-  @computed get duration() {
-    return NumCast(this.dataDoc[`${this.fieldKey}-duration`]);
-  }
-  @computed get anchorDocs() {
-    return DocListCast(this.dataDoc[this.annotationKey]);
-  }
-  @computed get links() {
-    return DocListCast(this.dataDoc.links);
-  }
-  @computed get pauseTime() {
-    return this._pauseEnd - this._pauseStart;
-  } // total time paused to update the correct time
-  @computed get heightPercent() {
-    return AudioBox.heightPercent;
-  }
-
-  constructor(props: Readonly<ViewBoxAnnotatableProps & FieldViewProps>) {
-    super(props);
-    AudioBox.Instance = this;
-
-    if (this.duration === undefined) {
-      runInAction(
-        () =>
-          (this.Document[this.fieldKey + "-duration"] = this.Document.duration)
-      );
+    public static LayoutString(fieldKey: string) {
+        return FieldView.LayoutString(AudioBox, fieldKey);
     }
-  }
-
-  getLinkData(l: Doc) {
-    let la1 = l.anchor1 as Doc;
-    let la2 = l.anchor2 as Doc;
-    const linkTime =
-      this._stackedTimeline.current?.anchorStart(la2) ||
-      this._stackedTimeline.current?.anchorStart(la1) ||
-      0;
-    if (Doc.AreProtosEqual(la1, this.dataDoc)) {
-      la1 = l.anchor2 as Doc;
-      la2 = l.anchor1 as Doc;
+    public static Enabled = false;
+    static playheadWidth = 40; // width of playhead
+    static heightPercent = 75; // height of timeline in percent of height of audioBox.
+    static Instance: AudioBox;
+
+    _disposers: { [name: string]: IReactionDisposer } = {};
+    _ele: HTMLAudioElement | null = null;
+    _stackedTimeline = React.createRef<CollectionStackedTimeline>();
+    _recorder: any;
+    _recordStart = 0;
+    _pauseStart = 0;
+    _pauseEnd = 0;
+    _pausedTime = 0;
+    _stream: MediaStream | undefined;
+    _start: number = 0;
+    _play: any = null;
+    _ended: boolean = false;
+
+    @observable static _scrubTime = 0;
+    @observable _markerEnd: number = 0;
+    @observable _position: number = 0;
+    @observable _waveHeight: Opt<number> = this.layoutDoc._height;
+    @observable _paused: boolean = false;
+    @observable _trimming: boolean = false;
+    @observable _trimStart: number = NumCast(this.layoutDoc.clipStart) ? NumCast(this.layoutDoc.clipStart) : 0;
+    @observable _trimEnd: number = NumCast(this.layoutDoc.clipEnd) ? NumCast(this.layoutDoc.clipEnd)
+        : this.duration;
+
+    @computed get mediaState():
+        | undefined
+        | "pendingRecording"
+        | "recording"
+        | "paused"
+        | "playing" {
+        return this.dataDoc.mediaState as
+            | undefined
+            | "pendingRecording"
+            | "recording"
+            | "paused"
+            | "playing";
     }
-    return { la1, la2, linkTime };
-  }
-
-  getAnchor = () => {
-    return (
-      CollectionStackedTimeline.createAnchor(
-        this.rootDoc,
-        this.dataDoc,
-        this.annotationKey,
-        "_timecodeToShow" /* audioStart */,
-        "_timecodeToHide" /* audioEnd */,
-        this._ele?.currentTime ||
-          Cast(this.props.Document._currentTimecode, "number", null) ||
-          (this.mediaState === "recording"
-            ? (Date.now() - (this.recordingStart || 0)) / 1000
-            : undefined)
-      ) || this.rootDoc
-    );
-  };
-
-  componentWillUnmount() {
-    Object.values(this._disposers).forEach((disposer) => disposer?.());
-    const ind = DocUtils.ActiveRecordings.indexOf(this);
-    ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
-  }
-
-  @action
-  componentDidMount() {
-    this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document.  this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
-
-    this.mediaState = this.path ? "paused" : undefined;
-
-    this._disposers.triggerAudio = reaction(
-      () =>
-        !LinkDocPreview.LinkInfo && this.props.renderDepth !== -1
-          ? NumCast(this.Document._triggerAudio, null)
-          : undefined,
-      (start) =>
-        start !== undefined &&
-        setTimeout(() => {
-          this.playFrom(start);
-          setTimeout(() => {
-            this.Document._currentTimecode = start;
-            this.Document._triggerAudio = undefined;
-          }, 10);
-        }), // wait for mainCont and try again to play
-      { fireImmediately: true }
-    );
-
-    this._disposers.audioStop = reaction(
-      () =>
-        this.props.renderDepth !== -1 && !LinkDocPreview.LinkInfo
-          ? Cast(this.Document._audioStop, "number", null)
-          : undefined,
-      (audioStop) =>
-        audioStop !== undefined &&
-        setTimeout(() => {
-          this.Pause();
-          setTimeout(() => (this.Document._audioStop = undefined), 10);
-        }), // wait for mainCont and try again to play
-      { fireImmediately: true }
-    );
-  }
-
-  // for updating the timecode
-  timecodeChanged = () => {
-    const htmlEle = this._ele;
-    if (this.mediaState !== "recording" && htmlEle) {
-      htmlEle.duration &&
-        htmlEle.duration !== Infinity &&
-        runInAction(
-          () => (this.dataDoc[this.fieldKey + "-duration"] = htmlEle.duration)
-        );
-      this.links
-        .map((l) => this.getLinkData(l))
-        .forEach(({ la1, la2, linkTime }) => {
-          if (
-            linkTime > NumCast(this.layoutDoc._currentTimecode) &&
-            linkTime < htmlEle.currentTime
-          ) {
-            Doc.linkFollowHighlight(la1);
-          }
-        });
-      this.layoutDoc._currentTimecode = htmlEle.currentTime;
+    set mediaState(value) {
+        this.dataDoc.mediaState = value;
     }
-  };
-
-  // pause play back
-  Pause = action(() => {
-    this._ele!.pause();
-    this.mediaState = "paused";
-  });
-
-  // play audio for documents created during recording
-  playFromTime = (absoluteTime: number) => {
-    this.recordingStart &&
-      this.playFrom((absoluteTime - this.recordingStart) / 1000);
-  };
-
-  // play back the audio from time
-  @action
-  playFrom = (seekTimeInSeconds: number, endTime: number = this.duration) => {
-    clearTimeout(this._play);
-    if (Number.isNaN(this._ele?.duration)) {
-      setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
-    } else if (this._ele && AudioBox.Enabled) {
-      if (seekTimeInSeconds < 0) {
-        if (seekTimeInSeconds > -1) {
-          setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000);
-        } else {
-          this.Pause();
+    public static SetScrubTime = action((timeInMillisFrom1970: number) => {
+        AudioBox._scrubTime = 0;
+        AudioBox._scrubTime = timeInMillisFrom1970;
+    });
+    @computed get recordingStart() {
+        return Cast(
+            this.dataDoc[this.props.fieldKey + "-recordingStart"],
+            DateField
+        )?.date.getTime();
+    }
+    @computed get duration() {
+        return NumCast(this.dataDoc[`${this.fieldKey}-duration`]);
+    }
+    @computed get trimDuration() {
+        return this._trimming && this._trimEnd ? this.duration : this._trimEnd - this._trimStart;
+    }
+    @computed get anchorDocs() {
+        return DocListCast(this.dataDoc[this.annotationKey]);
+    }
+    @computed get links() {
+        return DocListCast(this.dataDoc.links);
+    }
+    @computed get pauseTime() {
+        return this._pauseEnd - this._pauseStart;
+    } // total time paused to update the correct time
+    @computed get heightPercent() {
+        return AudioBox.heightPercent;
+    }
+
+    constructor(props: Readonly<ViewBoxAnnotatableProps & FieldViewProps>) {
+        super(props);
+        AudioBox.Instance = this;
+
+        if (this.duration === undefined) {
+            runInAction(
+                () =>
+                    (this.Document[this.fieldKey + "-duration"] = this.Document.duration)
+            );
         }
-      } else if (seekTimeInSeconds <= this._ele.duration) {
-        this._ele.currentTime = seekTimeInSeconds;
-        this._ele.play();
-        runInAction(() => (this.mediaState = "playing"));
-        if (endTime !== this.duration) {
-          this._play = setTimeout(
-            () => this.Pause(),
-            (endTime - seekTimeInSeconds) * 1000
-          ); // use setTimeout to play a specific duration
+    }
+
+    getLinkData(l: Doc) {
+        let la1 = l.anchor1 as Doc;
+        let la2 = l.anchor2 as Doc;
+        const linkTime =
+            this._stackedTimeline.current?.anchorStart(la2) ||
+            this._stackedTimeline.current?.anchorStart(la1) ||
+            0;
+        if (Doc.AreProtosEqual(la1, this.dataDoc)) {
+            la1 = l.anchor2 as Doc;
+            la2 = l.anchor1 as Doc;
         }
-      } else {
-        this.Pause();
-      }
+        return { la1, la2, linkTime };
     }
-  };
-
-  // update the recording time
-  updateRecordTime = () => {
-    if (this.mediaState === "recording") {
-      setTimeout(this.updateRecordTime, 30);
-      if (this._paused) {
-        this._pausedTime += (new Date().getTime() - this._recordStart) / 1000;
-      } else {
-        this.layoutDoc._currentTimecode =
-          (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
-      }
+
+    getAnchor = () => {
+        return (
+            CollectionStackedTimeline.createAnchor(
+                this.rootDoc,
+                this.dataDoc,
+                this.annotationKey,
+                "_timecodeToShow" /* audioStart */,
+                "_timecodeToHide" /* audioEnd */,
+                this._ele?.currentTime ||
+                Cast(this.props.Document._currentTimecode, "number", null) ||
+                (this.mediaState === "recording"
+                    ? (Date.now() - (this.recordingStart || 0)) / 1000
+                    : undefined)
+            ) || this.rootDoc
+        );
+    };
+
+    componentWillUnmount() {
+        Object.values(this._disposers).forEach((disposer) => disposer?.());
+        const ind = DocUtils.ActiveRecordings.indexOf(this);
+        ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
     }
-  };
-
-  // starts recording
-  recordAudioAnnotation = async () => {
-    this._stream = await navigator.mediaDevices.getUserMedia({ audio: true });
-    this._recorder = new MediaRecorder(this._stream);
-    this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(
-      new Date()
-    );
-    DocUtils.ActiveRecordings.push(this);
-    this._recorder.ondataavailable = async (e: any) => {
-      const [{ result }] = await Networking.UploadFilesToServer(e.data);
-      if (!(result instanceof Error)) {
-        this.props.Document[this.props.fieldKey] = new AudioField(
-          Utils.prepend(result.accessPaths.agnostic.client)
+
+    @action
+    componentDidMount() {
+        this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document.  this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
+
+        this.mediaState = this.path ? "paused" : undefined;
+
+        this.layoutDoc.clipStart = this.layoutDoc.clipStart ? this.layoutDoc.clipStart : 0;
+        this.layoutDoc.clipEnd = this.layoutDoc.clipEnd ? this.layoutDoc.clipEnd : this.duration ? this.duration : undefined;
+
+        this.path && this.setAnchorTime(NumCast(this.layoutDoc.clipStart));
+        this.path && this.timecodeChanged();
+
+        this._disposers.triggerAudio = reaction(
+            () =>
+                !LinkDocPreview.LinkInfo && this.props.renderDepth !== -1
+                    ? NumCast(this.Document._triggerAudio, null)
+                    : undefined,
+            (start) =>
+                start !== undefined &&
+                setTimeout(() => {
+                    this.playFrom(start);
+                    setTimeout(() => {
+                        this.Document._currentTimecode = start;
+                        this.Document._triggerAudio = undefined;
+                    }, 10);
+                }), // wait for mainCont and try again to play
+            { fireImmediately: true }
         );
-      }
+
+        this._disposers.audioStop = reaction(
+            () =>
+                this.props.renderDepth !== -1 && !LinkDocPreview.LinkInfo
+                    ? Cast(this.Document._audioStop, "number", null)
+                    : undefined,
+            (audioStop) =>
+                audioStop !== undefined &&
+                setTimeout(() => {
+                    this.Pause();
+                    setTimeout(() => (this.Document._audioStop = undefined), 10);
+                }), // wait for mainCont and try again to play
+            { fireImmediately: true }
+        );
+    }
+
+    // for updating the timecode
+    @action
+    timecodeChanged = () => {
+        const htmlEle = this._ele;
+        if (this.mediaState !== "recording" && htmlEle) {
+            htmlEle.duration &&
+                htmlEle.duration !== Infinity &&
+                runInAction(
+                    () => (this.dataDoc[this.fieldKey + "-duration"] = htmlEle.duration)
+                );
+            this.layoutDoc.clipEnd = this.layoutDoc.clipEnd ? Math.min(this.duration, NumCast(this.layoutDoc.clipEnd)) : this.duration;
+            this._trimEnd = this._trimEnd ? Math.min(this.duration, this._trimEnd) : this.duration;
+            this.links
+                .map((l) => this.getLinkData(l))
+                .forEach(({ la1, la2, linkTime }) => {
+                    if (
+                        linkTime > NumCast(this.layoutDoc._currentTimecode) &&
+                        linkTime < htmlEle.currentTime
+                    ) {
+                        Doc.linkFollowHighlight(la1);
+                    }
+                });
+            this.layoutDoc._currentTimecode = htmlEle.currentTime;
+
+        }
     };
-    this._recordStart = new Date().getTime();
-    runInAction(() => (this.mediaState = "recording"));
-    setTimeout(this.updateRecordTime, 0);
-    this._recorder.start();
-    setTimeout(() => this._recorder && this.stopRecording(), 60 * 60 * 1000); // stop after an hour
-  };
-
-  // context menu
-  specificContextMenu = (e: React.MouseEvent): void => {
-    const funcs: ContextMenuProps[] = [];
-    funcs.push({
-      description:
-        (this.layoutDoc.hideAnchors ? "Don't hide" : "Hide") + " anchors",
-      event: () => (this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors),
-      icon: "expand-arrows-alt",
-    });
-    funcs.push({
-      description:
-        (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") +
-        " play when link is selected",
-      event: () =>
-        (this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc
-          .dontAutoPlayFollowedLinks),
-      icon: "expand-arrows-alt",
-    });
-    funcs.push({
-      description:
-        (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") +
-        " anchors onClick",
-      event: () =>
-        (this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors),
-      icon: "expand-arrows-alt",
+
+    // pause play back
+    Pause = action(() => {
+        this._ele!.pause();
+        this.mediaState = "paused";
     });
-    ContextMenu.Instance?.addItem({
-      description: "Options...",
-      subitems: funcs,
-      icon: "asterisk",
+
+    // play audio for documents created during recording
+    playFromTime = (absoluteTime: number) => {
+        this.recordingStart &&
+            this.playFrom((absoluteTime - this.recordingStart) / 1000);
+    };
+
+    // play back the audio from time
+    @action
+    playFrom = (seekTimeInSeconds: number, endTime: number = this._trimEnd, fullPlay: boolean = false) => {
+        clearTimeout(this._play);
+        if (Number.isNaN(this._ele?.duration)) {
+            setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
+        } else if (this._ele && AudioBox.Enabled) {
+            if (seekTimeInSeconds < 0) {
+                if (seekTimeInSeconds > -1) {
+                    setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000);
+                } else {
+                    this.Pause();
+                }
+            } else if (this._trimStart <= endTime && seekTimeInSeconds <= this._trimEnd) {
+                const start = Math.max(this._trimStart, seekTimeInSeconds);
+                const end = Math.min(this._trimEnd, endTime);
+                this._ele.currentTime = start;
+                this._ele.play();
+                runInAction(() => (this.mediaState = "playing"));
+                if (endTime !== this.duration) {
+                    this._play = setTimeout(
+                        () => {
+                            this._ended = fullPlay ? true : this._ended;
+                            this.Pause()
+                        },
+                        (end - start) * 1000
+                    ); // use setTimeout to play a specific duration
+                }
+            } else {
+                this.Pause();
+            }
+        }
+    };
+
+    // update the recording time
+    updateRecordTime = () => {
+        if (this.mediaState === "recording") {
+            setTimeout(this.updateRecordTime, 30);
+            if (this._paused) {
+                this._pausedTime += (new Date().getTime() - this._recordStart) / 1000;
+            } else {
+                this.layoutDoc._currentTimecode =
+                    (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
+            }
+        }
+    };
+
+    // starts recording
+    recordAudioAnnotation = async () => {
+        this._stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+        this._recorder = new MediaRecorder(this._stream);
+        this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(
+            new Date()
+        );
+        DocUtils.ActiveRecordings.push(this);
+        this._recorder.ondataavailable = async (e: any) => {
+            const [{ result }] = await Networking.UploadFilesToServer(e.data);
+            if (!(result instanceof Error)) {
+                this.props.Document[this.props.fieldKey] = new AudioField(
+                    Utils.prepend(result.accessPaths.agnostic.client)
+                );
+            }
+        };
+        this._recordStart = new Date().getTime();
+        runInAction(() => (this.mediaState = "recording"));
+        setTimeout(this.updateRecordTime, 0);
+        this._recorder.start();
+        setTimeout(() => this._recorder && this.stopRecording(), 60 * 60 * 1000); // stop after an hour
+    };
+
+    // context menu
+    specificContextMenu = (e: React.MouseEvent): void => {
+        const funcs: ContextMenuProps[] = [];
+        funcs.push({
+            description:
+                (this.layoutDoc.hideAnchors ? "Don't hide" : "Hide") + " anchors",
+            event: () => (this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors),
+            icon: "expand-arrows-alt",
+        });
+        funcs.push({
+            description:
+                (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") +
+                " play when link is selected",
+            event: () =>
+            (this.layoutDoc.dontAutoPlayFollowedLinks =
+                !this.layoutDoc.dontAutoPlayFollowedLinks),
+            icon: "expand-arrows-alt",
+        });
+        funcs.push({
+            description:
+                (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") +
+                " anchors onClick",
+            event: () =>
+                (this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors),
+            icon: "expand-arrows-alt",
+        });
+        ContextMenu.Instance?.addItem({
+            description: "Options...",
+            subitems: funcs,
+            icon: "asterisk",
+        });
+    };
+
+    // stops the recording
+    stopRecording = action(() => {
+        this._recorder.stop();
+        this._recorder = undefined;
+        this.dataDoc[this.fieldKey + "-duration"] =
+            (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
+        this.mediaState = "paused";
+        this._trimEnd = this.duration;
+        this.layoutDoc.clipStart = 0;
+        this.layoutDoc.clipEnd = this.duration;
+        this._stream?.getAudioTracks()[0].stop();
+        const ind = DocUtils.ActiveRecordings.indexOf(this);
+        ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
     });
-  };
-
-  // stops the recording
-  stopRecording = action(() => {
-    this._recorder.stop();
-    this._recorder = undefined;
-    this.dataDoc[this.fieldKey + "-duration"] =
-      (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
-    this.mediaState = "paused";
-    this._trimBounds.end = this.duration;
-    this.dataDoc.clipStart = 0;
-    this.dataDoc.clipEnd = this.duration;
-    this._stream?.getAudioTracks()[0].stop();
-    const ind = DocUtils.ActiveRecordings.indexOf(this);
-    ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
-  });
-
-  // button for starting and stopping the recording
-  recordClick = (e: React.MouseEvent) => {
-    if (e.button === 0 && !e.ctrlKey) {
-      this._recorder ? this.stopRecording() : this.recordAudioAnnotation();
-      e.stopPropagation();
+
+    // button for starting and stopping the recording
+    recordClick = (e: React.MouseEvent) => {
+        if (e.button === 0 && !e.ctrlKey) {
+            this._recorder ? this.stopRecording() : this.recordAudioAnnotation();
+            e.stopPropagation();
+        }
+    };
+
+    // for play button
+    Play = (e?: any) => {
+        let start;
+        if (this._ended || this._ele!.currentTime == this.duration) {
+            start = this._trimStart;
+            this._ended = false;
+        }
+        else {
+            start = this._ele!.currentTime;
+        }
+
+        this.playFrom(start, this._trimEnd, true);
+        e?.stopPropagation?.();
+    };
+
+    // creates a text document for dictation
+    onFile = (e: any) => {
+        const newDoc = CurrentUserUtils.GetNewTextDoc(
+            "",
+            NumCast(this.props.Document.x),
+            NumCast(this.props.Document.y) +
+            NumCast(this.props.Document._height) +
+            10,
+            NumCast(this.props.Document._width),
+            2 * NumCast(this.props.Document._height)
+        );
+        Doc.GetProto(newDoc).recordingSource = this.dataDoc;
+        Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(
+            `self.recordingSource["${this.props.fieldKey}-recordingStart"]`
+        );
+        Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction(
+            "self.recordingSource.mediaState"
+        );
+        this.props.addDocument?.(newDoc);
+        e.stopPropagation();
+    };
+
+    // ref for updating time
+    setRef = (e: HTMLAudioElement | null) => {
+        e?.addEventListener("timeupdate", this.timecodeChanged);
+        e?.addEventListener("ended", this.Pause);
+        this._ele = e;
+    };
+
+    // returns the path of the audio file
+    @computed get path() {
+        const field = Cast(this.props.Document[this.props.fieldKey], AudioField);
+        const path = field instanceof AudioField ? field.url.href : "";
+        return path === nullAudio ? "" : path;
     }
-  };
-
-  // for play button
-  Play = (e?: any) => {
-    if (this._trimming) {
-      let end = this._trimBounds.end;
-      let start =
-        this._ele!.currentTime > end ||
-        this._ele!.currentTime < this._trimBounds.start
-          ? -1
-          : this._trimBounds.start;
-      this.playFrom(this._ele!.paused ? this._ele!.currentTime : start, end);
-    } else {
-      this.playFrom(this._ele!.paused ? this._ele!.currentTime : -1);
+
+    // returns the html audio element
+    @computed get audio() {
+        return (
+            <audio
+                ref={this.setRef}
+                className={`audiobox-control${this.isContentActive() ? "-interactive" : ""
+                    }`}
+            >
+                <source src={this.path} type="audio/mpeg" />
+                Not supported.
+            </audio>
+        );
     }
-    e?.stopPropagation?.();
-  };
-
-  // creates a text document for dictation
-  onFile = (e: any) => {
-    const newDoc = CurrentUserUtils.GetNewTextDoc(
-      "",
-      NumCast(this.props.Document.x),
-      NumCast(this.props.Document.y) +
-        NumCast(this.props.Document._height) +
-        10,
-      NumCast(this.props.Document._width),
-      2 * NumCast(this.props.Document._height)
-    );
-    Doc.GetProto(newDoc).recordingSource = this.dataDoc;
-    Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(
-      `self.recordingSource["${this.props.fieldKey}-recordingStart"]`
-    );
-    Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction(
-      "self.recordingSource.mediaState"
-    );
-    this.props.addDocument?.(newDoc);
-    e.stopPropagation();
-  };
-
-  // ref for updating time
-  setRef = (e: HTMLAudioElement | null) => {
-    e?.addEventListener("timeupdate", this.timecodeChanged);
-    e?.addEventListener("ended", this.Pause);
-    this._ele = e;
-  };
-
-  // returns the path of the audio file
-  @computed get path() {
-    const field = Cast(this.props.Document[this.props.fieldKey], AudioField);
-    const path = field instanceof AudioField ? field.url.href : "";
-    return path === nullAudio ? "" : path;
-  }
-
-  // returns the html audio element
-  @computed get audio() {
-    return (
-      <audio
-        ref={this.setRef}
-        className={`audiobox-control${
-          this.isContentActive() ? "-interactive" : ""
-        }`}
-      >
-        <source src={this.path} type="audio/mpeg" />
-        Not supported.
-      </audio>
-    );
-  }
-
-  // pause the time during recording phase
-  @action
-  recordPause = (e: React.MouseEvent) => {
-    this._pauseStart = new Date().getTime();
-    this._paused = true;
-    this._recorder.pause();
-    e.stopPropagation();
-  };
-
-  // continue the recording
-  @action
-  recordPlay = (e: React.MouseEvent) => {
-    this._pauseEnd = new Date().getTime();
-    this._paused = false;
-    this._recorder.resume();
-    e.stopPropagation();
-  };
-
-  playing = () => this.mediaState === "playing";
-  playLink = (link: Doc) => {
-    const stack = this._stackedTimeline.current;
-    if (link.annotationOn === this.rootDoc) {
-      if (!this.layoutDoc.dontAutoPlayFollowedLinks)
-        this.playFrom(stack?.anchorStart(link) || 0, stack?.anchorEnd(link));
-      else
-        this._ele!.currentTime = this.layoutDoc._currentTimecode =
-          stack?.anchorStart(link) || 0;
-    } else {
-      this.links
-        .filter((l) => l.anchor1 === link || l.anchor2 === link)
-        .forEach((l) => {
-          const { la1, la2 } = this.getLinkData(l);
-          const startTime = stack?.anchorStart(la1) || stack?.anchorStart(la2);
-          const endTime = stack?.anchorEnd(la1) || stack?.anchorEnd(la2);
-          if (startTime !== undefined) {
+
+    // pause the time during recording phase
+    @action
+    recordPause = (e: React.MouseEvent) => {
+        this._pauseStart = new Date().getTime();
+        this._paused = true;
+        this._recorder.pause();
+        e.stopPropagation();
+    };
+
+    // continue the recording
+    @action
+    recordPlay = (e: React.MouseEvent) => {
+        this._pauseEnd = new Date().getTime();
+        this._paused = false;
+        this._recorder.resume();
+        e.stopPropagation();
+    };
+
+    playing = () => this.mediaState === "playing";
+    playLink = (link: Doc) => {
+        const stack = this._stackedTimeline.current;
+        if (link.annotationOn === this.rootDoc) {
             if (!this.layoutDoc.dontAutoPlayFollowedLinks)
-              endTime
-                ? this.playFrom(startTime, endTime)
-                : this.playFrom(startTime);
+                this.playFrom(stack?.anchorStart(link) || 0, stack?.anchorEnd(link));
             else
-              this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime;
-          }
-        });
+                this._ele!.currentTime = this.layoutDoc._currentTimecode =
+                    stack?.anchorStart(link) || 0;
+        } else {
+            this.links
+                .filter((l) => l.anchor1 === link || l.anchor2 === link)
+                .forEach((l) => {
+                    const { la1, la2 } = this.getLinkData(l);
+                    const startTime = stack?.anchorStart(la1) || stack?.anchorStart(la2);
+                    const endTime = stack?.anchorEnd(la1) || stack?.anchorEnd(la2);
+                    if (startTime !== undefined) {
+                        if (!this.layoutDoc.dontAutoPlayFollowedLinks)
+                            endTime
+                                ? this.playFrom(startTime, endTime)
+                                : this.playFrom(startTime);
+                        else
+                            this._ele!.currentTime = this.layoutDoc._currentTimecode =
+                                startTime;
+                    }
+                });
+        }
+    };
+
+    // shows trim controls
+    @action
+    startTrim = () => {
+        if (!this.duration) {
+            this.timecodeChanged();
+        }
+        if (this.mediaState === "playing") {
+            this.Pause();
+        }
+        this._trimming = true;
+    };
+
+    // hides trim controls and displays new clip
+    @action
+    finishTrim = () => {
+        if (this.mediaState === "playing") {
+            this.Pause();
+        }
+        this.layoutDoc.clipStart = this._trimStart;
+        this.layoutDoc.clipEnd = this._trimEnd;
+        this._trimming = false;
+        this.setAnchorTime(Math.max(Math.min(this._trimEnd, this._ele!.currentTime), this._trimStart));
+    };
+
+    @action
+    setStartTrim = (newStart: number) => {
+        this._trimStart = newStart;
     }
-  };
 
-  // shows trim controls
-  @action
-  startTrim = () => {
-    if (this.mediaState === "playing") {
-      this.Pause();
+    @action
+    setEndTrim = (newEnd: number) => {
+        this._trimEnd = newEnd;
     }
-    this._trimming = true;
-  };
-
-  // hides trim controls and displays new clip
-  @action
-  finishTrim = () => {
-    if (this.mediaState === "playing") {
-      this.Pause();
+
+    isActiveChild = () => this._isAnyChildContentActive;
+    timelineWhenChildContentsActiveChanged = (isActive: boolean) =>
+        this.props.whenChildContentsActiveChanged(
+            runInAction(() => (this._isAnyChildContentActive = isActive))
+        );
+    timelineScreenToLocal = () =>
+        this.props
+            .ScreenToLocalTransform()
+            .translate(
+                -AudioBox.playheadWidth,
+                (-(100 - this.heightPercent) / 200) * this.props.PanelHeight()
+            );
+    setAnchorTime = (time: number) => {
+        (this._ele!.currentTime = this.layoutDoc._currentTimecode = time);
     }
-    this.dataDoc.clipStart = this._trimBounds.start;
-    this.dataDoc.clipEnd = this._trimBounds.end;
-    this._trimming = false;
-  };
-
-  isActiveChild = () => this._isAnyChildContentActive;
-  timelineWhenChildContentsActiveChanged = (isActive: boolean) =>
-    this.props.whenChildContentsActiveChanged(
-      runInAction(() => (this._isAnyChildContentActive = isActive))
-    );
-  timelineScreenToLocal = () =>
-    this.props
-      .ScreenToLocalTransform()
-      .translate(
-        -AudioBox.playheadWidth,
-        (-(100 - this.heightPercent) / 200) * this.props.PanelHeight()
-      );
-  setAnchorTime = (time: number) =>
-    (this._ele!.currentTime = this.layoutDoc._currentTimecode = time);
-  timelineHeight = () =>
-    (((this.props.PanelHeight() * this.heightPercent) / 100) *
-      this.heightPercent) /
-    100; // panelHeight * heightPercent is player height.  * heightPercent is timeline height (as per css inline)
-  timelineWidth = () => this.props.PanelWidth() - AudioBox.playheadWidth;
-  @computed get renderTimeline() {
-    return (
-      <CollectionStackedTimeline
-        ref={this._stackedTimeline}
-        {...this.props}
-        fieldKey={this.annotationKey}
-        dictationKey={this.fieldKey + "-dictation"}
-        mediaPath={this.path}
-        renderDepth={this.props.renderDepth + 1}
-        startTag={"_timecodeToShow" /* audioStart */}
-        endTag={"_timecodeToHide" /* audioEnd */}
-        focus={DocUtils.DefaultFocus}
-        bringToFront={emptyFunction}
-        CollectionView={undefined}
-        duration={this.duration}
-        playFrom={this.playFrom}
-        setTime={this.setAnchorTime}
-        playing={this.playing}
-        whenChildContentsActiveChanged={
-          this.timelineWhenChildContentsActiveChanged
-        }
-        removeDocument={this.removeDocument}
-        ScreenToLocalTransform={this.timelineScreenToLocal}
-        Play={this.Play}
-        Pause={this.Pause}
-        isContentActive={this.isContentActive}
-        playLink={this.playLink}
-        PanelWidth={this.timelineWidth}
-        PanelHeight={this.timelineHeight}
-        trimming={this._trimming}
-        trimBounds={this._trimBounds}
-      />
-    );
-  }
-
-  render() {
-    const interactive =
-      SnappingManager.GetIsDragging() || this.isContentActive()
-        ? "-interactive"
-        : "";
-    return (
-      <div
-        className="audiobox-container"
-        onContextMenu={this.specificContextMenu}
-        onClick={
-          !this.path && !this._recorder ? this.recordAudioAnnotation : undefined
-        }
-        style={{
-          pointerEvents:
-            this.props.layerProvider?.(this.layoutDoc) === false
-              ? "none"
-              : undefined,
-        }}
-      >
-        {!this.path ? (
-          <div className="audiobox-buttons">
-            <div className="audiobox-dictation" onClick={this.onFile}>
-              <FontAwesomeIcon
-                style={{
-                  width: "30px",
-                  background: !this.layoutDoc.dontAutoPlayFollowedLinks
-                    ? "yellow"
-                    : "rgba(0,0,0,0)",
-                }}
-                icon="file-alt"
-                size={this.props.PanelHeight() < 36 ? "1x" : "2x"}
-              />
-            </div>
-            {this.mediaState === "recording" || this.mediaState === "paused" ? (
-              <div className="recording" onClick={(e) => e.stopPropagation()}>
-                <div className="buttons" onClick={this.recordClick}>
-                  <FontAwesomeIcon
-                    icon={"stop"}
-                    size={this.props.PanelHeight() < 36 ? "1x" : "2x"}
-                  />
-                </div>
-                <div
-                  className="buttons"
-                  onClick={this._paused ? this.recordPlay : this.recordPause}
-                >
-                  <FontAwesomeIcon
-                    icon={this._paused ? "play" : "pause"}
-                    size={this.props.PanelHeight() < 36 ? "1x" : "2x"}
-                  />
-                </div>
-                <div className="time">
-                  {formatTime(
-                    Math.round(NumCast(this.layoutDoc._currentTimecode))
-                  )}
-                </div>
-              </div>
-            ) : (
-              <button
-                className={`audiobox-record${interactive}`}
-                style={{ backgroundColor: "black" }}
-              >
-                RECORD
-              </button>
-            )}
-          </div>
-        ) : (
-          <div
-            className="audiobox-controls"
-            style={{
-              pointerEvents:
-                this._isAnyChildContentActive || this.isContentActive()
-                  ? "all"
-                  : "none",
-            }}
-          >
-            <div className="audiobox-dictation" />
+
+    timelineHeight = () =>
+        (((this.props.PanelHeight() * this.heightPercent) / 100) *
+            this.heightPercent) /
+        100; // panelHeight * heightPercent is player height.  * heightPercent is timeline height (as per css inline)
+    timelineWidth = () => this.props.PanelWidth() - AudioBox.playheadWidth;
+    @computed get renderTimeline() {
+        return (
+            <CollectionStackedTimeline
+                ref={this._stackedTimeline}
+                {...this.props}
+                fieldKey={this.annotationKey}
+                dictationKey={this.fieldKey + "-dictation"}
+                mediaPath={this.path}
+                renderDepth={this.props.renderDepth + 1}
+                startTag={"_timecodeToShow" /* audioStart */}
+                endTag={"_timecodeToHide" /* audioEnd */}
+                focus={DocUtils.DefaultFocus}
+                bringToFront={emptyFunction}
+                CollectionView={undefined}
+                duration={this.duration}
+                playFrom={this.playFrom}
+                setTime={this.setAnchorTime}
+                playing={this.playing}
+                whenChildContentsActiveChanged={
+                    this.timelineWhenChildContentsActiveChanged
+                }
+                removeDocument={this.removeDocument}
+                ScreenToLocalTransform={this.timelineScreenToLocal}
+                Play={this.Play}
+                Pause={this.Pause}
+                isContentActive={this.isContentActive}
+                playLink={this.playLink}
+                PanelWidth={this.timelineWidth}
+                PanelHeight={this.timelineHeight}
+                trimming={this._trimming}
+                trimStart={this._trimStart}
+                trimEnd={this._trimEnd}
+                trimDuration={this.trimDuration}
+                setStartTrim={this.setStartTrim}
+                setEndTrim={this.setEndTrim}
+            />
+        );
+    }
+
+    render() {
+        const interactive =
+            SnappingManager.GetIsDragging() || this.isContentActive()
+                ? "-interactive"
+                : "";
+        return (
             <div
-              className="audiobox-player"
-              style={{ height: `${AudioBox.heightPercent}%` }}
-            >
-              <div
-                className="audiobox-playhead"
-                style={{
-                  width: AudioBox.playheadWidth,
-                  height: AudioBox.playheadWidth,
-                }}
-                title={this.mediaState === "paused" ? "play" : "pause"}
-                onClick={this.Play}
-              >
-                {" "}
-                <FontAwesomeIcon
-                  icon={this.mediaState === "paused" ? "play" : "pause"}
-                  size={"1x"}
-                />
-              </div>
-              <div
-                className="audiobox-playhead"
-                style={{
-                  width: AudioBox.playheadWidth,
-                  height: AudioBox.playheadWidth,
-                }}
-                title={this._trimming ? "finish" : "trim"}
-                onClick={this._trimming ? this.finishTrim : this.startTrim}
-              >
-                <FontAwesomeIcon
-                  icon={this._trimming ? "check" : "cut"}
-                  size={"1x"}
-                />
-              </div>
-              <div
-                className="audiobox-timeline"
+                className="audiobox-container"
+                onContextMenu={this.specificContextMenu}
+                onClick={
+                    !this.path && !this._recorder ? this.recordAudioAnnotation : undefined
+                }
                 style={{
-                  top: 0,
-                  height: `100%`,
-                  left: AudioBox.playheadWidth,
-                  width: `calc(100% - ${AudioBox.playheadWidth}px)`,
-                  background: "white",
+                    pointerEvents:
+                        this.props.layerProvider?.(this.layoutDoc) === false
+                            ? "none"
+                            : undefined,
                 }}
-              >
-                {this.renderTimeline}
-              </div>
-              {this.audio}
-              <div className="audioBox-current-time">
-                {formatTime(
-                  Math.round(NumCast(this.layoutDoc._currentTimecode))
+            >
+                {!this.path ? (
+                    <div className="audiobox-buttons">
+                        <div className="audiobox-dictation" onClick={this.onFile}>
+                            <FontAwesomeIcon
+                                style={{
+                                    width: "30px",
+                                    background: !this.layoutDoc.dontAutoPlayFollowedLinks
+                                        ? Colors.LIGHT_BLUE
+                                        : "rgba(0,0,0,0)",
+                                }}
+                                icon="file-alt"
+                                size={this.props.PanelHeight() < 36 ? "1x" : "2x"}
+                            />
+                        </div>
+                        {this.mediaState === "recording" || this.mediaState === "paused" ? (
+                            <div className="recording" onClick={(e) => e.stopPropagation()}>
+                                <div className="recording-buttons" onClick={this.recordClick}>
+                                    <FontAwesomeIcon
+                                        icon={"stop"}
+                                        size={this.props.PanelHeight() < 36 ? "1x" : "2x"}
+                                    />
+                                </div>
+                                <div
+                                    className="recording-buttons"
+                                    onClick={this._paused ? this.recordPlay : this.recordPause}
+                                >
+                                    <FontAwesomeIcon
+                                        icon={this._paused ? "play" : "pause"}
+                                        size={this.props.PanelHeight() < 36 ? "1x" : "2x"}
+                                    />
+                                </div>
+                                <div className="time">
+                                    {formatTime(
+                                        Math.round(NumCast(this.layoutDoc._currentTimecode))
+                                    )}
+                                </div>
+                            </div>
+                        ) : (
+                            <button
+                                className={`audiobox-record${interactive}`}
+                                style={{ backgroundColor: Colors.DARK_GRAY }}
+                            >
+                                RECORD
+                            </button>
+                        )}
+                    </div>
+                ) : (
+                    <div
+                        className="audiobox-controls"
+                        style={{
+                            pointerEvents:
+                                this._isAnyChildContentActive || this.isContentActive()
+                                    ? "all"
+                                    : "none",
+                        }}
+                    >
+                        <div className="audiobox-dictation" />
+                        <div
+                            className="audiobox-player"
+                            style={{ height: `${AudioBox.heightPercent}%` }}
+                        >
+                            <div
+                                className="audiobox-buttons"
+                                title={this.mediaState === "paused" ? "play" : "pause"}
+                                onClick={this.mediaState === "paused" ? this.Play : this.Pause}
+                            >
+                                {" "}
+                                <FontAwesomeIcon
+                                    icon={this.mediaState === "paused" ? "play" : "pause"}
+                                    size={"1x"}
+                                />
+                            </div>
+                            <div
+                                className="audiobox-buttons"
+                                title={this._trimming ? "finish" : "trim"}
+                                onClick={this._trimming ? this.finishTrim : this.startTrim}
+                            >
+                                <FontAwesomeIcon
+                                    icon={this._trimming ? "check" : "cut"}
+                                    size={"1x"}
+                                />
+                            </div>
+                            <div
+                                className="audiobox-timeline"
+                                style={{
+                                    top: 0,
+                                    height: `100%`,
+                                    left: AudioBox.playheadWidth,
+                                    width: `calc(100% - ${AudioBox.playheadWidth}px)`,
+                                    background: "white",
+                                }}
+                            >
+                                {this.renderTimeline}
+                            </div>
+                            {this.audio}
+                            <div className="audioBox-current-time">
+                                {this._trimming ?
+                                    formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))
+                                    : formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode) - NumCast(this._trimStart)))}
+                            </div>
+                            <div className="audioBox-total-time">
+                                {this._trimming || !this._trimEnd ?
+                                    formatTime(Math.round(NumCast(this.duration)))
+                                    : formatTime(Math.round(NumCast(this.trimDuration)))}
+                            </div>
+                        </div>
+                    </div>
                 )}
-              </div>
-              <div className="audioBox-total-time">
-                {formatTime(Math.round(this.duration))}
-              </div>
             </div>
-          </div>
-        )}
-      </div>
-    );
-  }
+        );
+    }
 }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 80a014926..23166335e 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -88,6 +88,7 @@ export interface DocComponentView {
     setKeyFrameEditing?: (set: boolean) => void; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
     playFrom?: (time: number, endTime?: number) => void;
     setFocus?: () => void;
+    getTitle?: () => string;
 }
 export interface DocumentViewSharedProps {
     renderDepth: number;
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 6a7793ff0..8d665b8a6 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -20,11 +20,26 @@ const LabelSchema = createSchema({});
 type LabelDocument = makeInterface<[typeof LabelSchema, typeof documentSchema]>;
 const LabelDocument = makeInterface(LabelSchema, documentSchema);
 
+export interface LabelBoxProps {
+    label?: string
+}
+
 @observer
-export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument>(LabelDocument) {
+export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxProps), LabelDocument>(LabelDocument) {
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LabelBox, fieldKey); }
+    public static LayoutStringWithTitle(fieldType: { name: string }, fieldStr: string, label: string) {
+        return `<${fieldType.name} fieldKey={'${fieldStr}'} label={'${label}'} {...props} />`;  //e.g., "<ImageBox {...props} fieldKey={"data} />"
+    }
     private dropDisposer?: DragManager.DragDropDisposer;
 
+    componentDidMount() {
+        this.props.setContentView?.(this);
+    }
+
+    getTitle() {
+        return this.props.label || "";
+    }
+
     protected createDropTarget = (ele: HTMLDivElement) => {
         this.dropDisposer?.();
         if (ele) {
@@ -65,8 +80,8 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument
     render() {
         const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []);
         const missingParams = params?.filter(p => !this.paramsDoc[p]);
-        params?.map(p => DocListCast(this.paramsDoc[p])); // bcz: really hacky form of prefetching ... 
-        const label = typeof this.rootDoc[this.fieldKey] === "string" ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title);
+        params?.map(p => DocListCast(this.paramsDoc[p])); // bcz: really hacky form of prefetching ...
+        const label = this.props.label ? this.props.label : typeof this.rootDoc[this.fieldKey] === "string" ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title);
         return (
             <div className="labelBox-outerDiv"
                 onMouseLeave={action(() => this._mouseOver = false)}
-- 
cgit v1.2.3-70-g09d2

