diff --git a/common/constants.js b/common/constants.js
index 00fd20a33a36df6b725bbc3735c4fc0d9d52dd32..ceeb3d027330bb5167bd469abe812569d8be6da0 100644
--- a/common/constants.js
+++ b/common/constants.js
@@ -60,6 +60,7 @@
 
     //Viewer mode
     VIEWER_MODE_ANNOTATING: 'annotating',
+    VIEWER_MODE_KEYFRAME: 'key frame',
 
     // Annotations
     USER_ANNOTATION_LIST: 'user annotations footer',
diff --git a/src/keyframesModule/constants.ts b/src/keyframesModule/constants.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5d716c05f4abaa7a9a6a98909a806c5ff03c63f2
--- /dev/null
+++ b/src/keyframesModule/constants.ts
@@ -0,0 +1,2 @@
+import { ARIA_LABELS } from 'common/constants'
+export const KEYFRAME_VIEWMODE = ARIA_LABELS.VIEWER_MODE_KEYFRAME
\ No newline at end of file
diff --git a/src/keyframesModule/index.ts b/src/keyframesModule/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/keyframesModule/keyframe.directive.ts b/src/keyframesModule/keyframe.directive.ts
new file mode 100644
index 0000000000000000000000000000000000000000..50708e85062ba075edf28ca9a41506d492694419
--- /dev/null
+++ b/src/keyframesModule/keyframe.directive.ts
@@ -0,0 +1,42 @@
+import { Directive, HostListener, Input } from "@angular/core";
+import { KeyFrameService } from "./service";
+
+@Directive({
+  selector: '[key-frame-play-now]'
+})
+
+export class KeyFrameDirective{
+  @HostListener('click')
+  onClick(){
+    if (this._mode === 'on') {
+      this.svc.startKeyFrameSession()
+      return
+    }
+    if (this._mode === 'off') {
+      this.svc.endKeyFrameSession()
+      return
+    }
+    if (this.svc.inSession) {
+      this.svc.endKeyFrameSession()
+    } else {
+      this.svc.startKeyFrameSession()
+    }
+  }
+
+  private _mode: 'toggle' | 'off' | 'on' = 'on'
+  @Input('key-frame-play-now')
+  set mode(val: string){
+    if (val === 'off') {
+      this._mode = val
+      return
+    }
+    if (val === 'toggle') {
+      this._mode = val
+      return
+    }
+    this._mode = 'on'
+  }
+
+  constructor(private svc: KeyFrameService){
+  }
+}
diff --git a/src/keyframesModule/keyframeCtrl/keyframeCtrl.component.ts b/src/keyframesModule/keyframeCtrl/keyframeCtrl.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..95f4f1a95e7885dfa378dd8bfe36f3b3ba6b4847
--- /dev/null
+++ b/src/keyframesModule/keyframeCtrl/keyframeCtrl.component.ts
@@ -0,0 +1,209 @@
+import { CdkDragDrop, moveItemInArray } from "@angular/cdk/drag-drop";
+import { Component, OnDestroy, Optional } from "@angular/core";
+import { MatSnackBar } from "@angular/material/snack-bar";
+import { Subscription } from "rxjs";
+import { getUuid } from "src/util/fn";
+import { timedValues } from "src/util/generator";
+import { TInteralStatePayload, ViewerInternalStateSvc } from "src/viewerModule/viewerInternalState.service";
+
+type TStoredState = {
+  name: string
+  duration: number
+  viewerType: string
+  payload: any
+}
+
+@Component({
+  selector: 'key-frame-controller',
+  templateUrl: './keyframeCtrl.template.html',
+  styleUrls: [
+    './keyframeCtrl.style.css'
+  ]
+})
+
+export class KeyFrameCtrlCmp implements OnDestroy {
+
+  public loopFlag = false
+  public linearFlag = false
+  public currState: any
+  private currViewerType: string
+  public internalStates: TStoredState[] = []
+
+  private subs: Subscription[] = []
+
+  ngOnDestroy(){
+    while(this.subs.length) this.subs.pop().unsubscribe()
+  }
+
+  constructor(
+    private snackbar: MatSnackBar,
+    @Optional() private viewerInternalSvc: ViewerInternalStateSvc
+  ){
+    if (!viewerInternalSvc) {
+      this.snackbar.open(`error: ViewerInternalStateSvc not injected.`)
+      return
+    }
+
+    this.subs.push(
+      viewerInternalSvc.viewerInternalState$.pipe(
+      ).subscribe(state => {
+        this.currState = state.payload
+        this.currViewerType = state.viewerType
+      })
+    )
+  }
+
+  addKeyFrame(){
+    this.internalStates = [
+      ...this.internalStates,
+      {
+        name: `Frame ${this.internalStates.length + 1}`,
+        duration: 1000,
+        viewerType: this.currViewerType,
+        payload: this.currState
+      }
+    ]
+  }
+
+  private raf: number
+  
+  private isPlaying = false
+  async togglePlay(){
+    if (this.isPlaying) {
+      this.isPlaying = false
+      return
+    }
+    this.isPlaying = true
+
+    let idx = 0
+    this.gotoFrame(this.internalStates[0])
+    while (true) {
+      try {
+        await this.animateFrame(
+          this.internalStates[idx % this.internalStates.length],
+          this.internalStates[(idx + 1) % this.internalStates.length]
+        )
+        idx ++
+        if (idx >= this.internalStates.length-1 && !this.loopFlag) break
+      } catch (e) {
+        // user interrupted
+        console.log(e)
+      }
+    }
+    this.isPlaying = false
+  }
+  private async animateFrame(fromFrame: TStoredState, toFrame: TStoredState) {
+    const toPayloadCamera = (toFrame.payload as any).camera
+    const fromPayloadCamera = (fromFrame.payload as any).camera
+
+    const delta = {
+      x: toPayloadCamera.x - fromPayloadCamera.x,
+      y: toPayloadCamera.y - fromPayloadCamera.y,
+      z: toPayloadCamera.z - fromPayloadCamera.z,
+    }
+
+    const applyDelta = (() => {
+      if (this.linearFlag) {
+        return (d: number) => {
+          return {
+            x: delta.x * d + fromPayloadCamera.x,
+            y: delta.y * d + fromPayloadCamera.y,
+            z: delta.z * d + fromPayloadCamera.z,
+          }
+        }
+      } else {
+        const THREE = (window as any).ThreeSurfer.THREE
+        const idQ = new THREE.Quaternion()
+        const targetQ = new THREE.Quaternion()
+        const vec1 = new THREE.Vector3(
+          fromPayloadCamera.x,
+          fromPayloadCamera.y,
+          fromPayloadCamera.z,
+        )
+
+        const startVec = vec1.clone()
+        const vec1Length = vec1.length()
+        const vec2 = new THREE.Vector3(
+          toPayloadCamera.x,
+          toPayloadCamera.y,
+          toPayloadCamera.z,
+        )
+        const vec2Length = vec2.length()
+        vec1.normalize()
+        vec2.normalize()
+
+        targetQ.setFromUnitVectors(vec1, vec2)
+
+        return (d: number) => {
+          const deltaQ = idQ.clone()
+          deltaQ.slerp(targetQ, d)
+          const v = startVec.clone()
+          v.applyQuaternion(deltaQ)
+          return {
+            x: v.x,
+            y: v.y,
+            z: v.z
+          }
+        }
+      }
+    })()
+
+    const gen = timedValues(toFrame.duration)
+
+    return new Promise((rs, rj) => {
+
+      const animate = () => {
+        if (!this.isPlaying) {
+          this.raf = null
+          return rj('User interrupted')
+        }
+        const next = gen.next()
+        const d = next.value
+
+        if (this.viewerInternalSvc) {
+          const camera = applyDelta(d)
+          this.viewerInternalSvc.applyInternalState({
+            "@id": getUuid(),
+            "@type": "TViewerInternalStateEmitterEvent",
+            viewerType: fromFrame.viewerType,
+            payload: {
+              camera
+            }
+          })
+        }
+
+        if (next.done) {
+          this.raf = null
+
+          rs('')
+        } else {
+          this.raf = requestAnimationFrame(() => {
+            animate()
+          })
+        }
+      }
+      this.raf = requestAnimationFrame(() => {
+        animate()
+      })
+    })
+  }
+
+  gotoFrame(item: TStoredState) {
+    if (this.viewerInternalSvc) {
+      this.viewerInternalSvc.applyInternalState({
+        "@id": getUuid(),
+        "@type": "TViewerInternalStateEmitterEvent",
+        viewerType: item.viewerType,
+        payload: item.payload
+      })
+    }
+  }
+
+  removeFrame(item: TStoredState){
+    this.internalStates = this.internalStates.filter(v => item !== v)
+  }
+
+  drop(event: CdkDragDrop<string[]>) {
+    moveItemInArray(this.internalStates, event.previousIndex, event.currentIndex);
+  }
+}
diff --git a/src/keyframesModule/keyframeCtrl/keyframeCtrl.style.css b/src/keyframesModule/keyframeCtrl/keyframeCtrl.style.css
new file mode 100644
index 0000000000000000000000000000000000000000..5a57375ec0d032b46d0553cc82e86ccbcfca162b
--- /dev/null
+++ b/src/keyframesModule/keyframeCtrl/keyframeCtrl.style.css
@@ -0,0 +1,21 @@
+.cdk-drop-list-dragging :not(.cdk-drag-placeholder)
+{
+  transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
+}
+.cdk-drag-placeholder {
+  opacity: 0.2;
+}
+[cdkDragHandle]
+{
+  cursor:ns-resize;
+}
+
+mat-form-field
+{
+  margin-right: 0.25rem;
+}
+
+.duration-container
+{
+  width: 3.5rem;
+}
\ No newline at end of file
diff --git a/src/keyframesModule/keyframeCtrl/keyframeCtrl.template.html b/src/keyframesModule/keyframeCtrl/keyframeCtrl.template.html
new file mode 100644
index 0000000000000000000000000000000000000000..2fecef02aa1eff14b9936582e42ec5e2f88fbc57
--- /dev/null
+++ b/src/keyframesModule/keyframeCtrl/keyframeCtrl.template.html
@@ -0,0 +1,61 @@
+<ng-template #noCurrStateTmpl>
+  No current state recorded
+</ng-template>
+
+<ng-template
+  [ngIf]="currState"
+  [ngIfElse]="noCurrStateTmpl">
+
+  <button
+    (click)="addKeyFrame()"
+    matTooltip="Add key frame. Shortcut: [a]"
+    mat-button
+    color="primary">
+    Add Key Frame
+  </button>
+
+  <mat-slide-toggle [(ngModel)]="loopFlag">Loop</mat-slide-toggle>
+  <mat-slide-toggle [(ngModel)]="linearFlag">Linear Camera</mat-slide-toggle>
+
+  <button mat-icon-button
+    (click)="togglePlay()">
+    <i class="fas fa-play"></i>
+  </button>
+
+  <span *ngIf="false">
+    <span *ngFor="let obj of currState | keyvalue">
+      {{ obj['key'] }}
+    </span>
+  </span>
+</ng-template>
+
+<mat-list cdkDropList (cdkDropListDropped)="drop($event)">
+  <mat-list-item *ngFor="let state of internalStates" cdkDrag>
+    <button mat-icon-button
+    cdkDragHandle>
+      <i class="fas fa-grip-vertical"></i>
+    </button>
+
+    <mat-form-field>
+      <mat-label>name</mat-label>
+      <input type="text" matInput [(ngModel)]="state.name">
+    </mat-form-field>
+    
+    <mat-form-field class="duration-container">
+      <mat-label>ms</mat-label>
+      <input type="number" matInput [(ngModel)]="state.duration">
+    </mat-form-field>
+    
+    <button mat-icon-button
+      (click)="gotoFrame(state)">
+      <i class="fas fa-map-marker-alt"></i>
+    </button>
+    
+    <button mat-icon-button
+      color="warn"
+      (click)="removeFrame(state)">
+      <i class="fas fa-trash"></i>
+    </button>
+    
+  </mat-list-item>
+</mat-list>
\ No newline at end of file
diff --git a/src/keyframesModule/module.ts b/src/keyframesModule/module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0042fdbf4a8dd60efc0f048897950236fba2e3b8
--- /dev/null
+++ b/src/keyframesModule/module.ts
@@ -0,0 +1,30 @@
+import { CommonModule } from "@angular/common";
+import { NgModule } from "@angular/core";
+import { FormsModule } from "@angular/forms";
+import { ComponentsModule } from "src/components";
+import { AngularMaterialModule } from "src/sharedModules";
+import { KeyFrameDirective } from "./keyframe.directive";
+import { KeyFrameCtrlCmp } from "./keyframeCtrl/keyframeCtrl.component";
+import { KeyFrameService } from "./service";
+
+@NgModule({
+  imports: [
+    CommonModule,
+    AngularMaterialModule,
+    ComponentsModule,
+    FormsModule,
+  ],
+  declarations: [
+    KeyFrameCtrlCmp,
+    KeyFrameDirective,
+  ],
+  exports: [
+    KeyFrameCtrlCmp,
+    KeyFrameDirective,
+  ],
+  providers: [
+    KeyFrameService,
+  ]
+})
+
+export class KeyFrameModule{}
\ No newline at end of file
diff --git a/src/keyframesModule/service.ts b/src/keyframesModule/service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b807aa15ba8414c823bf48ab132fb01c29c19590
--- /dev/null
+++ b/src/keyframesModule/service.ts
@@ -0,0 +1,51 @@
+import { Injectable, OnDestroy } from "@angular/core";
+import { MatDialog } from "@angular/material/dialog";
+import { Store } from "@ngrx/store";
+import { BehaviorSubject, Subscription } from "rxjs";
+import { distinctUntilChanged } from "rxjs/operators";
+import { viewerStateSetViewerMode } from "src/services/state/viewerState.store.helper";
+import { KEYFRAME_VIEWMODE } from "./constants";
+import { KeyFrameCtrlCmp } from "./keyframeCtrl/keyframeCtrl.component";
+
+@Injectable()
+export class KeyFrameService implements OnDestroy {
+
+  inSession$ = new BehaviorSubject(false)
+  private subs: Subscription[] = []
+  private _inSession = false
+  get inSession(){
+    return this._inSession
+  }
+  set inSession(val){
+    this._inSession = val
+    this.inSession$.next(val)
+  }
+
+  ngOnDestroy(){
+    while(this.subs.length) this.subs.pop().unsubscribe()
+  }
+  
+  startKeyFrameSession(){
+    this.inSession = true
+  }
+  endKeyFrameSession(){
+    this.inSession = false
+  }
+
+  constructor(
+    // private dialog: MatDialog,
+    private store: Store<any>
+  ){
+    this.inSession$.pipe(
+      distinctUntilChanged()
+    ).subscribe(flag => {
+
+      // TODO enable side bar when ready
+      this.store.dispatch(
+        viewerStateSetViewerMode({
+          payload: flag && KEYFRAME_VIEWMODE
+        })
+      )
+    })
+  }
+}
\ No newline at end of file
diff --git a/src/main.module.ts b/src/main.module.ts
index bfe4020d475c53317d98064505ca26cd3d79ef57..088d031e28b98de0e59c2be73a2c6e548c806bdd 100644
--- a/src/main.module.ts
+++ b/src/main.module.ts
@@ -234,7 +234,7 @@ export function debug(reducer: ActionReducer<any>): ActionReducer<any> {
     },
     {
       provide: BS_ENDPOINT,
-      useValue: (environment.BS_REST_URL || `https://siibra-api-latest.apps-dev.hbp.eu/v1_0`).replace(/\/$/, '')
+      useValue: ('https://siibra-api-edge.apps-dev.hbp.eu/v1_0' || environment.BS_REST_URL || `https://siibra-api-latest.apps-dev.hbp.eu/v1_0`).replace(/\/$/, '')
     },
   ],
   bootstrap : [
diff --git a/src/ui/topMenu/module.ts b/src/ui/topMenu/module.ts
index 9b4d781bb97bc6e21fa27a4453025e327a72727a..241a0e3d27543f3968c81d4964834b3f7dce2fa8 100644
--- a/src/ui/topMenu/module.ts
+++ b/src/ui/topMenu/module.ts
@@ -14,6 +14,7 @@ import { AngularMaterialModule } from "src/sharedModules";
 import { TopMenuCmp } from "./topMenuCmp/topMenu.components";
 import { UserAnnotationsModule } from "src/atlasComponents/userAnnotations";
 import { QuickTourModule } from "src/ui/quickTour/module";
+import { KeyFrameModule } from "src/keyframesModule/module";
 
 @NgModule({
   imports: [
@@ -30,6 +31,7 @@ import { QuickTourModule } from "src/ui/quickTour/module";
     AuthModule,
     ScreenshotModule,
     UserAnnotationsModule,
+    KeyFrameModule,
     QuickTourModule,
   ],
   declarations: [
diff --git a/src/ui/topMenu/topMenuCmp/topMenu.components.ts b/src/ui/topMenu/topMenuCmp/topMenu.components.ts
index d9adf9d8a823e893329e57832c411cc652464f1d..716a66662e49dbd00a17c97f1284aac92a6e616e 100644
--- a/src/ui/topMenu/topMenuCmp/topMenu.components.ts
+++ b/src/ui/topMenu/topMenuCmp/topMenu.components.ts
@@ -49,7 +49,8 @@ export class TopMenuCmp {
   public pluginTooltipText: string = `Plugins and Tools`
   public screenshotTooltipText: string = 'Take screenshot'
   public annotateTooltipText: string = 'Start annotating'
-
+  public keyFrameText = `Start KeyFrames`
+  
   public quickTourData: IQuickTourData = {
     description: QUICKTOUR_DESC.TOP_MENU,
     order: 8,
diff --git a/src/ui/topMenu/topMenuCmp/topMenu.template.html b/src/ui/topMenu/topMenuCmp/topMenu.template.html
index 6982b1ad149a20377d938d8f0b9ebe5a752aac7d..c090670d792dccab9a0754e79e1d1197ef0a1ddc 100644
--- a/src/ui/topMenu/topMenuCmp/topMenu.template.html
+++ b/src/ui/topMenu/topMenuCmp/topMenu.template.html
@@ -163,6 +163,17 @@
       Annotation mode
     </span>
   </button>
+
+  <button mat-menu-item
+    [disabled]="!viewerLoaded"
+    key-frame-play-now
+    [matTooltip]="keyFrameText">
+    <mat-icon fontSet="fas" fontIcon="fa-play"></mat-icon>
+    <span>
+      KeyFrames
+    </span>
+  </button>
+  
   <plugin-banner></plugin-banner>
 </mat-menu>
 
diff --git a/src/util/pureConstant.service.ts b/src/util/pureConstant.service.ts
index 16258cba419a3bef23721f6a7143737a946dbbbd..b7962a9fe0ed9622e7237cb2b7cd7245ac9539ad 100644
--- a/src/util/pureConstant.service.ts
+++ b/src/util/pureConstant.service.ts
@@ -448,6 +448,17 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}"
             return forkJoin(
               templateSpaces.map(
                 tmpl => {
+                  // hardcode 
+                  // see https://github.com/FZJ-INM1-BDA/siibra-python/issues/98
+                  if (
+                    tmpl.id === 'minds/core/referencespace/v1.0.0/tmp-fsaverage'
+                    && !tmpl.availableParcellations.find(p => p.id === 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290')  
+                  ) {
+                    tmpl.availableParcellations.push({
+                      id: 'minds/core/parcellationatlas/v1.0.0/94c1125b-b87e-45e4-901c-00daee7f2579-290',
+                      name: 'Julich-Brain Probabilistic Cytoarchitectonic Maps (v2.9)'
+                    })
+                  }
                   ngLayerObj[tmpl.id] = {}
                   return tmpl.availableParcellations.map(
                     parc => this.getRegions(atlas['@id'], parc.id, tmpl.id).pipe(
diff --git a/src/viewerModule/module.ts b/src/viewerModule/module.ts
index eb6302174da652e6328def8c94531b160894e9aa..53d48a2ecc114144ba740b3f25231f2030882df9 100644
--- a/src/viewerModule/module.ts
+++ b/src/viewerModule/module.ts
@@ -26,6 +26,8 @@ import { ViewerStateBreadCrumbModule } from "./viewerStateBreadCrumb/module";
 import { KgRegionalFeatureModule } from "src/atlasComponents/regionalFeatures/bsFeatures/kgRegionalFeature";
 import { API_SERVICE_SET_VIEWER_HANDLE_TOKEN, AtlasViewerAPIServices, setViewerHandleFactory } from "src/atlasViewer/atlasViewer.apiService.service";
 import { ILoadMesh, LOAD_MESH_TOKEN } from "src/messaging/types";
+import { KeyFrameModule } from "src/keyframesModule/module";
+import { ViewerInternalStateSvc } from "./viewerInternalState.service";
 
 @NgModule({
   imports: [
@@ -47,6 +49,7 @@ import { ILoadMesh, LOAD_MESH_TOKEN } from "src/messaging/types";
     ContextMenuModule,
     ViewerStateBreadCrumbModule,
     KgRegionalFeatureModule,
+    KeyFrameModule,
   ],
   declarations: [
     ViewerCmp,
@@ -95,6 +98,7 @@ import { ILoadMesh, LOAD_MESH_TOKEN } from "src/messaging/types";
       ]
     },
     AtlasViewerAPIServices,
+    ViewerInternalStateSvc,
   ],
   exports: [
     ViewerCmp,
diff --git a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts
index 852a10faa56829c7ab8da8b87cbb749a19e04cc4..c4a9fb323bebe16410bb186e1a0e978bdcdfbbd3 100644
--- a/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts
+++ b/src/viewerModule/threeSurfer/threeSurferGlue/threeSurfer.component.ts
@@ -14,8 +14,19 @@ import { REGION_OF_INTEREST } from "src/util/interfaces";
 import { MatSnackBar } from "@angular/material/snack-bar";
 import { CONST } from 'common/constants'
 import { API_SERVICE_SET_VIEWER_HANDLE_TOKEN, TSetViewerHandle } from "src/atlasViewer/atlasViewer.apiService.service";
-import { switchMapWaitFor } from "src/util/fn";
-
+import { getUuid, switchMapWaitFor } from "src/util/fn";
+import { TInteralStatePayload, ViewerInternalStateSvc } from "src/viewerModule/viewerInternalState.service";
+
+const viewerType = 'ThreeSurfer'
+type TInternalState = {
+  camera: {
+    x: number
+    y: number
+    z: number
+  }
+  mode: string
+  hemisphere: 'left' | 'right' | 'both'
+}
 const pZoomFactor = 5e3
 
 type THandlingCustomEv = {
@@ -88,6 +99,7 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af
   private localCameraNav: TCameraOrientation = null
 
   public allKeys: {name: string, checked: boolean}[] = []
+  private internalStateNext: (arg: TInteralStatePayload<TInternalState>) => void
 
   private regionMap: Map<string, Map<number, any>> = new Map()
   private mouseoverRegions = []
@@ -96,10 +108,29 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af
     private store$: Store<any>,
     private navStateStoreRelay: ComponentStore<{ perspectiveOrientation: [number, number, number, number], perspectiveZoom: number }>,
     private snackbar: MatSnackBar,
+    @Optional() intViewerStateSvc: ViewerInternalStateSvc,
     @Optional() @Inject(REGION_OF_INTEREST) private roi$: Observable<any>,
     @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor,
     @Optional() @Inject(API_SERVICE_SET_VIEWER_HANDLE_TOKEN) setViewerHandle: TSetViewerHandle,
   ){
+    if (intViewerStateSvc) {
+      const {
+        done,
+        next,
+      } = intViewerStateSvc.registerEmitter({
+        "@type": 'TViewerInternalStateEmitter',
+        viewerType,
+        applyState: arg => {
+          // type check
+          if (arg.viewerType !== viewerType) return
+          this.toTsRef(tsRef => {
+            tsRef.camera.position.copy((arg.payload as any).camera)
+          })
+        }
+      })
+      this.internalStateNext = next
+      this.onDestroyCb.push(() => done())
+    }
 
     // set viewer handle
     // the API won't be 100% compatible with ngviewer
@@ -474,7 +505,7 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af
             this.tsRef.dispose()
             this.tsRef = null
           }
-        );
+        )
         this.tsRef.control.enablePan = false
         while (this.tsRefInitCb.length > 0) this.tsRefInitCb.pop()(this.tsRef)
       }
@@ -574,6 +605,18 @@ export class ThreeSurferGlueCmp implements IViewer<'threeSurfer'>, OnChanges, Af
 
   private cameraEv$ = new Subject<{ position: { x: number, y: number, z: number }, zoom: number }>()
   private handleCustomCameraEvent(detail: any){
+    if (this.internalStateNext) {
+      this.internalStateNext({
+        "@id": getUuid(),
+        "@type": 'TViewerInternalStateEmitterEvent',
+        viewerType,
+        payload: {
+          mode: this.selectedMode,
+          camera: detail.position,
+          hemisphere: 'both'
+        }
+      })
+    }
     this.cameraEv$.next(detail)
   }
 
diff --git a/src/viewerModule/viewerCmp/viewerCmp.style.css b/src/viewerModule/viewerCmp/viewerCmp.style.css
index 735eba1520dca238a3688a96a4e35bbe68e14332..19b64d6b88116e5954dd0fb2226e6fc79b3dcc01 100644
--- a/src/viewerModule/viewerCmp/viewerCmp.style.css
+++ b/src/viewerModule/viewerCmp/viewerCmp.style.css
@@ -20,6 +20,10 @@
   margin-top: 1.5rem;
 }
 
+.tab-toggle-container > *
+{
+  display: block;
+}
 
 .tab-toggle
 {
diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html
index aa2004d86a9df45e61b54719dbd291f627df569e..30d2c0411ce039d73beba463374fe6a3e2bed884 100644
--- a/src/viewerModule/viewerCmp/viewerCmp.template.html
+++ b/src/viewerModule/viewerCmp/viewerCmp.template.html
@@ -6,34 +6,42 @@
 <layout-floating-container [zIndex]="10">
 
   <!-- Annotation mode -->
-  <div *ngIf="(viewerMode$ | async) === ARIA_LABELS.VIEWER_MODE_ANNOTATING">
-    <mat-drawer-container class="mat-drawer-content-overflow-visible w-100 h-100 position-absolute invisible"
+    <mat-drawer-container *ngIf="viewerMode$ | async as viewerMode"
+      class="mat-drawer-content-overflow-visible w-100 h-100 position-absolute invisible"
       [hasBackdrop]="false">
-      <mat-drawer #annotationDrawer="matDrawer"
+      
+      <mat-drawer #viewerModeDrawer="matDrawer"
         mode="side"
-        (annotation-event-directive)="annotationDrawer.open()"
+        (annotation-event-directive)="viewerModeDrawer.open()"
         [annotation-event-directive-filter]="['showList']"
         [autoFocus]="false"
         [disableClose]="true"
         class="p-0 pe-all col-10 col-sm-10 col-md-5 col-lg-4 col-xl-3 col-xxl-2">
-        <annotation-list></annotation-list>
-      </mat-drawer>
 
-      <mat-drawer-content class="visible position-relative pe-none">
+        <!-- annotation -->
+        <ng-template [ngIf]="viewerMode === ARIA_LABELS.VIEWER_MODE_ANNOTATING">
+          <annotation-list></annotation-list>
+        </ng-template>
 
-        <iav-layout-fourcorners>
+        <ng-template [ngIf]="viewerMode === ARIA_LABELS.VIEWER_MODE_KEYFRAME">
+          <key-frame-controller></key-frame-controller>
+        </ng-template>
+
+      </mat-drawer>
+      <mat-drawer-content class="visible position-relative pe-none">
 
+        <!-- annotation specific -->
+        <iav-layout-fourcorners *ngIf="viewerMode === ARIA_LABELS.VIEWER_MODE_ANNOTATING">
           <!-- pullable tab top right corner -->
           <div iavLayoutFourCornersTopLeft class="tab-toggle-container">
-            <div>
-              <ng-container *ngTemplateOutlet="tabTmpl_defaultTmpl; context: {
-                matColor: 'primary',
-                fontIcon: 'fa-list',
-                tooltip: 'Annotation list',
-                click: annotationDrawer.toggle.bind(annotationDrawer)
-              }">
-              </ng-container>
-            </div>
+
+            <ng-container *ngTemplateOutlet="tabTmpl_defaultTmpl; context: {
+              matColor: 'primary',
+              fontIcon: 'fa-list',
+              tooltip: 'Annotation list',
+              click: viewerModeDrawer.toggle.bind(viewerModeDrawer)
+            }">
+            </ng-container>
 
             <annotating-tools-panel class="z-index-10">
             </annotating-tools-panel>
@@ -53,13 +61,42 @@
               </button>
             </mat-card>
           </div>
+
         </iav-layout-fourcorners>
 
 
+        <!-- key frame specific -->
+        <iav-layout-fourcorners *ngIf="viewerMode === ARIA_LABELS.VIEWER_MODE_KEYFRAME">
+          <!-- pullable tab top right corner -->
+          <div iavLayoutFourCornersTopLeft class="tab-toggle-container">
+
+            <ng-container *ngTemplateOutlet="tabTmpl_defaultTmpl; context: {
+              matColor: 'primary',
+              fontIcon: 'fa-play',
+              tooltip: 'Annotation list',
+              click: viewerModeDrawer.toggle.bind(viewerModeDrawer)
+            }">
+            </ng-container>
+          </div>
+
+          <div iavLayoutFourCornersTopRight>
+            <mat-card class="mat-card-sm pe-all m-4">
+              <span>
+                Key Frame
+              </span>
+              <button mat-icon-button
+                color="warn"
+                key-frame-play-now="off">
+                <i class="fas fa-times"></i>
+              </button>
+            </mat-card>
+          </div>
+
+        </iav-layout-fourcorners>
       </mat-drawer-content>
+
     </mat-drawer-container>
-    <!-- <annotation-message></annotation-message> -->
-  </div>
+
 
   <!-- top drawer -->
   <mat-drawer-container
diff --git a/src/viewerModule/viewerInternalState.service.ts b/src/viewerModule/viewerInternalState.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..02600a20bdfe491fede2620b8566db3eb930a225
--- /dev/null
+++ b/src/viewerModule/viewerInternalState.service.ts
@@ -0,0 +1,51 @@
+import { Injectable } from "@angular/core";
+import { BehaviorSubject } from "rxjs";
+
+export type TInteralStatePayload<TPayloadShape> = {
+  '@type': 'TViewerInternalStateEmitterEvent'
+  '@id': string
+  viewerType: string
+  payload: TPayloadShape
+}
+
+type TViewerInternalStateEmitter<TPayloadShape> = {
+  '@type': 'TViewerInternalStateEmitter'
+  viewerType: string
+  applyState: (arg: TInteralStatePayload<TPayloadShape>) => void
+}
+
+type TEmitterCallbacks<TPayloadShape> = {
+  next: (arg: TInteralStatePayload<TPayloadShape>) => void
+  done: () => void
+}
+
+@Injectable()
+export class ViewerInternalStateSvc{
+
+  public viewerInternalState$ = new BehaviorSubject<TInteralStatePayload<any>>(null)
+
+  private registeredEmitter: TViewerInternalStateEmitter<any>
+
+  applyInternalState(arg: TInteralStatePayload<any>){
+    if (!this.registeredEmitter) {
+      throw new Error(`No emitter registered. Aborting.`)
+    }
+    this.registeredEmitter.applyState(arg)
+  }
+
+  registerEmitter<T>(emitter: TViewerInternalStateEmitter<T>): TEmitterCallbacks<T>{
+    this.registeredEmitter = emitter
+    return {
+      next: arg => {
+        this.viewerInternalState$.next(arg)
+      },
+      done: () => this.deregisterEmitter(emitter)
+    }
+  }
+  deregisterEmitter(emitter: TViewerInternalStateEmitter<any>){
+    if (emitter === this.registeredEmitter) {
+      this.viewerInternalState$.next(null)
+      this.registeredEmitter = null
+    }
+  }
+}
\ No newline at end of file