Repository: SwitchbackTech/compass
Pull request #218 (2025-01-16 03:46:44)

🐛 Fix: Implement better snapping when dragging events from sidebar to grid
## Description

Fixes [issue](https://github.com/SwitchbackTech/compass/issues/210)

This PR handles implementing more accurate snapping when dragging someday events to the grid.

The issue the PR addresses happens due to a calculation mismatch between 2 methods: `GridEventPreview:getTimePreview()` and `GridEventPreview/styled:getItemStyles()`. This is due to the methods relying on different calculation variables, producing different results in certain mouse movement edge cases.

The PR fixes that by matching the calculation logic inside the `getItemStyles` method with the `getTimePreview` method (it does not do it directly, because as a small refactor, we extracted snapping logic from `getItemStyles` inside its dedicated `snapToGrid` function. See `snapToGrid`)

To achieve that, we needed to feed calculation logic variable dependencies inside the tree, hence, why we needed to prop drill.

This is a common pattern inside the application and it is anti best practice. Ideally we should be using something like React context to feed variables down the tree without prop drilling.

I thought this type of refactor is out of scope of this PR, and I also felt like I was spending too much time on this PR so I wanted to tie things up and consult you from here.

As a side quest I snuck in an extra feature, horizontal snapping! (I took inspiration from google calendar and felt like we should implement it here too)

## Results
Please take a look at the before and after, you will notice the following:
- Event time accuracy issue is resolved
- Horizontal (column based) snapping is implemented

#### Before
[vertical_snap_before.webm](https://github.com/user-attachments/assets/2e727453-6d59-4d84-8f46-681f0160d8dd)


#### After
[vertical_snap_after.webm](https://github.com/user-attachments/assets/3f93afe8-d593-4b4f-9daf-c716ef99b8dd)



Patch:
diff --git a/package.json b/package.json
index 001366f5..91b730c0 100644
--- a/package.json
+++ b/package.json
@@ -46,22 +46,19 @@
     "@babel/preset-react": "^7.16.7",
     "@babel/preset-typescript": "^7.16.0",
     "@types/uuid": "^9.0.0",
-    "@typescript-eslint/eslint-plugin": "^5.0.0",
-    "@typescript-eslint/parser": "^5.0.0",
+    "@typescript-eslint/eslint-plugin": "^8.0.0",
+    "@typescript-eslint/parser": "^8.0.0",
     "babel-loader": "^9.1.0",
     "buffer": "^6.0.3",
     "concurrently": "^8.0.1",
     "cross-env": "^7.0.3",
     "crypto-browserify": "^3.12.0",
-    "eslint": "^8.2.0",
+    "eslint": "^9.18.0",
     "eslint-config-prettier": "^8.3.0",
     "eslint-import-resolver-lerna": "^2.0.0",
-    "eslint-plugin-import": "^2.25.2",
-    "eslint-plugin-jest": "^27.0.4",
-    "eslint-plugin-jsx-a11y": "^6.5.1",
+    "eslint-plugin-import": "^2.3.1",
+    "eslint-plugin-jest": "^28.11.0",
     "eslint-plugin-prettier": "^4.0.0",
-    "eslint-plugin-react": "^7.28.0",
-    "eslint-plugin-react-hooks": "^4.3.0",
     "husky": "^8.0.1",
     "jest": "^29.0.3",
     "lint-staged": "^15.2.10",
diff --git a/packages/web/package.json b/packages/web/package.json
index 59787cfe..497e3f17 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -19,7 +19,6 @@
     "classnames": "^2.3.1",
     "css-loader": "^6.3.0",
     "dayjs": "^1.10.7",
-    "eslint-plugin-testing-library": "^5.0.5",
     "focus-trap-react": "^10.3.0",
     "html-webpack-plugin": "^5.3.2",
     "http-server": "^14.1.0",
@@ -78,8 +77,12 @@
     "babel-jest": "^29.3.1",
     "babel-plugin-styled-components": "^2.0.2",
     "eslint-plugin-jest-dom": "^4.0.3",
+    "eslint-plugin-jsx-a11y": "^6.5.1",
+    "eslint-plugin-react": "^7.28.0",
+    "eslint-plugin-testing-library": "^5.0.5",
+    "eslint-plugin-react-hooks": "^4.3.0",
     "jest": "^29.0.3",
-    "jest-environment-jsdom": "^29.0.3",
+    "jest-environment-jsdom": "^29.7.0",
     "jest-styled-components": "^7.0.5",
     "msw": "^1.0.1",
     "ts-key-enum": "^2.0.12",
diff --git a/packages/web/src/common/types/util.types.ts b/packages/web/src/common/types/util.types.ts
index d2dac363..f87d14cf 100644
--- a/packages/web/src/common/types/util.types.ts
+++ b/packages/web/src/common/types/util.types.ts
@@ -4,7 +4,10 @@ export interface AssignResult {
   fits: boolean;
   rowNum?: number;
 }
-
+export interface Coordinates {
+  x: number;
+  y: number;
+}
 export interface Option_Time {
   label: string;
   value: string;
diff --git a/packages/web/src/views/Calendar/Calendar.tsx b/packages/web/src/views/Calendar/Calendar.tsx
index e52bfbf6..0b4d8616 100644
--- a/packages/web/src/views/Calendar/Calendar.tsx
+++ b/packages/web/src/views/Calendar/Calendar.tsx
@@ -73,6 +73,7 @@ export const CalendarView = () => {
           dateCalcs={dateCalcs}
           measurements={measurements}
           weekProps={weekProps}
+          gridRefs={gridRefs}
         />
       )}
       <StyledCalendar direction={FlexDirections.COLUMN} id={ID_MAIN}>
diff --git a/packages/web/src/views/Calendar/components/Event/Grid/GridEventPreview/GridEventPreview.tsx b/packages/web/src/views/Calendar/components/Event/Grid/GridEventPreview/GridEventPreview.tsx
index 07a31b9a..ca20a5ac 100644
--- a/packages/web/src/views/Calendar/components/Event/Grid/GridEventPreview/GridEventPreview.tsx
+++ b/packages/web/src/views/Calendar/components/Event/Grid/GridEventPreview/GridEventPreview.tsx
@@ -7,10 +7,15 @@ import { AlignItems, FlexWrap } from "@web/components/Flex/styled";
 import { Text } from "@web/components/Text";
 import { SOMEDAY_EVENT_HEIGHT } from "@web/views/Calendar/components/Sidebar/SomedayTab/SomedayEvents/styled";
 import { EVENT_ALLDAY_HEIGHT } from "@web/views/Calendar/layout.constants";
-import { Measurements_Grid } from "@web/views/Calendar/hooks/grid/useGridLayout";
+import {
+  Measurements_Grid,
+  Refs_Grid,
+} from "@web/views/Calendar/hooks/grid/useGridLayout";
 import { WeekProps } from "@web/views/Calendar/hooks/useWeek";
 import { DateCalcs } from "@web/views/Calendar/hooks/grid/useDateCalcs";
 import { SpaceCharacter } from "@web/components/SpaceCharacter";
+import { snapToGrid } from "@web/views/Calendar/components/Event/Grid/GridEventPreview/snap.grid";
+import { MouseCoords } from "@web/views/Calendar/hooks/draft/useMousePosition";
 
 import { getItemStyles, layerStyles, StyledGridEventPreview } from "./styled";
 
@@ -21,8 +26,9 @@ export interface Props {
   isOverAllDayRow: boolean;
   isOverMainGrid: boolean;
   measurements: Measurements_Grid;
-  mouseCoords: { x: number; y: number };
+  mouseCoords: MouseCoords;
   startOfView: WeekProps["component"]["startOfView"];
+  gridScrollRef: Refs_Grid["gridScrollRef"];
 }
 
 export const GridEventPreview: FC<Props> = memo(function GridEventPreview({
@@ -34,6 +40,7 @@ export const GridEventPreview: FC<Props> = memo(function GridEventPreview({
   measurements,
   mouseCoords,
   startOfView,
+  gridScrollRef,
 }) {
   const { colWidths } = measurements;
   const { x, y } = mouseCoords;
@@ -72,9 +79,16 @@ export const GridEventPreview: FC<Props> = memo(function GridEventPreview({
   const height = getHeight();
   const width = getWidth();
 
+  const { x: snappedX, y: snappedY } = snapToGrid(
+    x,
+    y,
+    measurements,
+    gridScrollRef.current?.scrollTop || 0
+  );
+
   return (
     <div style={layerStyles}>
-      <div style={getItemStyles({ x: 0, y: 0 }, { x, y })}>
+      <div style={getItemStyles({ x: snappedX, y: snappedY })}>
         <StyledGridEventPreview
           className={"active"}
           duration={1}
diff --git a/packages/web/src/views/Calendar/components/Event/Grid/GridEventPreview/snap.grid.ts b/packages/web/src/views/Calendar/components/Event/Grid/GridEventPreview/snap.grid.ts
index 83a22206..06f60fa0 100644
--- a/packages/web/src/views/Calendar/components/Event/Grid/GridEventPreview/snap.grid.ts
+++ b/packages/web/src/views/Calendar/components/Event/Grid/GridEventPreview/snap.grid.ts
@@ -1,7 +1,95 @@
-export const snapToGrid = (x: number, y: number): [number, number] => {
-  const yInterval = 10; // hacky way to get snap feel; not accurate
-  const snappedY = Math.round(y / yInterval) * yInterval;
-  const snappedX = x;
+import { roundToPrev } from "@web/common/utils";
+import { Measurements_Grid } from "@web/views/Calendar/hooks/grid/useGridLayout";
+import { GRID_TIME_STEP } from "@web/views/Calendar/layout.constants";
 
-  return [snappedX, snappedY];
+export interface SnappedCoords {
+  x: number;
+  y: number;
+}
+
+// `measurements.mainGrid.left` includes half the width of time column. It does not start at the first day column.
+// due to that, we need to account for its width, otherwise snappedX will be off.
+// Ideally we should fix how `mainGrid.left` is calculated to not include half the width of time column and
+// include only the grid interactivity area (i.e. day columns).
+// For now, we estimate the width of the time column to be 55px
+export const MAIN_GRID_TIME_COLUMN_WIDTH = 55;
+
+// TODO: Draw a step by step diagram to explain how the snapping works? (Might
+// help facilitate quicker understanding of the code)
+const snapYToGrid = (
+  cursorY: number,
+  measurements: Measurements_Grid,
+  scrollTop: number
+): number => {
+  if (!measurements.mainGrid) return cursorY; // TS guard
+
+  // Calculate the cursor's Y position relative to the grid's top and account for scrolling
+  const gridY = cursorY - measurements.mainGrid.top + scrollTop;
+
+  // Convert the grid time interval to a fractional hour (assuming GRID_TIME_STEP is in minutes and will never be larger than 60)
+  const fractionalHour = GRID_TIME_STEP / 60;
+
+  // Calculate the height of a single grid time interval in pixels
+  const intervalHeightInPixels = measurements.hourHeight * fractionalHour;
+
+  // Snap the relative Y position to the nearest grid interval
+  const snappedRelativeY = roundToPrev(gridY, intervalHeightInPixels);
+
+  // Adjust snappedY to position the event relative to the page's viewport
+  const snappedY = snappedRelativeY + measurements.mainGrid.top - scrollTop;
+
+  return snappedY;
+};
+
+const snapXToGrid = (
+  cursorX: number,
+  measurements: Measurements_Grid
+): number => {
+  if (!measurements.mainGrid) return cursorX; // TS guard
+
+  // Calculate the cursor's X position relative to the grid's left and account for scrolling
+  const gridX =
+    cursorX - MAIN_GRID_TIME_COLUMN_WIDTH - measurements.mainGrid.left;
+
+  // Width of a single grid column (right now it appears the width is the same for across all columns, even in
+  // different view ports, so we can reliably use the first column width)
+  const intervalWidthInPixels = measurements.colWidths[0];
+
+  // Snap the relative X position to the nearest grid column
+  const snappedRelativeX = roundToPrev(gridX, intervalWidthInPixels);
+
+  if (snappedRelativeX < 0) {
+    // We are not inside the 'real' main grid area (we are excluding the time column), return cursorX
+    return cursorX;
+  }
+
+  // Adjust snappedX to position the event relative to the grid's left
+  const snappedX =
+    measurements.mainGrid.left + MAIN_GRID_TIME_COLUMN_WIDTH + snappedRelativeX;
+
+  return snappedX;
+};
+
+export const snapToGrid = (
+  cursorX: number,
+  cursorY: number,
+  measurements: Measurements_Grid,
+  scrollTop: number
+): SnappedCoords => {
+  if (!measurements.mainGrid) {
+    return { x: cursorX, y: cursorY };
+  }
+
+  // Check if the cursor is within the bounds of the main grid
+  const isCursorWithinGrid = cursorY > measurements.mainGrid.top;
+
+  let snappedY = cursorY;
+  let snappedX = cursorX;
+
+  if (isCursorWithinGrid) {
+    snappedY = snapYToGrid(cursorY, measurements, scrollTop);
+    snappedX = snapXToGrid(cursorX, measurements);
+  }
+
+  return { x: snappedX, y: snappedY };
 };
diff --git a/packages/web/src/views/Calendar/components/Event/Grid/GridEventPreview/styled.ts b/packages/web/src/views/Calendar/components/Event/Grid/GridEventPreview/styled.ts
index 496bc4d3..52371746 100644
--- a/packages/web/src/views/Calendar/components/Event/Grid/GridEventPreview/styled.ts
+++ b/packages/web/src/views/Calendar/components/Event/Grid/GridEventPreview/styled.ts
@@ -6,28 +6,17 @@ import { ZIndex } from "@web/common/constants/web.constants";
 import { theme } from "@web/common/styles/theme";
 import { hoverColorByPriority } from "@web/common/styles/theme.util";
 
-import { snapToGrid } from "./snap.grid";
-
-export const getItemStyles = (
-  initialOffset: XYCoord | null,
-  currentOffset: XYCoord | null
-) => {
-  if (!initialOffset || !currentOffset) {
+export const getItemStyles = (currentOffset: XYCoord | null) => {
+  if (!currentOffset) {
     return {
       display: "none",
     };
   }
 
-  let { x, y } = currentOffset;
-
-  // snap logic
-  x -= initialOffset.x;
-  y -= initialOffset.y;
-  [x, y] = snapToGrid(x, y);
-  x += initialOffset.x;
-  y += initialOffset.y;
+  const { x, y } = currentOffset;
 
   const transform = `translate(${x}px, ${y}px)`;
+
   return {
     transform,
     WebkitTransform: transform,
diff --git a/packages/web/src/views/Calendar/components/Sidebar/Sidebar.tsx b/packages/web/src/views/Calendar/components/Sidebar/Sidebar.tsx
index 492d68a7..8ca3f0c4 100644
--- a/packages/web/src/views/Calendar/components/Sidebar/Sidebar.tsx
+++ b/packages/web/src/views/Calendar/components/Sidebar/Sidebar.tsx
@@ -1,7 +1,10 @@
 import React, { FC } from "react";
 import { WeekProps } from "@web/views/Calendar/hooks/useWeek";
 import { DateCalcs } from "@web/views/Calendar/hooks/grid/useDateCalcs";
-import { Measurements_Grid } from "@web/views/Calendar/hooks/grid/useGridLayout";
+import {
+  Measurements_Grid,
+  Refs_Grid,
+} from "@web/views/Calendar/hooks/grid/useGridLayout";
 import { useAppSelector } from "@web/store/store.hooks";
 import { selectSidebarTab } from "@web/ducks/events/selectors/view.selectors";
 
@@ -15,12 +18,14 @@ interface Props {
   dateCalcs: DateCalcs;
   measurements: Measurements_Grid;
   weekProps: WeekProps;
+  gridRefs: Refs_Grid;
 }
 
 export const Sidebar: FC<Props & React.HTMLAttributes<HTMLDivElement>> = ({
   dateCalcs,
   measurements,
   weekProps,
+  gridRefs,
 }: Props) => {
   const weekStart = weekProps.component.startOfView;
   const weekEnd = weekProps.component.endOfView;
@@ -42,6 +47,7 @@ export const Sidebar: FC<Props & React.HTMLAttributes<HTMLDivElement>> = ({
             sidebarProps={sidebarProps}
             viewStart={weekStart}
             viewEnd={weekEnd}
+            gridRefs={gridRefs}
           />
         )}
         {tab === "monthWidget" && (
diff --git a/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/MonthSection/MonthSection.tsx b/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/MonthSection/MonthSection.tsx
index 058947a2..46f66697 100644
--- a/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/MonthSection/MonthSection.tsx
+++ b/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/MonthSection/MonthSection.tsx
@@ -2,7 +2,10 @@ import React, { FC } from "react";
 import { Categories_Event } from "@core/types/event.types";
 import { SidebarProps } from "@web/views/Calendar/hooks/draft/sidebar/useSidebar";
 import { DateCalcs } from "@web/views/Calendar/hooks/grid/useDateCalcs";
-import { Measurements_Grid } from "@web/views/Calendar/hooks/grid/useGridLayout";
+import {
+  Measurements_Grid,
+  Refs_Grid,
+} from "@web/views/Calendar/hooks/grid/useGridLayout";
 import { Text } from "@web/components/Text";
 import { WeekProps } from "@web/views/Calendar/hooks/useWeek";
 import { AlignItems, JustifyContent } from "@web/components/Flex/styled";
@@ -16,6 +19,7 @@ interface Props {
   measurements: Measurements_Grid;
   somedayProps: SidebarProps;
   viewStart: WeekProps["component"]["startOfView"];
+  gridRefs: Refs_Grid;
 }
 
 export const MonthSection: FC<Props> = ({
@@ -23,6 +27,7 @@ export const MonthSection: FC<Props> = ({
   measurements,
   somedayProps,
   viewStart,
+  gridRefs,
 }) => {
   const monthLabel = getMonthListLabel(viewStart);
 
@@ -43,6 +48,7 @@ export const MonthSection: FC<Props> = ({
         measurements={measurements}
         sidebarProps={somedayProps}
         viewStart={viewStart}
+        gridScrollRef={gridRefs.gridScrollRef}
       />
     </SidebarSection>
   );
diff --git a/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/SomedayEvents/SomedayEvents.tsx b/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/SomedayEvents/SomedayEvents.tsx
index 1769ddb4..7e1d99a4 100644
--- a/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/SomedayEvents/SomedayEvents.tsx
+++ b/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/SomedayEvents/SomedayEvents.tsx
@@ -7,7 +7,10 @@ import {
   GRID_X_START,
   SIDEBAR_OPEN_WIDTH,
 } from "@web/views/Calendar/layout.constants";
-import { Measurements_Grid } from "@web/views/Calendar/hooks/grid/useGridLayout";
+import {
+  Measurements_Grid,
+  Refs_Grid,
+} from "@web/views/Calendar/hooks/grid/useGridLayout";
 import { SidebarProps } from "@web/views/Calendar/hooks/draft/sidebar/useSidebar";
 import { WeekProps } from "@web/views/Calendar/hooks/useWeek";
 import { GridEventPreview } from "@web/views/Calendar/components/Event/Grid/GridEventPreview/GridEventPreview";
@@ -23,6 +26,7 @@ interface Props {
   measurements: Measurements_Grid;
   sidebarProps: SidebarProps;
   viewStart: WeekProps["component"]["startOfView"];
+  gridScrollRef: Refs_Grid["gridScrollRef"];
 }
 
 export const SomedayEvents: FC<Props> = ({
@@ -31,6 +35,7 @@ export const SomedayEvents: FC<Props> = ({
   measurements,
   sidebarProps,
   viewStart,
+  gridScrollRef,
 }) => {
   const { state, util } = sidebarProps;
   const gridX = state.mouseCoords.x - (SIDEBAR_OPEN_WIDTH + GRID_X_START);
@@ -57,6 +62,7 @@ export const SomedayEvents: FC<Props> = ({
           measurements={measurements}
           mouseCoords={state.mouseCoords}
           startOfView={viewStart}
+          gridScrollRef={gridScrollRef}
         />
       )}
 
diff --git a/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/SomedayTab.tsx b/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/SomedayTab.tsx
index d31908b6..9b23c2a6 100644
--- a/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/SomedayTab.tsx
+++ b/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/SomedayTab.tsx
@@ -5,7 +5,10 @@ import { AbsoluteOverflowLoader } from "@web/components/AbsoluteOverflowLoader";
 import { Divider } from "@web/components/Divider";
 import { useAppSelector } from "@web/store/store.hooks";
 import { DateCalcs } from "@web/views/Calendar/hooks/grid/useDateCalcs";
-import { Measurements_Grid } from "@web/views/Calendar/hooks/grid/useGridLayout";
+import {
+  Measurements_Grid,
+  Refs_Grid,
+} from "@web/views/Calendar/hooks/grid/useGridLayout";
 import { WeekProps } from "@web/views/Calendar/hooks/useWeek";
 import { theme } from "@web/common/styles/theme";
 import { SidebarProps } from "@web/views/Calendar/hooks/draft/sidebar/useSidebar";
@@ -20,6 +23,7 @@ interface Props {
   sidebarProps: SidebarProps;
   viewStart: WeekProps["component"]["startOfView"];
   viewEnd: WeekProps["component"]["endOfView"];
+  gridRefs: Refs_Grid;
 }
 
 export const SomedayTab: FC<Props> = ({
@@ -28,6 +32,7 @@ export const SomedayTab: FC<Props> = ({
   sidebarProps,
   viewEnd,
   viewStart,
+  gridRefs,
 }) => {
   const isProcessing = useAppSelector(selectIsGetSomedayEventsProcessing);
 
@@ -46,6 +51,7 @@ export const SomedayTab: FC<Props> = ({
         sidebarProps={sidebarProps}
         viewStart={viewStart}
         weekLabel={weekLabel}
+        gridRefs={gridRefs}
       />
 
       <Divider
@@ -60,6 +66,7 @@ export const SomedayTab: FC<Props> = ({
         measurements={measurements}
         somedayProps={sidebarProps}
         viewStart={viewStart}
+        gridRefs={gridRefs}
       />
     </SidebarContent>
   );
diff --git a/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/WeekSection/WeekSection.tsx b/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/WeekSection/WeekSection.tsx
index dc2b63c5..2dd53450 100644
--- a/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/WeekSection/WeekSection.tsx
+++ b/packages/web/src/views/Calendar/components/Sidebar/SomedayTab/WeekSection/WeekSection.tsx
@@ -3,7 +3,10 @@ import { Categories_Event } from "@core/types/event.types";
 import { Text } from "@web/components/Text";
 import { DateCalcs } from "@web/views/Calendar/hooks/grid/useDateCalcs";
 import { SidebarProps } from "@web/views/Calendar/hooks/draft/sidebar/useSidebar";
-import { Measurements_Grid } from "@web/views/Calendar/hooks/grid/useGridLayout";
+import {
+  Measurements_Grid,
+  Refs_Grid,
+} from "@web/views/Calendar/hooks/grid/useGridLayout";
 import { WeekProps } from "@web/views/Calendar/hooks/useWeek";
 import { AlignItems, JustifyContent } from "@web/components/Flex/styled";
 
@@ -16,6 +19,7 @@ interface Props {
   sidebarProps: SidebarProps;
   viewStart: WeekProps["component"]["startOfView"];
   weekLabel: string;
+  gridRefs: Refs_Grid;
 }
 
 export const WeekSection: FC<Props> = ({
@@ -24,6 +28,7 @@ export const WeekSection: FC<Props> = ({
   sidebarProps,
   viewStart,
   weekLabel,
+  gridRefs,
 }) => {
   return (
     <SidebarSection>
@@ -42,6 +47,7 @@ export const WeekSection: FC<Props> = ({
         measurements={measurements}
         sidebarProps={sidebarProps}
         viewStart={viewStart}
+        gridScrollRef={gridRefs.gridScrollRef}
       />
     </SidebarSection>
   );
diff --git a/packages/web/src/views/Calendar/hooks/draft/sidebar/useSidebarUtil.ts b/packages/web/src/views/Calendar/hooks/draft/sidebar/useSidebarUtil.ts
index df9d5a2d..19506ce8 100644
--- a/packages/web/src/views/Calendar/hooks/draft/sidebar/useSidebarUtil.ts
+++ b/packages/web/src/views/Calendar/hooks/draft/sidebar/useSidebarUtil.ts
@@ -12,7 +12,6 @@ import {
 } from "@core/types/event.types";
 import { DropResult } from "@hello-pangea/dnd";
 import { ID_SOMEDAY_DRAFT } from "@web/common/constants/web.constants";
-import { getUserId } from "@web/auth/auth.util";
 import { DropResult_ReactDND } from "@web/common/types/dnd.types";
 import { Schema_GridEvent } from "@web/common/types/web.event.types";
 import {
@@ -21,6 +20,8 @@ import {
   prepEvtBeforeSubmit,
 } from "@web/common/utils/event.util";
 import { getX } from "@web/common/utils/grid.util";
+import { getUserId } from "@web/auth/auth.util";
+import { Coordinates } from "@web/common/types/util.types";
 import { draftSlice } from "@web/ducks/events/slices/draft.slice";
 import {
   createEventSlice,
@@ -158,7 +159,7 @@ export const useSidebarUtil = (
 
   const getDatesAfterDroppingOn = (
     target: "mainGrid" | "alldayRow",
-    mouseCoords: { x: number; y: number }
+    mouseCoords: Coordinates
   ) => {
     const x = getX(mouseCoords.x, true);
     const y = mouseCoords.y;
diff --git a/packages/web/src/views/Calendar/hooks/draft/useMousePosition.ts b/packages/web/src/views/Calendar/hooks/draft/useMousePosition.ts
index 41663a01..1f64fda7 100644
--- a/packages/web/src/views/Calendar/hooks/draft/useMousePosition.ts
+++ b/packages/web/src/views/Calendar/hooks/draft/useMousePosition.ts
@@ -4,6 +4,7 @@ import {
   SIDEBAR_X_START,
   GRID_Y_START,
 } from "@web/views/Calendar/layout.constants";
+import { Coordinates } from "@web/common/types/util.types";
 
 export const useMousePosition = (
   isDragging: boolean,
@@ -14,7 +15,7 @@ export const useMousePosition = (
   const [isOverMainGrid, setIsOverMainGrid] = useState(false);
   const [isOverAllDayRow, setIsOverAllDayRow] = useState(false);
 
-  const [mouseCoords, setMouseCoords] = useState({ x: 0, y: 0 });
+  const [mouseCoords, setMouseCoords] = useState<Coordinates>({ x: 0, y: 0 });
 
   const { allDayRow } = measurements;
 
diff --git a/yarn.lock b/yarn.lock
index 88764f40..1a3c4fc2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1270,37 +1270,66 @@
   resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6"
   integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==
 
-"@eslint-community/eslint-utils@^4.2.0":
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
-  integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==
+"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz#d1145bf2c20132d6400495d6df4bf59362fd9d56"
+  integrity sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==
   dependencies:
-    eslint-visitor-keys "^3.3.0"
+    eslint-visitor-keys "^3.4.3"
 
-"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1":
-  version "4.11.1"
-  resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.1.tgz#a547badfc719eb3e5f4b556325e542fbe9d7a18f"
-  integrity sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==
+"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.12.1":
+  version "4.12.1"
+  resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0"
+  integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==
 
-"@eslint/eslintrc@^2.1.4":
-  version "2.1.4"
-  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad"
-  integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==
+"@eslint/config-array@^0.19.0":
+  version "0.19.1"
+  resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.19.1.tgz#734aaea2c40be22bbb1f2a9dac687c57a6a4c984"
+  integrity sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==
+  dependencies:
+    "@eslint/object-schema" "^2.1.5"
+    debug "^4.3.1"
+    minimatch "^3.1.2"
+
+"@eslint/core@^0.10.0":
+  version "0.10.0"
+  resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.10.0.tgz#23727063c21b335f752dbb3a16450f6f9cbc9091"
+  integrity sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==
+  dependencies:
+    "@types/json-schema" "^7.0.15"
+
+"@eslint/eslintrc@^3.2.0":
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.2.0.tgz#57470ac4e2e283a6bf76044d63281196e370542c"
+  integrity sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==
   dependencies:
     ajv "^6.12.4"
     debug "^4.3.2"
-    espree "^9.6.0"
-    globals "^13.19.0"
+    espree "^10.0.1"
+    globals "^14.0.0"
     ignore "^5.2.0"
     import-fresh "^3.2.1"
     js-yaml "^4.1.0"
     minimatch "^3.1.2"
     strip-json-comments "^3.1.1"
 
-"@eslint/js@8.57.1":
-  version "8.57.1"
-  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2"
-  integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==
+"@eslint/js@9.18.0":
+  version "9.18.0"
+  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.18.0.tgz#3356f85d18ed3627ab107790b53caf7e1e3d1e84"
+  integrity sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==
+
+"@eslint/object-schema@^2.1.5":
+  version "2.1.5"
+  resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.5.tgz#8670a8f6258a2be5b2c620ff314a1d984c23eb2e"
+  integrity sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==
+
+"@eslint/plugin-kit@^0.2.5":
+  version "0.2.5"
+  resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz#ee07372035539e7847ef834e3f5e7b79f09e3a81"
+  integrity sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==
+  dependencies:
+    "@eslint/core" "^0.10.0"
+    levn "^0.4.1"
 
 "@floating-ui/core@^1.6.0":
   version "1.6.8"
@@ -1364,24 +1393,33 @@
   resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-2.1.5.tgz#1e13f34976cc542deae92353c01c8b3d7942e9ba"
   integrity sha512-FuzFN+BsHa+7OxbvAERtgBTNeZpUjgM/MIizfVkSCL2/edriN0Hx/DWRCR//aPYwO5QX/YlgLGXk+E3PcfZwjA==
 
-"@humanwhocodes/config-array@^0.13.0":
-  version "0.13.0"
-  resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748"
-  integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==
+"@humanfs/core@^0.19.1":
+  version "0.19.1"
+  resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77"
+  integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==
+
+"@humanfs/node@^0.16.6":
+  version "0.16.6"
+  resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.6.tgz#ee2a10eaabd1131987bf0488fd9b820174cd765e"
+  integrity sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==
   dependencies:
-    "@humanwhocodes/object-schema" "^2.0.3"
-    debug "^4.3.1"
-    minimatch "^3.0.5"
+    "@humanfs/core" "^0.19.1"
+    "@humanwhocodes/retry" "^0.3.0"
 
 "@humanwhocodes/module-importer@^1.0.1":
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
   integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
 
-"@humanwhocodes/object-schema@^2.0.3":
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
-  integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
+"@humanwhocodes/retry@^0.3.0":
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.1.tgz#c72a5c76a9fbaf3488e231b13dc52c0da7bab42a"
+  integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==
+
+"@humanwhocodes/retry@^0.4.1":
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.1.tgz#9a96ce501bc62df46c4031fbd970e3cc6b10f07b"
+  integrity sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==
 
 "@inquirer/checkbox@^2.5.0":
   version "2.5.0"
@@ -1831,7 +1869,7 @@
   resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
   integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
 
-"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8":
+"@nodelib/fs.walk@^1.2.3":
   version "1.2.8"
   resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
   integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
@@ -2274,7 +2312,7 @@
   dependencies:
     "@types/ms" "*"
 
-"@types/estree@^1.0.5":
+"@types/estree@^1.0.5", "@types/estree@^1.0.6":
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
   integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
@@ -2413,7 +2451,7 @@
     "@types/tough-cookie" "*"
     parse5 "^7.0.0"
 
-"@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
+"@types/json-schema@^7.0.15", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
   version "7.0.15"
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
   integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
@@ -2750,30 +2788,30 @@
   dependencies:
     "@types/yargs-parser" "*"
 
-"@typescript-eslint/eslint-plugin@^5.0.0":
-  version "5.62.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db"
-  integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==
+"@typescript-eslint/eslint-plugin@^8.0.0":
+  version "8.20.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz#b47a398e0e551cb008c60190b804394e6852c863"
+  integrity sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A==
   dependencies:
-    "@eslint-community/regexpp" "^4.4.0"
-    "@typescript-eslint/scope-manager" "5.62.0"
-    "@typescript-eslint/type-utils" "5.62.0"
-    "@typescript-eslint/utils" "5.62.0"
-    debug "^4.3.4"
+    "@eslint-community/regexpp" "^4.10.0"
+    "@typescript-eslint/scope-manager" "8.20.0"
+    "@typescript-eslint/type-utils" "8.20.0"
+    "@typescript-eslint/utils" "8.20.0"
+    "@typescript-eslint/visitor-keys" "8.20.0"
     graphemer "^1.4.0"
-    ignore "^5.2.0"
-    natural-compare-lite "^1.4.0"
-    semver "^7.3.7"
-    tsutils "^3.21.0"
+    ignore "^5.3.1"
+    natural-compare "^1.4.0"
+    ts-api-utils "^2.0.0"
 
-"@typescript-eslint/parser@^5.0.0":
-  version "5.62.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7"
-  integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==
+"@typescript-eslint/parser@^8.0.0":
+  version "8.20.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.20.0.tgz#5caf2230a37094dc0e671cf836b96dd39b587ced"
+  integrity sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g==
   dependencies:
-    "@typescript-eslint/scope-manager" "5.62.0"
-    "@typescript-eslint/types" "5.62.0"
-    "@typescript-eslint/typescript-estree" "5.62.0"
+    "@typescript-eslint/scope-manager" "8.20.0"
+    "@typescript-eslint/types" "8.20.0"
+    "@typescript-eslint/typescript-estree" "8.20.0"
+    "@typescript-eslint/visitor-keys" "8.20.0"
     debug "^4.3.4"
 
 "@typescript-eslint/scope-manager@5.62.0":
@@ -2784,21 +2822,34 @@
     "@typescript-eslint/types" "5.62.0"
     "@typescript-eslint/visitor-keys" "5.62.0"
 
-"@typescript-eslint/type-utils@5.62.0":
-  version "5.62.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a"
-  integrity sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==
+"@typescript-eslint/scope-manager@8.20.0":
+  version "8.20.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.20.0.tgz#aaf4198b509fb87a6527c02cfbfaf8901179e75c"
+  integrity sha512-J7+VkpeGzhOt3FeG1+SzhiMj9NzGD/M6KoGn9f4dbz3YzK9hvbhVTmLj/HiTp9DazIzJ8B4XcM80LrR9Dm1rJw==
   dependencies:
-    "@typescript-eslint/typescript-estree" "5.62.0"
-    "@typescript-eslint/utils" "5.62.0"
+    "@typescript-eslint/types" "8.20.0"
+    "@typescript-eslint/visitor-keys" "8.20.0"
+
+"@typescript-eslint/type-utils@8.20.0":
+  version "8.20.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.20.0.tgz#958171d86b213a3f32b5b16b91db267968a4ef19"
+  integrity sha512-bPC+j71GGvA7rVNAHAtOjbVXbLN5PkwqMvy1cwGeaxUoRQXVuKCebRoLzm+IPW/NtFFpstn1ummSIasD5t60GA==
+  dependencies:
+    "@typescript-eslint/typescript-estree" "8.20.0"
+    "@typescript-eslint/utils" "8.20.0"
     debug "^4.3.4"
-    tsutils "^3.21.0"
+    ts-api-utils "^2.0.0"
 
 "@typescript-eslint/types@5.62.0":
   version "5.62.0"
   resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f"
   integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==
 
+"@typescript-eslint/types@8.20.0":
+  version "8.20.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.20.0.tgz#487de5314b5415dee075e95568b87a75a3e730cf"
+  integrity sha512-cqaMiY72CkP+2xZRrFt3ExRBu0WmVitN/rYPZErA80mHjHx/Svgp8yfbzkJmDoQ/whcytOPO9/IZXnOc+wigRA==
+
 "@typescript-eslint/typescript-estree@5.62.0":
   version "5.62.0"
   resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b"
@@ -2812,7 +2863,31 @@
     semver "^7.3.7"
     tsutils "^3.21.0"
 
-"@typescript-eslint/utils@5.62.0", "@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@^5.58.0":
+"@typescript-eslint/typescript-estree@8.20.0":
+  version "8.20.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.20.0.tgz#658cea07b7e5981f19bce5cf1662cb70ad59f26b"
+  integrity sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==
+  dependencies:
+    "@typescript-eslint/types" "8.20.0"
+    "@typescript-eslint/visitor-keys" "8.20.0"
+    debug "^4.3.4"
+    fast-glob "^3.3.2"
+    is-glob "^4.0.3"
+    minimatch "^9.0.4"
+    semver "^7.6.0"
+    ts-api-utils "^2.0.0"
+
+"@typescript-eslint/utils@8.20.0", "@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0":
+  version "8.20.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.20.0.tgz#53127ecd314b3b08836b4498b71cdb86f4ef3aa2"
+  integrity sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==
+  dependencies:
+    "@eslint-community/eslint-utils" "^4.4.0"
+    "@typescript-eslint/scope-manager" "8.20.0"
+    "@typescript-eslint/types" "8.20.0"
+    "@typescript-eslint/typescript-estree" "8.20.0"
+
+"@typescript-eslint/utils@^5.58.0":
   version "5.62.0"
   resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86"
   integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==
@@ -2834,10 +2909,13 @@
     "@typescript-eslint/types" "5.62.0"
     eslint-visitor-keys "^3.3.0"
 
-"@ungap/structured-clone@^1.2.0":
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
-  integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
+"@typescript-eslint/visitor-keys@8.20.0":
+  version "8.20.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.20.0.tgz#2df6e24bc69084b81f06aaaa48d198b10d382bed"
+  integrity sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==
+  dependencies:
+    "@typescript-eslint/types" "8.20.0"
+    eslint-visitor-keys "^4.2.0"
 
 "@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1":
   version "1.12.1"
@@ -3040,11 +3118,16 @@ acorn-walk@^8.0.0, acorn-walk@^8.0.2, acorn-walk@^8.1.1:
   dependencies:
     acorn "^8.11.0"
 
-acorn@^8.0.4, acorn@^8.1.0, acorn@^8.11.0, acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0:
+acorn@^8.0.4, acorn@^8.1.0, acorn@^8.11.0, acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2:
   version "8.12.1"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248"
   integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==
 
+acorn@^8.14.0:
+  version "8.14.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0"
+  integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==
+
 agent-base@6:
   version "6.0.2"
   resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
@@ -3611,6 +3694,13 @@ brace-expansion@^1.1.7:
     balanced-match "^1.0.0"
     concat-map "0.0.1"
 
+brace-expansion@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
+  integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
+  dependencies:
+    balanced-match "^1.0.0"
+
 braces@^3.0.3, braces@~3.0.2:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
@@ -4273,7 +4363,7 @@ cross-fetch@^3.1.6:
   dependencies:
     node-fetch "^2.6.12"
 
-cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
+cross-spawn@^7.0.1, cross-spawn@^7.0.3, cross-spawn@^7.0.6:
   version "7.0.6"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
   integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
@@ -4472,7 +4562,7 @@ debug@2.6.9:
   dependencies:
     ms "2.0.0"
 
-debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4, debug@~4.3.6:
+debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4, debug@~4.3.6:
   version "4.3.7"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52"
   integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==
@@ -4493,6 +4583,13 @@ debug@^3.2.7:
   dependencies:
     ms "^2.1.1"
 
+debug@^4.3.1, debug@^4.3.2:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
+  integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
+  dependencies:
+    ms "^2.1.3"
+
 decimal.js@^10.4.2:
   version "10.4.3"
   resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23"
@@ -4661,13 +4758,6 @@ doctrine@^2.1.0:
   dependencies:
     esutils "^2.0.2"
 
-doctrine@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
-  integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
-  dependencies:
-    esutils "^2.0.2"
-
 dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9:
   version "0.5.16"
   resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
@@ -5080,17 +5170,17 @@ eslint-import-resolver-node@^0.3.9:
     is-core-module "^2.13.0"
     resolve "^1.22.4"
 
-eslint-module-utils@^2.9.0:
+eslint-module-utils@^2.12.0:
   version "2.12.0"
   resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz#fe4cfb948d61f49203d7b08871982b65b9af0b0b"
   integrity sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==
   dependencies:
     debug "^3.2.7"
 
-eslint-plugin-import@^2.25.2:
-  version "2.30.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz#21ceea0fc462657195989dd780e50c92fe95f449"
-  integrity sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==
+eslint-plugin-import@^2.3.1:
+  version "2.31.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz#310ce7e720ca1d9c0bb3f69adfd1c6bdd7d9e0e7"
+  integrity sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==
   dependencies:
     "@rtsao/scc" "^1.1.0"
     array-includes "^3.1.8"
@@ -5100,7 +5190,7 @@ eslint-plugin-import@^2.25.2:
     debug "^3.2.7"
     doctrine "^2.1.0"
     eslint-import-resolver-node "^0.3.9"
-    eslint-module-utils "^2.9.0"
+    eslint-module-utils "^2.12.0"
     hasown "^2.0.2"
     is-core-module "^2.15.1"
     is-glob "^4.0.3"
@@ -5109,6 +5199,7 @@ eslint-plugin-import@^2.25.2:
     object.groupby "^1.0.3"
     object.values "^1.2.0"
     semver "^6.3.1"
+    string.prototype.trimend "^1.0.8"
     tsconfig-paths "^3.15.0"
 
 eslint-plugin-jest-dom@^4.0.3:
@@ -5120,12 +5211,12 @@ eslint-plugin-jest-dom@^4.0.3:
     "@testing-library/dom" "^8.11.1"
     requireindex "^1.2.0"
 
-eslint-plugin-jest@^27.0.4:
-  version "27.9.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-27.9.0.tgz#7c98a33605e1d8b8442ace092b60e9919730000b"
-  integrity sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==
+eslint-plugin-jest@^28.11.0:
+  version "28.11.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-28.11.0.tgz#2641ecb4411941bbddb3d7cf8a8ff1163fbb510e"
+  integrity sha512-QAfipLcNCWLVocVbZW8GimKn5p5iiMcgGbRzz8z/P5q7xw+cNEpYqyzFMtIF/ZgF2HLOyy+dYBut+DoYolvqig==
   dependencies:
-    "@typescript-eslint/utils" "^5.10.0"
+    "@typescript-eslint/utils" "^6.0.0 || ^7.0.0 || ^8.0.0"
 
 eslint-plugin-jsx-a11y@^6.5.1:
   version "6.10.0"
@@ -5200,78 +5291,79 @@ eslint-scope@5.1.1, eslint-scope@^5.1.1:
     esrecurse "^4.3.0"
     estraverse "^4.1.1"
 
-eslint-scope@^7.2.2:
-  version "7.2.2"
-  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f"
-  integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==
+eslint-scope@^8.2.0:
+  version "8.2.0"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.2.0.tgz#377aa6f1cb5dc7592cfd0b7f892fd0cf352ce442"
+  integrity sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==
   dependencies:
     esrecurse "^4.3.0"
     estraverse "^5.2.0"
 
-eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3:
+eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.3:
   version "3.4.3"
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
   integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
 
-eslint@^8.2.0:
-  version "8.57.1"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9"
-  integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==
+eslint-visitor-keys@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45"
+  integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==
+
+eslint@^9.18.0:
+  version "9.18.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.18.0.tgz#c95b24de1183e865de19f607fda6518b54827850"
+  integrity sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==
   dependencies:
     "@eslint-community/eslint-utils" "^4.2.0"
-    "@eslint-community/regexpp" "^4.6.1"
-    "@eslint/eslintrc" "^2.1.4"
-    "@eslint/js" "8.57.1"
-    "@humanwhocodes/config-array" "^0.13.0"
+    "@eslint-community/regexpp" "^4.12.1"
+    "@eslint/config-array" "^0.19.0"
+    "@eslint/core" "^0.10.0"
+    "@eslint/eslintrc" "^3.2.0"
+    "@eslint/js" "9.18.0"
+    "@eslint/plugin-kit" "^0.2.5"
+    "@humanfs/node" "^0.16.6"
     "@humanwhocodes/module-importer" "^1.0.1"
-    "@nodelib/fs.walk" "^1.2.8"
-    "@ungap/structured-clone" "^1.2.0"
+    "@humanwhocodes/retry" "^0.4.1"
+    "@types/estree" "^1.0.6"
+    "@types/json-schema" "^7.0.15"
     ajv "^6.12.4"
     chalk "^4.0.0"
-    cross-spawn "^7.0.2"
+    cross-spawn "^7.0.6"
     debug "^4.3.2"
-    doctrine "^3.0.0"
     escape-string-regexp "^4.0.0"
-    eslint-scope "^7.2.2"
-    eslint-visitor-keys "^3.4.3"
-    espree "^9.6.1"
-    esquery "^1.4.2"
+    eslint-scope "^8.2.0"
+    eslint-visitor-keys "^4.2.0"
+    espree "^10.3.0"
+    esquery "^1.5.0"
     esutils "^2.0.2"
     fast-deep-equal "^3.1.3"
-    file-entry-cache "^6.0.1"
+    file-entry-cache "^8.0.0"
     find-up "^5.0.0"
     glob-parent "^6.0.2"
-    globals "^13.19.0"
-    graphemer "^1.4.0"
     ignore "^5.2.0"
     imurmurhash "^0.1.4"
     is-glob "^4.0.0"
-    is-path-inside "^3.0.3"
-    js-yaml "^4.1.0"
     json-stable-stringify-without-jsonify "^1.0.1"
-    levn "^0.4.1"
     lodash.merge "^4.6.2"
     minimatch "^3.1.2"
     natural-compare "^1.4.0"
     optionator "^0.9.3"
-    strip-ansi "^6.0.1"
-    text-table "^0.2.0"
 
-espree@^9.6.0, espree@^9.6.1:
-  version "9.6.1"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
-  integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==
+espree@^10.0.1, espree@^10.3.0:
+  version "10.3.0"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-10.3.0.tgz#29267cf5b0cb98735b65e64ba07e0ed49d1eed8a"
+  integrity sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==
   dependencies:
-    acorn "^8.9.0"
+    acorn "^8.14.0"
     acorn-jsx "^5.3.2"
-    eslint-visitor-keys "^3.4.1"
+    eslint-visitor-keys "^4.2.0"
 
 esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
   integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
 
-esquery@^1.4.2:
+esquery@^1.5.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7"
   integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==
@@ -5477,6 +5569,17 @@ fast-glob@^3.2.9:
     merge2 "^1.3.0"
     micromatch "^4.0.4"
 
+fast-glob@^3.3.2:
+  version "3.3.3"
+  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
+  integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
+  dependencies:
+    "@nodelib/fs.stat" "^2.0.2"
+    "@nodelib/fs.walk" "^1.2.3"
+    glob-parent "^5.1.2"
+    merge2 "^1.3.0"
+    micromatch "^4.0.8"
+
 fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
@@ -5503,9 +5606,9 @@ fastest-levenshtein@^1.0.12:
   integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==
 
 fastq@^1.6.0:
-  version "1.17.1"
-  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47"
-  integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.18.0.tgz#d631d7e25faffea81887fe5ea8c9010e1b36fee0"
+  integrity sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==
   dependencies:
     reusify "^1.0.4"
 
@@ -5535,12 +5638,12 @@ figures@^3.0.0:
   dependencies:
     escape-string-regexp "^1.0.5"
 
-file-entry-cache@^6.0.1:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
-  integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
+file-entry-cache@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f"
+  integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==
   dependencies:
-    flat-cache "^3.0.4"
+    flat-cache "^4.0.0"
 
 fill-range@^7.1.1:
   version "7.1.1"
@@ -5624,14 +5727,13 @@ find-up@^6.3.0:
     locate-path "^7.1.0"
     path-exists "^5.0.0"
 
-flat-cache@^3.0.4:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee"
-  integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==
+flat-cache@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c"
+  integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==
   dependencies:
     flatted "^3.2.9"
-    keyv "^4.5.3"
-    rimraf "^3.0.2"
+    keyv "^4.5.4"
 
 flat@^5.0.2:
   version "5.0.2"
@@ -5639,9 +5741,9 @@ flat@^5.0.2:
   integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==
 
 flatted@^3.2.9:
-  version "3.3.1"
-  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a"
-  integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.2.tgz#adba1448a9841bec72b42c532ea23dbbedef1a27"
+  integrity sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==
 
 flow-parser@0.*:
   version "0.247.1"
@@ -5845,12 +5947,10 @@ globals@^11.1.0:
   resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
   integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
 
-globals@^13.19.0:
-  version "13.24.0"
-  resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171"
-  integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==
-  dependencies:
-    type-fest "^0.20.2"
+globals@^14.0.0:
+  version "14.0.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
+  integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
 
 globalthis@^1.0.3:
   version "1.0.4"
@@ -6254,7 +6354,7 @@ ieee754@^1.1.13, ieee754@^1.2.1:
   resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
   integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
 
-ignore@^5.2.0:
+ignore@^5.2.0, ignore@^5.3.1:
   version "5.3.2"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
   integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
@@ -6571,11 +6671,6 @@ is-number@^7.0.0:
   resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
   integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
 
-is-path-inside@^3.0.3:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
-  integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
-
 is-plain-obj@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7"
@@ -6873,7 +6968,7 @@ jest-each@^29.7.0:
     jest-util "^29.7.0"
     pretty-format "^29.7.0"
 
-jest-environment-jsdom@^29.0.3:
+jest-environment-jsdom@^29.7.0:
   version "29.7.0"
   resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz#d206fa3551933c3fd519e5dfdb58a0f5139a837f"
   integrity sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==
@@ -7359,7 +7454,7 @@ jws@^4.0.0:
     jwa "^2.0.0"
     safe-buffer "^5.0.1"
 
-keyv@^4.5.3:
+keyv@^4.5.4:
   version "4.5.4"
   resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
   integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==
@@ -7727,7 +7822,7 @@ methods@~1.1.2:
   resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
   integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
 
-micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.7, micromatch@~4.0.8:
+micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.7, micromatch@^4.0.8, micromatch@~4.0.8:
   version "4.0.8"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
   integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
@@ -7803,13 +7898,20 @@ minimalistic-crypto-utils@^1.0.1:
   resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
   integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==
 
-minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
+minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
   integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
   dependencies:
     brace-expansion "^1.1.7"
 
+minimatch@^9.0.4:
+  version "9.0.5"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
+  integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
+  dependencies:
+    brace-expansion "^2.0.1"
+
 minimist@^1.2.0, minimist@^1.2.6:
   version "1.2.8"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
@@ -7973,11 +8075,6 @@ nanoid@^3.3.7:
   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
   integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
 
-natural-compare-lite@^1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4"
-  integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==
-
 natural-compare@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@@ -10248,11 +10345,6 @@ text-hex@1.0.x:
   resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5"
   integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==
 
-text-table@^0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
-  integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
-
 through@^2.3.6:
   version "2.3.8"
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
@@ -10346,6 +10438,11 @@ triple-beam@^1.3.0:
   resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.4.1.tgz#6fde70271dc6e5d73ca0c3b24e2d92afb7441984"
   integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==
 
+ts-api-utils@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.0.0.tgz#b9d7d5f7ec9f736f4d0f09758b8607979044a900"
+  integrity sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==
+
 ts-key-enum@^2.0.12:
   version "2.0.13"
   resolved "https://registry.yarnpkg.com/ts-key-enum/-/ts-key-enum-2.0.13.tgz#527e7b9534ab6568b369ee21a155563358cd1482"
@@ -10463,11 +10560,6 @@ type-detect@4.0.8:
   resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
   integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
 
-type-fest@^0.20.2:
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
-  integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
-
 type-fest@^0.21.3:
   version "0.21.3"
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"


Test patch:
diff --git a/packages/web/src/views/Calendar/components/Event/Grid/GridEventPreview/snap.grid.test.ts b/packages/web/src/views/Calendar/components/Event/Grid/GridEventPreview/snap.grid.test.ts
new file mode 100644
index 00000000..903d9b07
--- /dev/null
+++ b/packages/web/src/views/Calendar/components/Event/Grid/GridEventPreview/snap.grid.test.ts
@@ -0,0 +1,256 @@
+import {
+  MeasureableElement,
+  Measurements_Grid,
+} from "@web/views/Calendar/hooks/grid/useGridLayout";
+
+import { snapToGrid, MAIN_GRID_TIME_COLUMN_WIDTH } from "./snap.grid";
+import { Coordinates } from "@web/common/types/util.types";
+
+// 7 day main grid
+describe("snapToGrid", () => {
+  const measurements: Measurements_Grid = {
+    mainGrid: {
+      top: 100,
+      left: 50,
+      height: 0,
+      width: 0,
+      x: 0,
+      y: 0,
+      bottom: 0,
+      right: 0,
+      toJSON: function () {
+        throw new Error("Function not implemented.");
+      },
+    },
+    hourHeight: 60,
+    colWidths: Array(7).fill(100) as number[],
+    allDayRow: null,
+    remeasure: (elem: MeasureableElement) => {
+      console.log("remeasuring element", elem);
+    },
+  };
+
+  const DEFAULT_X = 105;
+  const DEFAULT_Y = 160;
+  const mouseCoords: Coordinates = { x: DEFAULT_X, y: DEFAULT_Y };
+
+  // Hack to tell TS that measurements.mainGrid is not null in below "it" blocks
+  if (!measurements.mainGrid) return;
+  const { mainGrid: measurementsMainGrid } = measurements;
+
+  // Override main grid
+  const mainGrid = {
+    ...measurementsMainGrid,
+    // Real "left" for main grid. See comment for MAIN_GRID_TIME_COLUMN_WIDTH in snap.grid.ts for why we do this
+    left: measurementsMainGrid.left + MAIN_GRID_TIME_COLUMN_WIDTH,
+  };
+
+  afterEach(() => {
+    // Reset mouse coords incase they were changed
+    mouseCoords.x = DEFAULT_X;
+    mouseCoords.y = DEFAULT_Y;
+  });
+
+  describe("Edge Cases", () => {
+    it("Returns the cursor position if measurements are not available", () => {
+      const result = snapToGrid(
+        mouseCoords.x,
+        mouseCoords.y,
+        {
+          mainGrid: null,
+          allDayRow: null,
+          colWidths: [],
+          hourHeight: 0,
+          remeasure: (elem: MeasureableElement) => {
+            console.log("foo", elem);
+          },
+        },
+        0
+      );
+
+      expect(result).toEqual({
+        x: mouseCoords.x,
+        y: mouseCoords.y,
+      });
+    });
+
+    it("Returns the cursor position if cursor is not within the grid", () => {
+      const result = snapToGrid(mouseCoords.x, mouseCoords.y, measurements, 0);
+
+      expect(result).toEqual({
+        x: mouseCoords.x,
+        y: mouseCoords.y,
+      });
+    });
+  });
+
+  describe("Horizontal Snap", () => {
+    it("Snaps X to correct day column", () => {
+      let result = snapToGrid(mouseCoords.x, mouseCoords.y, measurements, 0);
+
+      const expectedY = mouseCoords.y;
+      const expectedX = mainGrid.left;
+
+      expect(result).toEqual({ x: expectedX, y: expectedY });
+
+      // Simulate moving the mouse to the right very very close to the next
+      // column, but don't cross its boundary. We should remain in the same
+      // column (because we didn't cross the boundary)
+      mouseCoords.x =
+        mouseCoords.x +
+        measurements.colWidths[0] -
+        // "1px". This helps us not cross the boundary
+        1;
+
+      // Expect to still be in the same column
+      result = snapToGrid(mouseCoords.x, mouseCoords.y, measurements, 0);
+
+      expect(result).toEqual({ x: expectedX, y: expectedY });
+    });
+    it("Snaps X to correct day column when scrolled", () => {
+      const scrollTop = measurements.hourHeight * 3;
+
+      let result = snapToGrid(
+        mouseCoords.x,
+        mouseCoords.y,
+        measurements,
+        scrollTop
+      );
+
+      const expectedY = mouseCoords.y;
+      const expectedX = mainGrid.left;
+
+      expect(result).toEqual({ x: expectedX, y: expectedY });
+
+      mouseCoords.x = mouseCoords.x + measurements.colWidths[0] - 1;
+
+      result = snapToGrid(mouseCoords.x, mouseCoords.y, measurements, 0);
+
+      expect(result).toEqual({ x: expectedX, y: expectedY });
+    });
+    it("Correctly updates X to next day column when cursor is moved to the next column", () => {
+      const result = snapToGrid(mouseCoords.x, mouseCoords.y, measurements, 0);
+
+      const expectedY = DEFAULT_Y;
+      const expectedX = mainGrid.left;
+
+      expect(result).toEqual({ x: expectedX, y: expectedY });
+
+      // Move the cursor to the next column
+      mouseCoords.x = mouseCoords.x + measurements.colWidths[0];
+
+      const nextResult = snapToGrid(
+        mouseCoords.x,
+        mouseCoords.y,
+        measurements,
+        0
+      );
+
+      const expectedNextX = expectedX + measurements.colWidths[0];
+
+      expect(nextResult).toEqual({ x: expectedNextX, y: expectedY });
+    });
+    it("Correctly updates X to prev day column when cursor is moved to the prev column", () => {
+      mouseCoords.x = mouseCoords.x + measurements.colWidths[0];
+
+      const result = snapToGrid(mouseCoords.x, mouseCoords.y, measurements, 0);
+
+      const expectedX = mainGrid.left + measurements.colWidths[0];
+
+      expect(result).toEqual({ x: expectedX, y: DEFAULT_Y });
+
+      // Move the cursor to the previous column
+      mouseCoords.x = mouseCoords.x - measurements.colWidths[0];
+
+      const nextResult = snapToGrid(
+        mouseCoords.x,
+        mouseCoords.y,
+        measurements,
+        0
+      );
+
+      const expectedNextX = expectedX - measurements.colWidths[0];
+
+      expect(nextResult).toEqual({ x: expectedNextX, y: DEFAULT_Y });
+    });
+  });
+  describe("Vertical Snap", () => {
+    it("Snaps Y to correct time interval", () => {
+      const result = snapToGrid(mouseCoords.x, mouseCoords.y, measurements, 0);
+
+      const expectedX = mainGrid.left;
+
+      expect(result).toEqual({ x: expectedX, y: DEFAULT_Y });
+
+      // Move the cursor down a little
+      mouseCoords.y = mouseCoords.y + 5;
+
+      // Expect result to still be the same
+      expect(result).toEqual({ x: expectedX, y: DEFAULT_Y });
+    });
+    it("Snaps Y to correct time interval when scrolled", () => {
+      // Scroll down a few hour rows. This can be changed to any number and we will
+      // still always get the same result as long as we scroll down hour rows consistently.
+      const HOURS_TO_SCROLL = 3;
+      const scrollTop = measurements.hourHeight * HOURS_TO_SCROLL;
+
+      const result = snapToGrid(
+        mouseCoords.x,
+        mouseCoords.y,
+        measurements,
+        scrollTop
+      );
+
+      const expectedY = DEFAULT_Y;
+      const expectedX = mainGrid.left;
+
+      expect(result).toEqual({ x: expectedX, y: expectedY });
+    });
+
+    it("Correctly updates Y to next time interval when cursor is moved to the next interval", () => {
+      const result = snapToGrid(mouseCoords.x, mouseCoords.y, measurements, 0);
+
+      const expectedX = mainGrid.left;
+
+      expect(result).toEqual({ x: expectedX, y: DEFAULT_Y });
+
+      mouseCoords.y = mouseCoords.y + measurements.hourHeight;
+
+      const nextResult = snapToGrid(
+        mouseCoords.x,
+        mouseCoords.y,
+        measurements,
+        0
+      );
+
+      const expectedNextY = DEFAULT_Y + measurements.hourHeight;
+
+      expect(nextResult).toEqual({ x: expectedX, y: expectedNextY });
+    });
+
+    it("Correctly updates Y to prev time interval when cursor is moved to the prev interval", () => {
+      mouseCoords.y = mouseCoords.y + measurements.hourHeight;
+
+      const result = snapToGrid(mouseCoords.x, mouseCoords.y, measurements, 0);
+
+      const expectedY = DEFAULT_Y + measurements.hourHeight;
+      const expectedX = mainGrid.left;
+
+      expect(result).toEqual({ x: expectedX, y: expectedY });
+
+      // Move the cursor up to the previous time interval
+      mouseCoords.y = mouseCoords.y - measurements.hourHeight;
+
+      const nextResult = snapToGrid(
+        mouseCoords.x,
+        mouseCoords.y,
+        measurements,
+        0
+      );
+
+      const expectedNextY = expectedY - measurements.hourHeight;
+
+      expect(nextResult).toEqual({ x: expectedX, y: expectedNextY });
+    });
+  });
+});