diff --git a/hmm.json b/hmm.json
index 4086c92f4..c8b1d911e 100644
--- a/hmm.json
+++ b/hmm.json
@@ -54,7 +54,7 @@
       "name": "haxeui-core",
       "type": "git",
       "dir": null,
-      "ref": "5b2d5b8e7e470cf637953e1369c80a1f42016a75",
+      "ref": "8a7846b",
       "url": "https://github.com/haxeui/haxeui-core"
     },
     {
diff --git a/source/funkin/data/event/SongEventSchema.hx b/source/funkin/data/event/SongEventSchema.hx
index 7ebaa5ae1..9591e601e 100644
--- a/source/funkin/data/event/SongEventSchema.hx
+++ b/source/funkin/data/event/SongEventSchema.hx
@@ -6,9 +6,14 @@ import funkin.data.song.SongData.SongEventData;
 import funkin.util.macro.ClassMacro;
 import funkin.play.event.ScriptedSongEvent;
 
-@:forward(name, tittlte, type, keys, min, max, step, defaultValue, iterator)
+@:forward(name, title, type, keys, min, max, step, units, defaultValue, iterator)
 abstract SongEventSchema(SongEventSchemaRaw)
 {
+  /**
+   * These units look better when placed immediately next to the value, rather than after a space.
+   */
+  static final NO_SPACE_UNITS:Array<String> = ['x', '°', '%'];
+
   public function new(?fields:Array<SongEventSchemaField>)
   {
     this = fields;
@@ -42,7 +47,7 @@ abstract SongEventSchema(SongEventSchemaRaw)
     return this[k] = v;
   }
 
-  public function stringifyFieldValue(name:String, value:Dynamic):String
+  public function stringifyFieldValue(name:String, value:Dynamic, addUnits:Bool = true):String
   {
     var field:SongEventSchemaField = getByName(name);
     if (field == null) return 'Unknown';
@@ -52,21 +57,36 @@ abstract SongEventSchema(SongEventSchemaRaw)
       case SongEventFieldType.STRING:
         return Std.string(value);
       case SongEventFieldType.INTEGER:
-        return Std.string(value);
+        var returnValue:String = Std.string(value);
+        if (addUnits) return addUnitsToString(returnValue, field);
+        return returnValue;
       case SongEventFieldType.FLOAT:
-        return Std.string(value);
+        var returnValue:String = Std.string(value);
+        if (addUnits) return addUnitsToString(returnValue, field);
+        return returnValue;
       case SongEventFieldType.BOOL:
         return Std.string(value);
       case SongEventFieldType.ENUM:
+        var valueString:String = Std.string(value);
         for (key in field.keys.keys())
         {
-          if (field.keys.get(key) == value) return key;
+          // Comparing these values as strings because comparing Dynamic variables is jank.
+          if (Std.string(field.keys.get(key)) == valueString) return key;
         }
-        return Std.string(value);
+        return valueString;
       default:
         return 'Unknown';
     }
   }
+
+  function addUnitsToString(value:String, field:SongEventSchemaField)
+  {
+    if (field.units == null || field.units == '') return value;
+
+    var unit:String = field.units;
+
+    return value + (NO_SPACE_UNITS.contains(unit) ? '' : ' ') + '${unit}';
+  }
 }
 
 typedef SongEventSchemaRaw = Array<SongEventSchemaField>;
@@ -115,6 +135,12 @@ typedef SongEventSchemaField =
    */
   ?step:Float,
 
+  /**
+   * Used for INTEGER and FLOAT values.
+   * The units that the value is expressed in (pixels, percent, etc).
+   */
+  ?units:String,
+
   /**
    * An optional default value for the field.
    */
