diff --git a/docs/releases/v2.12.5.md b/docs/releases/v2.12.5.md
index 99593eb2a15a269a0dee8d845eceb735659b7c8b..d7714ac02ddb6386a42822bebaed27ea0e05c761 100644
--- a/docs/releases/v2.12.5.md
+++ b/docs/releases/v2.12.5.md
@@ -4,3 +4,5 @@
 
 - enable connectivity for Julich Brain v3
 - added version inspector in UI
+- (experimental) add keyframe support for NG viewer
+- allow experimental flag to be toggled via `setExperimentalFlag` global method
diff --git a/src/main.module.ts b/src/main.module.ts
index 735f7f66dea3d897c4c71f9186a20e7ab2cde9f4..dca320a003b36c2b5ca08acaecc810c5375a63fc 100644
--- a/src/main.module.ts
+++ b/src/main.module.ts
@@ -41,6 +41,7 @@ import {
   atlasSelection,
   RootStoreModule,
   getStoreEffects,
+  userPreference,
 } from "./state"
 import { DARKTHEME } from './util/injectionTokens';
 import { map } from 'rxjs/operators';
@@ -173,12 +174,17 @@ import { ViewerCommonEffects } from './viewerModule';
     },
     {
       provide: APP_INITIALIZER,
-      useFactory: (authSvc: AuthService) => {
+      useFactory: (authSvc: AuthService, store: Store) => {
+        window['setExperimentalFlag'] = (flag: boolean) => {
+          store.dispatch(userPreference.actions.setShowExperimental({
+            flag
+          }))
+        }
         authSvc.authReloadState()
         return () => Promise.resolve()
       },
       multi: true,
-      deps: [ AuthService ]
+      deps: [ AuthService, Store ]
     }
   ],
   bootstrap: [
diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts
index dac942b8e0c95f4ab43ff91940546e333f0dbb54..9ff4c75cc8234c254e143e1a1812519dba9a74ff 100644
--- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts
+++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts
@@ -2,7 +2,7 @@ import { Component, ElementRef, EventEmitter, OnDestroy, Output, Inject, Optiona
 import { Subscription, BehaviorSubject, Observable, Subject, of, interval, combineLatest } from 'rxjs'
 import { debounceTime, filter, scan, switchMap, take, distinctUntilChanged, debounce, map } from "rxjs/operators";
 import { LoggingService } from "src/logging";
-import { bufferUntil, getExportNehuba, switchMapWaitFor } from "src/util/fn";
+import { bufferUntil, getExportNehuba, getUuid, switchMapWaitFor } from "src/util/fn";
 import { deserializeSegment, NEHUBA_INSTANCE_INJTKN } from "../util";
 import { arrayOrderedEql, rgbToHex } from 'common/util'
 import { IMeshesToLoad, SET_MESHES_TO_LOAD, PERSPECTIVE_ZOOM_FUDGE_FACTOR } from "../constants";
@@ -14,6 +14,7 @@ import { IColorMap, SET_COLORMAP_OBS, SET_LAYER_VISIBILITY } from "../layerCtrl.
 import { INgLayerCtrl, NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY, TNgLayerCtrl, Z_TRAVERSAL_MULTIPLIER } from "../layerCtrl.service/layerCtrl.util";
 import { NgCoordinateSpace, Unit } from "../types";
 import { PeriodicSvc } from "src/util/periodic.service";
+import { ViewerInternalStateSvc, AUTO_ROTATE } from "src/viewerModule/viewerInternalState.service";
 
 function translateUnit(unit: Unit) {
   if (unit === "m") {
@@ -130,6 +131,7 @@ export class NehubaViewerUnit implements OnDestroy {
     @Optional() @Inject(SET_SEGMENT_VISIBILITY) private segVis$: Observable<string[]>,
     @Optional() @Inject(NG_LAYER_CONTROL) private layerCtrl$: Observable<TNgLayerCtrl<keyof INgLayerCtrl>>,
     @Optional() @Inject(Z_TRAVERSAL_MULTIPLIER) multiplier$: Observable<number>,
+    @Optional() intViewerStateSvc: ViewerInternalStateSvc,
   ) {
     if (multiplier$) {
       this.ondestroySubscriptions.push(
@@ -139,6 +141,54 @@ export class NehubaViewerUnit implements OnDestroy {
       this.multplier[0] = 1
     }
 
+    if (intViewerStateSvc) {
+      let raqRef: number
+      const {
+        done,
+        next,
+      } = intViewerStateSvc.registerEmitter({
+        "@type": "TViewerInternalStateEmitter",
+        viewerType: "nehuba",
+        applyState: arg => {
+          
+          if (arg.viewerType === AUTO_ROTATE) {
+            if (raqRef) {
+              cancelAnimationFrame(raqRef)
+            }
+            const autoPlayFlag = (arg.payload as any).play
+            const reverseFlag = (arg.payload as any).reverse
+            const autoplaySpeed = (arg.payload as any).speed
+            
+            if (!autoPlayFlag) {
+              return
+            }
+            const deg = (reverseFlag ? 1 : -1) * autoplaySpeed * 1e-3
+            const animate = () => {
+              raqRef = requestAnimationFrame(() => {
+                animate()
+              })
+              const perspectivePose = this.nehubaViewer?.ngviewer?.perspectiveNavigationState?.pose
+              if (!perspectivePose) {
+                return
+              }
+              perspectivePose.rotateAbsolute([0, 0, 1], deg, [0, 0, 0])
+            }
+
+            animate()
+            return
+          }
+        }
+      })
+
+      this.onDestroyCb.push(() => done())
+      next({
+        "@id": getUuid(),
+        '@type': "TViewerInternalStateEmitterEvent",
+        viewerType: "nehuba",
+        payload: {}
+      })
+    }
+
     if (this.nehubaViewer$) {
       this.nehubaViewer$.next(this)
     }