diff --git a/docs/releases/v2.5.0.md b/docs/releases/v2.5.0.md
index 5f9bcd52628076fb2cb360b74bf06cf9f11eb838..0f7dfbe92b89bba34755d8e89b1c1c16cc9c8660 100644
--- a/docs/releases/v2.5.0.md
+++ b/docs/releases/v2.5.0.md
@@ -1,5 +1,9 @@
 # v2.5.0
 
+## New features
+
+- allow local NiFTi's to be visualised
+
 ## Under the hood stuff
 
 - refactor: remove unneeded code
diff --git a/src/dragDropFile/dragDrop.directive.ts b/src/dragDropFile/dragDrop.directive.ts
index 0427836c4a369ddf3fd7ced09358639d206cdcf1..99e82c0c23b1cfbf957e74f872e37e7f3485a847 100644
--- a/src/dragDropFile/dragDrop.directive.ts
+++ b/src/dragDropFile/dragDrop.directive.ts
@@ -36,13 +36,14 @@ export class DragDropFileDirective implements OnInit, OnDestroy {
     ev.preventDefault()
     this.reset()
 
-    this.dragDropOnDrop.emit(Array.from(ev.dataTransfer.files))
+    this.dragDropOnDrop.emit(Array.from(ev?.dataTransfer?.files || []))
   }
 
   public reset() {
     if (this.snackbarRef) {
       this.snackbarRef.dismiss()
     }
+    this.snackbarRef = null
     this.opacity = null
   }
 