diff --git a/source/funkin/data/song/SongDataUtils.hx b/source/funkin/data/song/SongDataUtils.hx
index 275106f3a..01ea2da32 100644
--- a/source/funkin/data/song/SongDataUtils.hx
+++ b/source/funkin/data/song/SongDataUtils.hx
@@ -153,8 +153,8 @@ class SongDataUtils
   public static function buildNoteClipboard(notes:Array<SongNoteData>, ?timeOffset:Int = null):Array<SongNoteData>
   {
     if (notes.length == 0) return notes;
-    if (timeOffset == null) timeOffset = -Std.int(notes[0].time);
-    return offsetSongNoteData(sortNotes(notes), timeOffset);
+    if (timeOffset == null) timeOffset = Std.int(notes[0].time);
+    return offsetSongNoteData(sortNotes(notes), -timeOffset);
   }
 
   /**
@@ -165,8 +165,8 @@ class SongDataUtils
   public static function buildEventClipboard(events:Array<SongEventData>, ?timeOffset:Int = null):Array<SongEventData>
   {
     if (events.length == 0) return events;
-    if (timeOffset == null) timeOffset = -Std.int(events[0].time);
-    return offsetSongEventData(sortEvents(events), timeOffset);
+    if (timeOffset == null) timeOffset = Std.int(events[0].time);
+    return offsetSongEventData(sortEvents(events), -timeOffset);
   }
 
   /**
diff --git a/source/funkin/play/event/FocusCameraSongEvent.hx b/source/funkin/play/event/FocusCameraSongEvent.hx
index 83c978ba8..847df4a60 100644
--- a/source/funkin/play/event/FocusCameraSongEvent.hx
+++ b/source/funkin/play/event/FocusCameraSongEvent.hx
@@ -135,10 +135,10 @@ class FocusCameraSongEvent extends SongEvent
     return new SongEventSchema([
       {
         name: "char",
-        title: "Character",
+        title: "Target",
         defaultValue: 0,
         type: SongEventFieldType.ENUM,
-        keys: ["Position" => -1, "Boyfriend" => 0, "Dad" => 1, "Girlfriend" => 2]
+        keys: ["Position" => -1, "Player" => 0, "Opponent" => 1, "Girlfriend" => 2]
       },
       {
         name: "x",
@@ -146,6 +146,7 @@ class FocusCameraSongEvent extends SongEvent
         defaultValue: 0,
         step: 10.0,
         type: SongEventFieldType.FLOAT,
+        units: "px"
       },
       {
         name: "y",
@@ -153,6 +154,7 @@ class FocusCameraSongEvent extends SongEvent
         defaultValue: 0,
         step: 10.0,
         type: SongEventFieldType.FLOAT,
+        units: "px"
       }
     ]);
   }
diff --git a/source/funkin/play/event/SetCameraBopSongEvent.hx b/source/funkin/play/event/SetCameraBopSongEvent.hx
index d0e01346f..a82577a5f 100644
--- a/source/funkin/play/event/SetCameraBopSongEvent.hx
+++ b/source/funkin/play/event/SetCameraBopSongEvent.hx
@@ -78,14 +78,16 @@ class SetCameraBopSongEvent extends SongEvent
         title: 'Intensity',
         defaultValue: 1.0,
         step: 0.1,
-        type: SongEventFieldType.FLOAT
+        type: SongEventFieldType.FLOAT,
+        units: 'x'
       },
       {
         name: 'rate',
-        title: 'Rate (beats/zoom)',
+        title: 'Rate',
         defaultValue: 4,
         step: 1,
         type: SongEventFieldType.INTEGER,
+        units: 'beats/zoom'
       }
     ]);
   }
diff --git a/source/funkin/play/event/ZoomCameraSongEvent.hx b/source/funkin/play/event/ZoomCameraSongEvent.hx
index a35a12e1e..809130499 100644
--- a/source/funkin/play/event/ZoomCameraSongEvent.hx
+++ b/source/funkin/play/event/ZoomCameraSongEvent.hx
@@ -106,14 +106,16 @@ class ZoomCameraSongEvent extends SongEvent
         title: 'Zoom Level',
         defaultValue: 1.0,
         step: 0.1,
-        type: SongEventFieldType.FLOAT
+        type: SongEventFieldType.FLOAT,
+        units: 'x'
       },
       {
         name: 'duration',
-        title: 'Duration (in steps)',
+        title: 'Duration',
         defaultValue: 4.0,
         step: 0.5,
         type: SongEventFieldType.FLOAT,
+        units: 'steps'
       },
       {
         name: 'ease',
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 719f6aceb..d0326be30 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -688,6 +688,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
    */
   var activeToolboxes:Map<String, CollapsibleDialog> = new Map<String, CollapsibleDialog>();
 
+  /**
+   * The camera component we're using for this state.
+   */
+  var uiCamera:FlxCamera;
+
   // Audio
 
   /**
@@ -2040,7 +2045,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
     loadPreferences();
 
-    fixCamera();
+    uiCamera = new FlxCamera();
+    FlxG.cameras.reset(uiCamera);
 
     buildDefaultSongData();
 
@@ -5297,7 +5303,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
         Paths.setCurrentLevel('weekend1');
     }
 
-    subStateClosed.add(fixCamera);
+    subStateClosed.add(reviveUICamera);
     subStateClosed.add(resetConductorAfterTest);
 
     FlxTransitionableState.skipNextTransIn = false;
@@ -5322,6 +5328,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     }
     targetState.vocals = audioVocalTrackGroup;
 
+    // Kill and replace the UI camera so it doesn't get destroyed during the state transition.
+    uiCamera.kill();
+    FlxG.cameras.remove(uiCamera, false);
+    FlxG.cameras.reset(new FlxCamera());
+
     this.persistentUpdate = false;
     this.persistentDraw = false;
     stopWelcomeMusic();
@@ -5411,13 +5422,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
   }
 
   /**
-   * Fix a camera issue caused when closing the PlayState used when testing.
+   * Revive the UI camera and re-establish it as the main camera so UI elements depending on it don't explode.
    */
