Repository: dermotduffy/frigate-hass-card (ts)

Issue:
UX: Change Default TimelinePanBehavior = 'seek' to 'pan'
**Is your feature request related to a problem? Please describe.**
My problem is on mobile, in the Home Assistant App, it's hard to use the mini-timeline when added to live view cards to pan around the timeline (when the default is set to seek). When the default is set to seek, most any clicks link you over to watch the clips. When the default is in pan mode, you can actually navigate around the timeline, then select & view the clip you'd like to watch.

**Describe the solution you'd like**
[frigate-hass-card/src/components/timeline-core.ts : line 209](https://github.com/dermotduffy/frigate-hass-card/blob/3b82dd6843d9a619d8e35151489dd90066a9b746/src/components/timeline-core.ts#L209)
```
...
  public itemClickAction?: TimelineItemClickAction;

  @state()
  protected _panBehavior: TimelinePanBehavior = 'seek';

  protected _targetBarVisible = false;
...
```

Change this from 'seek' to 'pan'

**Describe alternatives you've considered**
I've played around with seek & seek-in-media and those don't allow clicking on timeline with out the hass-card navigating away from live-view. I think pan is better.

I know you should be able to change this via the small icon next to the calendar in the bottom right corner of the timeline...problem is the click event handler seems to overlay on top of those icons when using mobile and it's hard to click (seems impossible actually)

Also even if I set this icon to 'pan' instead of one of the 'seek's...I don't believe this change would survive a reload of the page or app. 

Alternatively, this could be a setting in the hass-card options...but I still think default 'pan' is the best default behavior and thus should simply just be changed to the default for UX reasons.


Pull request:
Add ability to set timeline pan mode via config
* Closes #1343
* Also sets the default mode to `pan` instead of `seek` since it's more performant and less jarring (esp. on mobile).

Patch:
diff --git a/README.md b/README.md
index 2ad37a81..eed4af83 100644
--- a/README.md
+++ b/README.md
@@ -632,6 +632,7 @@ live:
 | Option | Default | Overridable | Description |
 | - | - | - | - |
 | `mode` | `none` | :white_check_mark: | Whether to show the thumbnail carousel `below` the media, `above` the media, in a drawer to the `left` or `right` of the media or to hide it entirely (`none`).|
+| `pan_mode` | `pan`| :white_check_mark: | If set to `pan` then dragging the timeline will have no effect on the media. If set to `seek` will open media for that time and seek to that position . If set to `seek-in-media` will seek within the opened media only. If set to `seek-in-camera` will seek within the selected camera only. |
 | `style` | `ribbon` | :white_check_mark: | Whether the timeline should show events as a single flat `ribbon` or a `stack` of events that are clustered using the `clustering_threshold` (below). |
 | `window_seconds` | `3600` | :white_check_mark: | The length of the default timeline in seconds. By default, 1 hour (`3600` seconds) is shown in the timeline. |
 | `clustering_threshold` | `3` | :white_check_mark: | The minimum number of overlapping events to allow prior to clustering/grouping them. Higher numbers cause clustering to happen less frequently. Depending on the timescale/zoom of the timeline, the underlying timeline library may still allow overlaps for low values of this parameter -- for a fully "flat" timeline use the `ribbon` style. `0` disables clustering entirely. Only used in the `stack` style of timeline. |
@@ -814,7 +815,9 @@ media_viewer:
 
 | Option | Default | Overridable | Description |
 | - | - | - | - |
-| `mode` | `none` | :heavy_multiplication_x: | Whether to show the thumbnail carousel `below` the media, `above` the media, in a drawer to the `left` or `right` of the media or to hide it entirely (`none`).|
+| `mode` | `none` | :heavy_multiplication_x: | Whether to show the thumbnail carousel `below` the media, `above` the media, in a drawer to the 
+`left` or `right` of the media or to hide it entirely (`none`).|
+| `pan_mode` | `pan`| :white_check_mark: | If set to `pan` then dragging the timeline will have no effect on the media. If set to `seek` will open media for that time and seek to that position . If set to `seek-in-media` will seek within the opened media only. If set to `seek-in-camera` will seek within the selected camera only. |
 | `style` | `ribbon` | :heavy_multiplication_x: | Whether the timeline should show events as a single flat `ribbon` or a `stack` of events that are clustered using the `clustering_threshold` (below). |
 | `window_seconds` | `3600` | :heavy_multiplication_x: | The length of the default timeline in seconds. By default, 1 hour (`3600` seconds) is shown in the timeline. |
 | `clustering_threshold` | `3` | :heavy_multiplication_x: | The minimum number of overlapping events to allow prior to clustering/grouping them. Higher numbers cause clustering to happen less frequently. Depending on the timescale/zoom of the timeline, the underlying timeline library may still allow overlaps for low values of this parameter -- for a fully "flat" timeline use the `ribbon` style. `0` disables clustering entirely. Only used in the `stack` style of timeline. |
@@ -2139,6 +2142,7 @@ live:
     timeline:
       style: ribbon
       mode: none
+      pan_mode: pan
       clustering_threshold: 3
       events_media_type: all
       show_recordings: true
@@ -2232,6 +2236,7 @@ media_viewer:
     timeline:
       style: ribbon
       mode: none
+      pan_mode: pan
       clustering_threshold: 3
       events_media_type: all
       show_recordings: true
diff --git a/src/components/timeline-core.ts b/src/components/timeline-core.ts
index a7ed952d..ba20b89b 100644
--- a/src/components/timeline-core.ts
+++ b/src/components/timeline-core.ts
@@ -39,6 +39,7 @@ import {
   FrigateCardView,
   ThumbnailsControlBaseConfig,
   TimelineCoreConfig,
+  TimelinePanMode,
 } from '../config/types';
 import { localize } from '../localize/localize';
 import timelineCoreStyle from '../scss/timeline-core.scss';
@@ -86,11 +87,9 @@ interface TimelineRangeChange extends TimelineWindow {
 
 interface TimelineViewContext {
   window?: TimelineWindow;
-  panBehavior?: TimelinePanBehavior;
 }
 
 type TimelineItemClickAction = 'play' | 'select';
-type TimelinePanBehavior = 'pan' | 'seek' | 'seek-in-media' | 'seek-in-camera';
 
 declare module 'view' {
   interface ViewContext {
@@ -214,7 +213,7 @@ export class FrigateCardTimelineCore extends LitElement {
   public itemClickAction?: TimelineItemClickAction;
 
   @state()
-  protected _panBehavior: TimelinePanBehavior = 'seek';
+  protected _panMode: TimelinePanMode | null = null;
 
   protected _targetBarVisible = false;
 
@@ -288,20 +287,22 @@ export class FrigateCardTimelineCore extends LitElement {
     }
 
     const capabilities = this.cameraManager?.getAggregateCameraCapabilities(cameraIDs);
+    const panMode = this._getEffectivePanMode();
+
     const panTitle =
-      this._panBehavior === 'pan'
-        ? localize('timeline.pan_behavior.pan')
-        : this._panBehavior === 'seek'
-        ? localize('timeline.pan_behavior.seek')
-        : this._panBehavior === 'seek-in-media'
-        ? localize('timeline.pan_behavior.seek-in-media')
-        : localize('timeline.pan_behavior.seek-in-camera');
+      panMode === 'pan'
+        ? localize('config.common.controls.timeline.pan_modes.pan')
+        : panMode === 'seek'
+        ? localize('config.common.controls.timeline.pan_modes.seek')
+        : panMode === 'seek-in-media'
+        ? localize('config.common.controls.timeline.pan_modes.seek-in-media')
+        : localize('config.common.controls.timeline.pan_modes.seek-in-camera');
     const panIcon =
-      this._panBehavior === 'pan'
+      panMode === 'pan'
         ? 'mdi:pan-horizontal'
-        : this._panBehavior === 'seek'
+        : panMode === 'seek'
         ? 'mdi:filmstrip-box-multiple'
-        : this._panBehavior === 'seek-in-media'
+        : panMode === 'seek-in-media'
         ? 'mdi:play-box-lock'
         : 'mdi:camera-lock';
 
@@ -318,12 +319,12 @@ export class FrigateCardTimelineCore extends LitElement {
               ? html` <ha-icon
                   .icon=${panIcon}
                   @click=${() => {
-                    this._panBehavior =
-                      this._panBehavior === 'pan'
+                    this._panMode =
+                      panMode === 'pan'
                         ? 'seek'
-                        : this._panBehavior === 'seek'
+                        : panMode === 'seek'
                         ? 'seek-in-media'
-                        : this._panBehavior === 'seek-in-media'
+                        : panMode === 'seek-in-media'
                         ? 'seek-in-camera'
                         : 'pan';
                   }}
@@ -404,15 +405,15 @@ export class FrigateCardTimelineCore extends LitElement {
       return;
     }
 
+    const panMode = this._getEffectivePanMode();
     const targetBarOn =
       this._shouldSupportSeeking() &&
-      (this._panBehavior === 'seek' ||
-        this._panBehavior === 'seek-in-camera' ||
-        (this._panBehavior === 'seek-in-media' &&
+      (panMode === 'seek' ||
+        ((panMode === 'seek-in-camera' || panMode === 'seek-in-media') &&
           this._timeline.getSelection().some((id) => {
             const item = this._timelineSource?.dataset?.get(id);
             return (
-              this._panBehavior !== 'seek-in-camera' ||
+              panMode !== 'seek-in-camera' ||
                 item?.media?.getCameraID() === this.view?.camera,
               item &&
                 item.start &&
@@ -473,6 +474,7 @@ export class FrigateCardTimelineCore extends LitElement {
   ): Promise<void> {
     const results = this.view?.queryResults;
     const media = results?.getResults();
+    const panMode = this._getEffectivePanMode();
     if (
       !media ||
       !results ||
@@ -480,7 +482,7 @@ export class FrigateCardTimelineCore extends LitElement {
       !this.view ||
       !this.hass ||
       !this.cameraManager ||
-      this._panBehavior === 'pan'
+      panMode === 'pan'
     ) {
       return;
     }
@@ -488,7 +490,7 @@ export class FrigateCardTimelineCore extends LitElement {
     const canSeek = this._shouldSupportSeeking();
     let newResults: MediaQueriesResults | null = null;
 
-    if (this._panBehavior === 'seek') {
+    if (panMode === 'seek') {
       newResults = results
         .clone()
         .resetSelectedResult()
@@ -499,7 +501,7 @@ export class FrigateCardTimelineCore extends LitElement {
             main: true,
           },
         );
-    } else if (this._panBehavior === 'seek-in-camera') {
+    } else if (panMode === 'seek-in-camera') {
       newResults = results
         .clone()
         .resetSelectedResult()
@@ -507,7 +509,7 @@ export class FrigateCardTimelineCore extends LitElement {
           cameraID: this.view.camera,
         })
         .promoteCameraSelectionToMainSelection(this.view.camera);
-    } else if (this._panBehavior === 'seek-in-media') {
+    } else if (panMode === 'seek-in-media') {
       newResults = results;
     }
 
@@ -531,6 +533,10 @@ export class FrigateCardTimelineCore extends LitElement {
       .dispatchChangeEvent(this);
   }
 
+  protected _getEffectivePanMode(): TimelinePanMode {
+    return this._panMode ?? this.timelineConfig?.pan_mode ?? 'pan';
+  }
+
   /**
    * Called whenever the timeline is clicked.
    * @param properties The properties of the timeline click event.
@@ -988,10 +994,6 @@ export class FrigateCardTimelineCore extends LitElement {
         : null;
     const context = this.view.context?.timeline;
 
-    if (context && context.panBehavior) {
-      this._panBehavior = context.panBehavior;
-    }
-
     if (context && context.window) {
       desiredWindow = context.window;
     } else if (mediaWindow && !rangesOverlap(mediaWindow, timelineWindow)) {
@@ -1107,7 +1109,6 @@ export class FrigateCardTimelineCore extends LitElement {
     return {
       timeline: {
         ...this.view?.context?.timeline,
-        panBehavior: this._panBehavior,
         ...(newWindow && { window: newWindow }),
       },
     };
diff --git a/src/config/types.ts b/src/config/types.ts
index c6086fa0..d241c793 100644
--- a/src/config/types.ts
+++ b/src/config/types.ts
@@ -727,8 +727,17 @@ const timelineCoreConfigDefault = {
   window_seconds: 60 * 60,
   show_recordings: true,
   style: 'stack' as const,
+  pan_mode: 'pan' as const,
 };
 
+export const timelinePanModeSchema = z.enum([
+  'pan',
+  'seek',
+  'seek-in-media',
+  'seek-in-camera',
+]);
+export type TimelinePanMode = z.infer<typeof timelinePanModeSchema>;
+
 const timelineCoreConfigSchema = z.object({
   clustering_threshold: z
     .number()
@@ -748,6 +757,9 @@ const timelineCoreConfigSchema = z.object({
     .optional()
     .default(timelineCoreConfigDefault.show_recordings),
   style: z.enum(['stack', 'ribbon']).optional().default(timelineCoreConfigDefault.style),
+  pan_mode: timelinePanModeSchema
+    .optional()
+    .default(timelineCoreConfigDefault.pan_mode),
 });
 export type TimelineCoreConfig = z.infer<typeof timelineCoreConfigSchema>;
 
diff --git a/src/const.ts b/src/const.ts
index 1dd10870..fdb6237c 100644
--- a/src/const.ts
+++ b/src/const.ts
@@ -163,6 +163,8 @@ export const CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE =
   `${CONF_MEDIA_VIEWER}.controls.timeline.events_media_type` as const;
 export const CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_MODE =
   `${CONF_MEDIA_VIEWER}.controls.timeline.mode` as const;
+export const CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_PAN_MODE =
+  `${CONF_MEDIA_VIEWER}.controls.timeline.pan_mode` as const;
 export const CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS =
   `${CONF_MEDIA_VIEWER}.controls.timeline.show_recordings` as const;
 export const CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_STYLE =
@@ -219,6 +221,8 @@ export const CONF_LIVE_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE =
   `${CONF_LIVE}.controls.timeline.events_media_type` as const;
 export const CONF_LIVE_CONTROLS_TIMELINE_MODE =
   `${CONF_LIVE}.controls.timeline.mode` as const;
+export const CONF_LIVE_CONTROLS_TIMELINE_PAN_MODE =
+  `${CONF_LIVE}.controls.timeline.pan_mode` as const;
 export const CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS =
   `${CONF_LIVE}.controls.timeline.show_recordings` as const;
 export const CONF_LIVE_CONTROLS_TIMELINE_STYLE =
diff --git a/src/editor.ts b/src/editor.ts
index b9f9493a..415d5e56 100644
--- a/src/editor.ts
+++ b/src/editor.ts
@@ -91,6 +91,7 @@ import {
   CONF_LIVE_CONTROLS_THUMBNAILS_EVENTS_MEDIA_TYPE,
   CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA_TYPE,
   CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
+  CONF_LIVE_CONTROLS_TIMELINE_PAN_MODE,
   CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
   CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
   CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
@@ -140,6 +141,7 @@ import {
   CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_CLUSTERING_THRESHOLD,
   CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE,
   CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_MODE,
+  CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_PAN_MODE,
   CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS,
   CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_STYLE,
   CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_WINDOW_SECONDS,
@@ -719,6 +721,26 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
     },
   ];
 
+  protected _timelinePanModes: EditorSelectOption[] = [
+    { value: '', label: '' },
+    {
+      value: 'pan',
+      label: localize('config.common.controls.timeline.pan_modes.pan'),
+    },
+    {
+      value: 'seek',
+      label: localize('config.common.controls.timeline.pan_modes.seek'),
+    },
+    {
+      value: 'seek-in-media',
+      label: localize('config.common.controls.timeline.pan_modes.seek-in-media'),
+    },
+    {
+      value: 'seek-in-camera',
+      label: localize('config.common.controls.timeline.pan_modes.seek-in-camera'),
+    },
+  ];
+
   public setConfig(config: RawFrigateCardConfig): void {
     // Note: This does not use Zod to parse the configuration, so it may be
     // partially or completely invalid. It's more useful to have a partially
@@ -1197,30 +1219,34 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
     configPathTimelineEventsMediaType: string,
     configPathShowRecordings: string,
     defaultShowRecordings: boolean,
+    configPathPanMode?: string,
   ): TemplateResult {
-    return html` ${this._renderOptionSelector(
-      configPathStyle,
-      this._timelineStyleTypes,
-      {
+    return html`
+      ${this._renderOptionSelector(configPathStyle, this._timelineStyleTypes, {
         label: localize(`config.common.${CONF_TIMELINE_STYLE}`),
-      },
-    )}
-    ${this._renderNumberInput(configPathWindowSeconds, {
-      label: localize(`config.common.${CONF_TIMELINE_WINDOW_SECONDS}`),
-    })}
-    ${this._renderNumberInput(configPathClusteringThreshold, {
-      label: localize(`config.common.${CONF_TIMELINE_CLUSTERING_THRESHOLD}`),
-    })}
-    ${this._renderOptionSelector(
-      configPathTimelineEventsMediaType,
-      this._timelineEventsMediaTypes,
-      {
-        label: localize(`config.common.${CONF_TIMELINE_EVENTS_MEDIA_TYPE}`),
-      },
-    )}
-    ${this._renderSwitch(configPathShowRecordings, defaultShowRecordings, {
-      label: localize(`config.common.${CONF_TIMELINE_SHOW_RECORDINGS}`),
-    })}`;
+      })}
+      ${configPathPanMode
+        ? this._renderOptionSelector(configPathPanMode, this._timelinePanModes, {
+            label: localize(`config.common.controls.timeline.pan_mode`),
+          })
+        : ``}
+      ${this._renderNumberInput(configPathWindowSeconds, {
+        label: localize(`config.common.${CONF_TIMELINE_WINDOW_SECONDS}`),
+      })}
+      ${this._renderNumberInput(configPathClusteringThreshold, {
+        label: localize(`config.common.${CONF_TIMELINE_CLUSTERING_THRESHOLD}`),
+      })}
+      ${this._renderOptionSelector(
+        configPathTimelineEventsMediaType,
+        this._timelineEventsMediaTypes,
+        {
+          label: localize(`config.common.${CONF_TIMELINE_EVENTS_MEDIA_TYPE}`),
+        },
+      )}
+      ${this._renderSwitch(configPathShowRecordings, defaultShowRecordings, {
+        label: localize(`config.common.${CONF_TIMELINE_SHOW_RECORDINGS}`),
+      })}
+    `;
   }
 
   /**
@@ -1241,6 +1267,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
     configPathTimelineEventsMediaType: string,
     configPathShowRecordings: string,
     showRecordingsDefault: boolean,
+    configPathPanMode: string,
   ): TemplateResult | void {
     return this._putInSubmenu(
       domain,
@@ -1257,6 +1284,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
         configPathTimelineEventsMediaType,
         configPathShowRecordings,
         showRecordingsDefault,
+        configPathPanMode,
       )}`,
     );
   }
@@ -2208,6 +2236,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
                       CONF_LIVE_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE,
                       CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS,
                       this._defaults.live.controls.timeline.show_recordings,
+                      CONF_LIVE_CONTROLS_TIMELINE_PAN_MODE,
                     )}
                     ${this._putInSubmenu(
                       MENU_LIVE_CONTROLS_PTZ,
@@ -2394,6 +2423,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
                     CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE,
                     CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS,
                     this._defaults.media_viewer.controls.timeline.show_recordings,
+                    CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_PAN_MODE,
                   )}
                 `,
               )}
diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json
index 6e41fb24..25407e9c 100644
--- a/src/localize/languages/en.json
+++ b/src/localize/languages/en.json
@@ -173,6 +173,13 @@
             "above": "Above",
             "below": "Below",
             "none": "None"
+          },
+          "pan_mode": "Default pan mode",
+          "pan_modes": {
+            "pan": "Pan",
+            "seek": "Pan seeks across all media",
+            "seek-in-camera": "Pan seeks within selected camera only",
+            "seek-in-media": "Pan seeks within selected media item only"
           }
         },
         "title": {
@@ -589,12 +596,6 @@
     "timeline": "See media in timeline"
   },
   "timeline": {
-    "pan_behavior": {
-      "pan": "Pan",
-      "seek": "Pan seeks across all media",
-      "seek-in-camera": "Pan seeks within selected camera only",
-      "seek-in-media": "Pan seeks within selected media item only"
-    },
     "select_date": "Choose date"
   }
-}
\ No newline at end of file
+}
diff --git a/src/localize/languages/it.json b/src/localize/languages/it.json
index 76f7fa6c..94cb26f5 100644
--- a/src/localize/languages/it.json
+++ b/src/localize/languages/it.json
@@ -28,8 +28,8 @@
         "editor_label": "Opzioni di dipendenza"
       },
       "dimensions": {
-        "editor_label": "",
         "aspect_ratio": "",
+        "editor_label": "",
         "layout": {
           "fit": "Adatta al layout",
           "fits": {
@@ -172,6 +172,13 @@
             "above": "sopra",
             "below": "sotto",
             "none": "sessuna"
+          },
+          "pan_mode": "",
+          "pan_modes": {
+            "pan": "",
+            "seek": "",
+            "seek-in-camera": "",
+            "seek-in-media": ""
           }
         },
         "title": {
@@ -577,12 +584,6 @@
     "timeline": "Vedi evento nella timeline"
   },
   "timeline": {
-    "pan_behavior": {
-      "pan": "",
-      "seek": "",
-      "seek-in-camera": "",
-      "seek-in-media": ""
-    },
     "select_date": "Scegli la data"
   }
 }
diff --git a/src/localize/languages/pt-BR.json b/src/localize/languages/pt-BR.json
index 0243437f..85ee08a3 100644
--- a/src/localize/languages/pt-BR.json
+++ b/src/localize/languages/pt-BR.json
@@ -28,8 +28,8 @@
         "editor_label": "Opções de dependência"
       },
       "dimensions": {
-        "editor_label": "",
         "aspect_ratio": "",
+        "editor_label": "",
         "layout": {
           "fit": "Ajuste de layout",
           "fits": {
@@ -172,6 +172,13 @@
             "above": "Acima",
             "below": "Abaixo",
             "none": "Nenhum"
+          },
+          "pan_mode": "",
+          "pan_modes": {
+            "pan": "",
+            "seek": "",
+            "seek-in-camera": "",
+            "seek-in-media": ""
           }
         },
         "title": {
@@ -587,12 +594,6 @@
     "timeline": "Ver evento na linha do tempo"
   },
   "timeline": {
-    "pan_behavior": {
-      "pan": "",
-      "seek": "",
-      "seek-in-camera": "",
-      "seek-in-media": ""
-    },
     "select_date": "Escolha a data"
   }
 }
diff --git a/src/localize/languages/pt-PT.json b/src/localize/languages/pt-PT.json
index 00d9b704..859fa85f 100644
--- a/src/localize/languages/pt-PT.json
+++ b/src/localize/languages/pt-PT.json
@@ -28,8 +28,8 @@
         "editor_label": "Opções de dependência"
       },
       "dimensions": {
-        "editor_label": "",
         "aspect_ratio": "",
+        "editor_label": "",
         "layout": {
           "fit": "Fit",
           "fits": {
@@ -102,9 +102,9 @@
         "editor_label": "Opções de activação",
         "entities": "Activar a partir de outras entidades",
         "events": {
+          "clips": "",
           "editor_label": "",
           "events": "",
-          "clips": "",
           "snapshots": ""
         },
         "motion": "Activar detectando automaticamente o sensor de movimento",
@@ -172,6 +172,13 @@
             "above": "Por cima",
             "below": "Abaixo",
             "none": "Nenhum"
+          },
+          "pan_mode": "",
+          "pan_modes": {
+            "pan": "",
+            "seek": "",
+            "seek-in-camera": "",
+            "seek-in-media": ""
           }
         },
         "title": {
@@ -569,12 +576,6 @@
     "timeline": "Ver evento na linha do tempo"
   },
   "timeline": {
-    "pan_behavior": {
-      "pan": "",
-      "seek": "",
-      "seek-in-camera": "",
-      "seek-in-media": ""
-    },
     "select_date": "Selecionar a data"
   }
 }
diff --git a/vite.config.ts b/vite.config.ts
index 29404bb3..2c0b04a2 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -80,13 +80,13 @@ export default defineConfig({
 
         // Expected thresholds for anything that does not have 100% coverage
         // yet.
-        statements: 10.67,
+        statements: 10.69,
         branches: 4.9,
         functions: 7.81,
-        lines: 10.92,
+        lines: 10.94,
 
         ...calculateFullCoverageThresholds(),
       },
     },
   },
-});
+});
\ No newline at end of file


Test patch:
diff --git a/tests/config/types.test.ts b/tests/config/types.test.ts
index c3b2d4c2..9ce96b3e 100644
--- a/tests/config/types.test.ts
+++ b/tests/config/types.test.ts
@@ -91,6 +91,7 @@ describe('config defaults', () => {
             clustering_threshold: 3,
             events_media_type: 'all',
             mode: 'none',
+            pan_mode: 'pan',
             show_recordings: true,
             style: 'ribbon',
             window_seconds: 3600,
@@ -147,6 +148,7 @@ describe('config defaults', () => {
             clustering_threshold: 3,
             events_media_type: 'all',
             mode: 'none',
+            pan_mode: 'pan',
             show_recordings: true,
             style: 'ribbon',
             window_seconds: 3600,
@@ -271,6 +273,7 @@ describe('config defaults', () => {
           },
         },
         events_media_type: 'all',
+        pan_mode: 'pan',
         show_recordings: true,
         style: 'stack',
         window_seconds: 3600,