@@ -54,7 +55,16 @@ export class DragDropFileDirective implements OnInit, OnDestroy {
         debounceTime(16),
       ).subscribe(flag => {
         if (flag) {
-          this.snackbarRef = this.snackBar.open(this.snackText || `Drop file(s) here.`)
+          this.snackbarRef = this.snackBar.open(this.snackText || `Drop file(s) here.`, 'Dismiss')
+
+          /**
+           * In buggy scenarios, user could at least dismiss by action
+           */
+          this.snackbarRef.afterDismissed().subscribe(reason => {
+            if (reason.dismissedByAction) {
+              this.reset()
+            }
+          })
           this.opacity = 0.2
         } else {
           this.reset()
diff --git a/src/viewerModule/nehuba/module.ts b/src/viewerModule/nehuba/module.ts
index afb3ea038ea52294bc5f2f0714023243c8cf0aae..63afffbfb47711024c899a8d845092e4531d1fbf 100644
--- a/src/viewerModule/nehuba/module.ts
+++ b/src/viewerModule/nehuba/module.ts
@@ -26,6 +26,7 @@ import { AuthModule } from "src/auth";
 import {QuickTourModule} from "src/ui/quickTour/module";
 import { WindowResizeModule } from "src/util/windowResize";
 import { ViewerCtrlModule } from "./viewerCtrl";
+import { DragDropFileModule } from "src/dragDropFile/module";
 
 @NgModule({
   imports: [
@@ -41,6 +42,7 @@ import { ViewerCtrlModule } from "./viewerCtrl";
     ShareModule,
     WindowResizeModule,
     ViewerCtrlModule,
+    DragDropFileModule,
 
     /**
      * should probably break this into its own...
diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts
index 0afa1fda6c3ad59fab421629150d6afa5c5aab81..89b72c6556b4d4773e5014a83aaed3bef6c19a2e 100644
--- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts
+++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.spec.ts
@@ -1,27 +1,83 @@
 import { CommonModule } from "@angular/common"
-import { TestBed } from "@angular/core/testing"
+import { Component, Directive } from "@angular/core"
+import { async, ComponentFixture, TestBed } from "@angular/core/testing"
+import { FormsModule, ReactiveFormsModule } from "@angular/forms"
 import { MockStore, provideMockStore } from "@ngrx/store/testing"
-import { Subject } from "rxjs"
+import { NEVER, of, Subject } from "rxjs"
+import { ComponentsModule } from "src/components"
 import { ClickInterceptorService } from "src/glue"
+import { LayoutModule } from "src/layouts/layout.module"
 import { PANELS } from "src/services/state/ngViewerState/constants"
 import { ngViewerSelectorOctantRemoval, ngViewerSelectorPanelMode, ngViewerSelectorPanelOrder } from "src/services/state/ngViewerState/selectors"
 import { uiStateMouseOverSegmentsSelector } from "src/services/state/uiState/selectors"
-import { viewerStateSetSelectedRegions } from "src/services/state/viewerState/actions"
+import { viewerStateSetSelectedRegions } from "src/services/state/viewerState.store.helper"
 import { viewerStateCustomLandmarkSelector, viewerStateNavigationStateSelector, viewerStateSelectedRegionsSelector } from "src/services/state/viewerState/selectors"
-import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util"
+import { Landmark2DModule } from "src/ui/nehubaContainer/2dLandmarks/module"
+import { QuickTourModule } from "src/ui/quickTour"
+import { AngularMaterialModule } from "src/ui/sharedModules/angularMaterial.module"
+import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR, UtilModule } from "src/util"
+import { WindowResizeModule } from "src/util/windowResize"
 import { NehubaLayerControlService } from "../layerCtrl.service"
+import { MaximisePanelButton } from "../maximisePanelButton/maximisePanelButton.component"
 import { NehubaMeshService } from "../mesh.service"
+import { NehubaViewerTouchDirective } from "../nehubaViewerInterface/nehubaViewerTouch.directive"
+import { selectorAuxMeshes } from "../store"
+import { TouchSideClass } from "../touchSideClass.directive"
 import { NehubaGlueCmp } from "./nehubaViewerGlue.component"
+import { HarnessLoader } from "@angular/cdk/testing"
+
+
+@Component({
+  selector: 'viewer-ctrl-component',
+  template: ''
+})
+
+class MockViewerCtrlCmp{}
+
+@Directive({
+  selector: '[iav-nehuba-viewer-container]',
+  exportAs: 'iavNehubaViewerContainer',
+})
+
+class MockNehubaViewerContainerDirective{
+  public viewportToDatas: any
+  public nehubaViewerInstance = {
+    nehubaViewer: {
+      ngviewer: null
+    }
+  }
+  mouseOverSegments = NEVER
+  navigationEmitter = NEVER
+  mousePosEmitter = NEVER
+}
 
 describe('> nehubaViewerGlue.component.ts', () => {
   let mockStore: MockStore
-  beforeEach(() => {
+  let rootLoader: HarnessLoader
+  let fixture: ComponentFixture<NehubaGlueCmp>
+  beforeEach( async(() => {
     TestBed.configureTestingModule({
       imports: [
         CommonModule,
+        AngularMaterialModule,
+        LayoutModule,
+        Landmark2DModule,
+        QuickTourModule,
+        ComponentsModule,
+        UtilModule,
+        WindowResizeModule,
+        FormsModule,
+        ReactiveFormsModule,
       ],
       declarations: [
-        NehubaGlueCmp
+        NehubaGlueCmp,
+        MaximisePanelButton,
+        MockViewerCtrlCmp,
+        TouchSideClass,
+
+        // TODO this may introduce a lot more dep
+        MockNehubaViewerContainerDirective,
+        NehubaViewerTouchDirective,
       ],
       providers: [
         /**
@@ -59,13 +115,8 @@ describe('> nehubaViewerGlue.component.ts', () => {
           }
         }
       ]
-    }).overrideComponent(NehubaGlueCmp, {
-      set: {
-        template: '',
-        templateUrl: null
-      }
     }).compileComponents()
-  })
+  }))
 
   beforeEach(() => {
     mockStore = TestBed.inject(MockStore)
@@ -76,11 +127,13 @@ describe('> nehubaViewerGlue.component.ts', () => {
     mockStore.overrideSelector(viewerStateSelectedRegionsSelector, [])
     mockStore.overrideSelector(uiStateMouseOverSegmentsSelector, [])
     mockStore.overrideSelector(viewerStateNavigationStateSelector, null)
+
+    mockStore.overrideSelector(selectorAuxMeshes, [])
   })
 
 
   it('> can be init', () => {
-    const fixture = TestBed.createComponent(NehubaGlueCmp)
+    fixture = TestBed.createComponent(NehubaGlueCmp)
     expect(fixture.componentInstance).toBeTruthy()
   })
 
@@ -170,4 +223,108 @@ describe('> nehubaViewerGlue.component.ts', () => {
       })
     })
   })
+
+  describe('> handleFileDrop', () => {
+    let addNgLayerSpy: jasmine.Spy
+    let removeNgLayersSpy: jasmine.Spy
+    let dummyFile1: File
+    let dummyFile2: File
+    let input: File[]
+
+    beforeEach(() => {
+      dummyFile1 = (() => {
+        const bl: any = new Blob([], { type: 'text' })
+        bl.name = 'filename1.txt'
+        bl.lastModifiedDate = new Date()
+        return bl as File
+      })()
+
+      dummyFile2 = (() => {
+        const bl: any = new Blob([], { type: 'text' })
+        bl.name = 'filename2.txt'
+        bl.lastModifiedDate = new Date()
+        return bl as File
+      })()
+
+      fixture = TestBed.createComponent(NehubaGlueCmp)
+      fixture.detectChanges()
+
+      addNgLayerSpy = spyOn(fixture.componentInstance['layerCtrlService'], 'addNgLayer').and.callFake(() => {
+        
+      })
+      removeNgLayersSpy = spyOn(fixture.componentInstance['layerCtrlService'], 'removeNgLayers').and.callFake(() => {
+        
+      })
+    })
+    afterEach(() => {
+      addNgLayerSpy.calls.reset()
+      removeNgLayersSpy.calls.reset()
+    })
+
+    describe('> malformed input', () => {
+      const scenarios = [{
+        desc: 'too few files',
+        inp: []
+      }, {
+        desc: 'too many files', 
+        inp: [dummyFile1, dummyFile2]
+      }]
+
+      for (const { desc, inp } of scenarios) {
+        describe(`> ${desc}`, () => {
+          beforeEach(() => {
+            input = inp
+  
+            const cmp = fixture.componentInstance
+            cmp.handleFileDrop(input)
+          })
+  
+          it('> should not call addnglayer', () => {
+            expect(removeNgLayersSpy).not.toHaveBeenCalled()
+            expect(addNgLayerSpy).not.toHaveBeenCalled()
+          })
+  
+          // TODO having a difficult time getting snackbar harness
+          // it('> snackbar should show error message', async () => {
+          //   console.log('get harness')
+            
+          //   rootLoader = TestbedHarnessEnvironment.documentRootLoader(fixture)
+          //   const loader = TestbedHarnessEnvironment.loader(fixture)
+          //   fixture.detectChanges()
+          //   const snackbarHarness = await rootLoader.getHarness(MatSnackBarHarness)
+          //   console.log('got harness', snackbarHarness)
+          //   // const message = await snackbarHarness.getMessage()
+          //   // console.log('got message')
+          //   // expect(message).toEqual(INVALID_FILE_INPUT)
+          // })
+        })
+      }
+    })
+
+    describe('> correct input', () => {
+      beforeEach(() => {
+        input = [dummyFile1]
+
+        const cmp = fixture.componentInstance
+        cmp.handleFileDrop(input)
+      })
+
+      afterEach(() => {
+        // remove remove all urls
+        fixture.componentInstance['dismissAllAddedLayers']()
+      })
+
+      it('> should call addNgLayer', () => {
+        expect(removeNgLayersSpy).not.toHaveBeenCalled()
+        expect(addNgLayerSpy).toHaveBeenCalledTimes(1)
+      })
+      it('> on repeated input, both remove nglayer and remove ng layer called', () => {
+        const cmp = fixture.componentInstance
+        cmp.handleFileDrop(input)
+
+        expect(removeNgLayersSpy).toHaveBeenCalledTimes(1)
+        expect(addNgLayerSpy).toHaveBeenCalledTimes(2)
+      })
+    })
+  })
 })
diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts
index 4b203fcbf7c3bb734798e2d70cd29feeb4547b66..665d7a33b321f34d66f362c1a23492b7653351ec 100644
--- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts
+++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts
@@ -1,6 +1,6 @@
 import { AfterViewInit, Component, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, Optional, Output, SimpleChanges, ViewChild } from "@angular/core";
 import { select, Store } from "@ngrx/store";
-import { asyncScheduler, combineLatest, fromEvent, merge, Observable, of, Subject } from "rxjs";
+import { asyncScheduler, combineLatest, fromEvent, merge, NEVER, Observable, of, Subject } from "rxjs";
 import { ngViewerActionToggleMax } from "src/services/state/ngViewerState/actions";
 import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR } from "src/util";
 import { uiStateMouseOverSegmentsSelector } from "src/services/state/uiState/selectors";
@@ -23,9 +23,14 @@ import { MouseHoverDirective } from "src/mouseoverModule";
 import { NehubaMeshService } from "../mesh.service";
 import { IQuickTourData } from "src/ui/quickTour/constrants";
 import { NehubaLayerControlService, IColorMap, SET_COLORMAP_OBS, SET_LAYER_VISIBILITY } from "../layerCtrl.service";
-import { switchMapWaitFor } from "src/util/fn";
+import { getUuid, switchMapWaitFor } from "src/util/fn";
 import { INavObj } from "../navigation.service";
 import { NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY } from "../layerCtrl.service/layerCtrl.util";
+import { MatSnackBar } from "@angular/material/snack-bar";
+import { getShader } from "src/util/constants";
+import { EnumColorMapName } from "src/util/colorMaps";
+
+export const INVALID_FILE_INPUT = `Exactly one (1) nifti file is required!`
 
 @Component({
   selector: 'iav-cmp-viewer-nehuba-glue',
@@ -162,10 +167,10 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A
     this.setQuickTourPos()
 
     const { 
-      mouseOverSegments,
-      navigationEmitter,
-      mousePosEmitter,
-    } = this.nehubaContainerDirective
+      mouseOverSegments = NEVER,
+      navigationEmitter = NEVER,
+      mousePosEmitter = NEVER,
+    } = this.nehubaContainerDirective || {}
     const sub = combineLatest([
       mouseOverSegments,
       navigationEmitter,
@@ -301,6 +306,7 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A
     private store$: Store<any>,
     private el: ElementRef,
     private log: LoggingService,
+    private snackbar: MatSnackBar,
     @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor,
     @Optional() @Inject(API_SERVICE_SET_VIEWER_HANDLE_TOKEN) setViewerHandle: TSetViewerHandle,
     @Optional() private layerCtrlService: NehubaLayerControlService,
@@ -697,6 +703,56 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A
     return element
   }
 
+  private droppedLayerNames: {
+    layerName: string
+    resourceUrl: string
+  }[] = []
+  private dismissAllAddedLayers(){
+    while (this.droppedLayerNames.length) {
+      const { resourceUrl, layerName } = this.droppedLayerNames.pop()
+      this.layerCtrlService.removeNgLayers([ layerName ])
+      URL.revokeObjectURL(resourceUrl)
+    }
+  }
+  public handleFileDrop(files: File[]){
+    if (files.length !== 1) {
+      this.snackbar.open(INVALID_FILE_INPUT, 'Dismiss', {
+        duration: 5000
+      })
+      return
+    }
+    const randomUuid = getUuid()
+    const file = files[0]
+
+    /**
+     * TODO check extension?
+     */
+    
+    this.dismissAllAddedLayers()
+    
+    const url = URL.createObjectURL(file)
+    this.droppedLayerNames.push({
+      layerName: randomUuid,
+      resourceUrl: url
+    })
+    this.layerCtrlService.addNgLayer([{
+      name: randomUuid,
+      mixability: 'mixable',
+      source: `nifti://${url}`,
+      shader: getShader({
+        colormap: EnumColorMapName.MAGMA
+      })
+    }])
+
+    this.snackbar.open(
+      `Viewing ${file.name}`,
+      'Clear',
+      { duration: 0 }
+    ).afterDismissed().subscribe(() => {
+      this.dismissAllAddedLayers()
+    })
+  }
+
 
   public returnTruePos(quadrant: number, data: any) {
     const pos = quadrant > 2
diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html
index 83852858e81d2b7e6953310670cdd92e7d252b19..e8e372f6aa358b169a094e16baddb0384b3eeabc 100644
--- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html
+++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html
@@ -1,5 +1,6 @@
 <div class="d-block w-100 h-100"
   (touchmove)="$event.preventDefault()"
+  (drag-drop-file)="handleFileDrop($event)"
   iav-viewer-touch-interface
   [iav-viewer-touch-interface-v-panels]="viewPanels"
   [iav-viewer-touch-interface-vp-to-data]="iavContainer?.viewportToDatas"
@@ -168,7 +169,6 @@
   <!-- NB must not lazy load. key listener needs to work even when component is not yet rendered -->
   <!-- stop propagation is needed, or else click will result in dismiss of menu -->
   <viewer-ctrl-component class="d-block m-2 ml-3 mr-3"
-    #viewerCtrlCmp="viewerCtrlCmp"
     (click)="$event.stopPropagation()">
   </viewer-ctrl-component>
 </mat-menu>