-  function fixCamera(_:FlxSubState = null):Void
+  function reviveUICamera(_:FlxSubState = null):Void
   {
-    FlxG.cameras.reset(new FlxCamera());
-    FlxG.camera.focusOn(new FlxPoint(FlxG.width / 2, FlxG.height / 2));
-    FlxG.camera.zoom = 1.0;
+    uiCamera.revive();
+    FlxG.cameras.reset(uiCamera);
 
     add(this.root);
   }
diff --git a/source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx b/source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx
index 4361f867f..6c5152a29 100644
--- a/source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx
@@ -46,26 +46,14 @@ class CopyItemsCommand implements ChartEditorCommand
 
   function performVisuals(state:ChartEditorState):Void
   {
+    var hasNotes:Bool = false;
+    var hasEvents:Bool = false;
+
+    // Wiggle copied notes.
     if (state.currentNoteSelection.length > 0)
     {
-      // Display the "Copied Notes" text.
-      if (state.txtCopyNotif != null)
-      {
-        state.txtCopyNotif.visible = true;
-        state.txtCopyNotif.text = "Copied " + state.currentNoteSelection.length + " notes to clipboard";
-        state.txtCopyNotif.x = FlxG.mouse.x - (state.txtCopyNotif.width / 2);
-        state.txtCopyNotif.y = FlxG.mouse.y - 16;
-        FlxTween.tween(state.txtCopyNotif, {y: state.txtCopyNotif.y - 32}, 0.5,
-          {
-            type: FlxTween.ONESHOT,
-            ease: FlxEase.quadOut,
-            onComplete: function(_) {
-              state.txtCopyNotif.visible = false;
-            }
-          });
-      }
+      hasNotes = true;
 
-      // Wiggle the notes.
       for (note in state.renderedNotes.members)
       {
         if (state.isNoteSelected(note.noteData))
@@ -91,8 +79,13 @@ class CopyItemsCommand implements ChartEditorCommand
             });
         }
       }
+    }
+
+    // Wiggle copied events.
+    if (state.currentEventSelection.length > 0)
+    {
+      hasEvents = true;
 
-      // Wiggle the events.
       for (event in state.renderedEvents.members)
       {
         if (state.isEventSelected(event.eventData))
@@ -119,6 +112,39 @@ class CopyItemsCommand implements ChartEditorCommand
         }
       }
     }
+
+    // Display the "Copied Notes" text.
+    if ((hasNotes || hasEvents) && state.txtCopyNotif != null)
+    {
+      var copiedString:String = '';
+      if (hasNotes)
+      {
+        var copiedNotes:Int = state.currentNoteSelection.length;
+        copiedString += '${copiedNotes} note';
+        if (copiedNotes > 1) copiedString += 's';
+
+        if (hasEvents) copiedString += ' and ';
+      }
+      if (hasEvents)
+      {
+        var copiedEvents:Int = state.currentEventSelection.length;
+        copiedString += '${state.currentEventSelection.length} event';
+        if (copiedEvents > 1) copiedString += 's';
+      }
+
+      state.txtCopyNotif.visible = true;
+      state.txtCopyNotif.text = 'Copied ${copiedString} to clipboard';
+      state.txtCopyNotif.x = FlxG.mouse.x - (state.txtCopyNotif.width / 2);
+      state.txtCopyNotif.y = FlxG.mouse.y - 16;
+      FlxTween.tween(state.txtCopyNotif, {y: state.txtCopyNotif.y - 32}, 0.5,
+        {
+          type: FlxTween.ONESHOT,
+          ease: FlxEase.quadOut,
+          onComplete: function(_) {
+            state.txtCopyNotif.visible = false;
+          }
+        });
+    }
   }
 
   public function undo(state:ChartEditorState):Void
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx
index fbd1562b4..7b163ad3d 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx
@@ -18,6 +18,7 @@ import haxe.ui.core.Component;
 import funkin.data.event.SongEventRegistry;
 import haxe.ui.components.TextField;
 import haxe.ui.containers.Box;
+import haxe.ui.containers.HBox;
 import haxe.ui.containers.Frame;
 import haxe.ui.events.UIEvent;
 import haxe.ui.data.ArrayDataSource;
@@ -214,7 +215,20 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
           input.text = field.type;
       }
 
-      target.addComponent(input);
+      // Putting in a box so we can add a unit label easily if there is one.
+      var inputBox:HBox = new HBox();
+      inputBox.addComponent(input);
+
+      // Add a unit label if applicable.
+      if (field.units != null && field.units != "")
+      {
+        var units:Label = new Label();
+        units.text = field.units;
+        units.verticalAlign = "center";
+        inputBox.addComponent(units);
+      }
+
+      target.addComponent(inputBox);
 
       // Update the value of the event data.
       input.onChange = function(event:UIEvent) {