diff --git a/docs/releases/v2.14.5.md b/docs/releases/v2.14.5.md
index a1c15104ac8d92556681f6bf5be939a3f8f31151..6b645d432029d5c37eea56f45ff2abad7a182e58 100644
--- a/docs/releases/v2.14.5.md
+++ b/docs/releases/v2.14.5.md
@@ -8,6 +8,7 @@
 - Reworded point assignment UI
 - Allow multi selected region names to be copied
 - Added legend to region hierarchy
+- (experimental) allow addition of custom linear coordinate space
 
 ## Bugfix
 
diff --git a/src/components/coordTextBox/coordTextBox.component.ts b/src/components/coordTextBox/coordTextBox.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e15e9b04b2cd3aff9eea4762561128278e7a2634
--- /dev/null
+++ b/src/components/coordTextBox/coordTextBox.component.ts
@@ -0,0 +1,139 @@
+import { CommonModule } from "@angular/common";
+import { Component, EventEmitter, HostListener, Input, Output, ViewChild, inject } from "@angular/core";
+import { BehaviorSubject, combineLatest } from "rxjs";
+import { map, shareReplay } from "rxjs/operators";
+import { AngularMaterialModule, MatInput } from "src/sharedModules";
+import { DestroyDirective } from "src/util/directives/destroy.directive";
+
+type TTriplet = [number, number, number]
+type TVec4 = [number, number, number, number]
+export type TAffine = [TVec4, TVec4, TVec4, TVec4]
+export type Render = (v: TTriplet) => string
+
+export function isTVec4(val: unknown): val is TAffine {
+  if (!Array.isArray(val)) {
+    return false
+  }
+  if (val.length !== 4) {
+    return false
+  }
+  return val.every(v => typeof v === "number")
+}
+
+export function isAffine(val: unknown): val is TAffine {
+  if (!Array.isArray(val)) {
+    return false
+  }
+  if (val.length !== 4) {
+    return false
+  }
+  return val.every(v => isTVec4(v))
+}
+
+export function isTriplet(val: unknown): val is TTriplet{
+  if (!Array.isArray(val)) {
+    return false
+  }
+  if (val.some(v => typeof v !== "number")) {
+    return false
+  }
+  return val.length === 3
+}
+
+@Component({
+  selector: 'coordinate-text-input',
+  templateUrl: './coordTextBox.template.html',
+  styleUrls: [
+    './coordTextBox.style.css'
+  ],
+  standalone: true,
+  imports: [
+    CommonModule,
+    AngularMaterialModule
+  ],
+  hostDirectives: [
+    DestroyDirective
+  ]
+})
+
+export class CoordTextBox {
+
+  #destroyed$ = inject(DestroyDirective).destroyed$
+
+  @ViewChild(MatInput)
+  input: MatInput
+
+  @Output('enter')
+  enter = new EventEmitter()
+
+  @HostListener('keydown.enter')
+  @HostListener('keydown.tab')
+  enterHandler() {
+    this.enter.emit()
+  }
+  
+  #coordinates = new BehaviorSubject<TTriplet>([0, 0, 0])
+
+  @Input()
+  set coordinates(val: unknown) {
+    if (!isTriplet(val)) {
+      console.error(`${val} is not TTriplet`)
+      return
+    }
+    this.#coordinates.next(val)
+  }
+
+  
+  #affine = new BehaviorSubject<TAffine>([
+    [1, 0, 0, 0],
+    [0, 1, 0, 0],
+    [0, 0, 1, 0],
+    [0, 0, 0, 1],
+  ])
+
+  @Input()
+  set affine(val: unknown) {
+    if (!isAffine(val)) {
+      console.error(`${val} is not TAffine!`)
+      return
+    }
+    this.#affine.next(val)
+  }
+  
+  #render = new BehaviorSubject<Render>(v => v.join(`, `))
+
+  @Input()
+  set render(val: Render) {
+    this.#render.next(val)
+  }
+
+  inputValue$ = combineLatest([
+    this.#coordinates,
+    this.#affine,
+    this.#render,
+  ]).pipe(
+    map(([ coord, flattenedAffine, render ]) => {
+      const [
+        [m00, m10, m20, m30],
+        [m01, m11, m21, m31],
+        [m02, m12, m22, m32],
+        // [m03, m13, m23, m33],
+      ] = flattenedAffine
+
+      const newCoord: TTriplet = [
+        coord[0] * m00 + coord[1] * m10 + coord[2] * m20 + 1 * m30,
+        coord[0] * m01 + coord[1] * m11 + coord[2] * m21 + 1 * m31,
+        coord[0] * m02 + coord[1] * m12 + coord[2] * m22 + 1 * m32
+      ]
+      return render(newCoord)
+    }),
+    shareReplay(1),
+  )
+
+  @Input()
+  label: string = "Coordinates"
+
+  get inputValue(){
+    return this.input?.value
+  }
+}
diff --git a/src/components/coordTextBox/coordTextBox.style.css b/src/components/coordTextBox/coordTextBox.style.css
new file mode 100644
index 0000000000000000000000000000000000000000..e0db9f988ab4342413fa9dc02c1df442ac63c10c
--- /dev/null
+++ b/src/components/coordTextBox/coordTextBox.style.css
@@ -0,0 +1,15 @@
+:host
+{
+    display: flex;
+    width: 100%;
+}
+
+mat-form-field
+{
+    flex: 1 1 auto;
+}
+
+.suffix
+{
+    flex: 0 0 auto;
+}
diff --git a/src/components/coordTextBox/coordTextBox.template.html b/src/components/coordTextBox/coordTextBox.template.html
new file mode 100644
index 0000000000000000000000000000000000000000..ddf0c59a1d4f8d5a90eac540651acb0b9f4d6251
--- /dev/null
+++ b/src/components/coordTextBox/coordTextBox.template.html
@@ -0,0 +1,13 @@
+<mat-form-field>
+    <mat-label>
+        {{ label }}
+    </mat-label>
+
+    <input type="text" matInput
+        [value]="inputValue$ | async">
+    
+</mat-form-field>
+
+<div class="suffix">
+    <ng-content select="[suffix]"></ng-content>
+</div>
diff --git a/src/components/coordTextBox/index.ts b/src/components/coordTextBox/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fe8e63ce12dec965848966a2e5ca41dd810fb49f
--- /dev/null
+++ b/src/components/coordTextBox/index.ts
@@ -0,0 +1 @@
+export * from "./coordTextBox.component"
diff --git a/src/sharedModules/angularMaterial.exports.ts b/src/sharedModules/angularMaterial.exports.ts
index 9769df1c2a4dafc8a3d8f9aefed9d561c25e4ebe..09611d70f3f6c8afdcacd927a0cbe3be40339fae 100644
--- a/src/sharedModules/angularMaterial.exports.ts
+++ b/src/sharedModules/angularMaterial.exports.ts
@@ -11,5 +11,6 @@ export { UntypedFormControl } from "@angular/forms";
 export { MatTreeFlatDataSource, MatTreeFlattener } from "@angular/material/tree"
 export { MatAutocompleteSelectedEvent } from "@angular/material/autocomplete";
 export { MatPaginator } from "@angular/material/paginator";
+export { MatInput } from "@angular/material/input";
 
 export { MatSlideToggleHarness } from '@angular/material/slide-toggle/testing'
diff --git a/src/theme.scss b/src/theme.scss
index 4898089b853c95dc60f71a39fcbdaa7b7d0eb615..d184be4f42fe0557569f03c05d5600f4073cf63d 100644
--- a/src/theme.scss
+++ b/src/theme.scss
@@ -133,7 +133,7 @@ $sxplr-dark-theme: mat.define-dark-theme((
 {
   @include mat.all-component-themes($sxplr-dark-theme);
   @include custom-cmp($sxplr-dark-theme);
-  input[type="text"]
+  input[type="text"],textarea
   {
     caret-color: white!important;
   }
diff --git a/src/viewerModule/nehuba/module.ts b/src/viewerModule/nehuba/module.ts
index 485c478b8078df06a5d660dd1838f6aac68c32f8..f7e7026c186bf4aa75d663587ffc1c25053ed1f6 100644
--- a/src/viewerModule/nehuba/module.ts
+++ b/src/viewerModule/nehuba/module.ts
@@ -29,6 +29,8 @@ import { NgAnnotationEffects } from "./annotation/effects";
 import { NehubaViewerContainer } from "./nehubaViewerInterface/nehubaViewerContainer.component";
 import { NehubaUserLayerModule } from "./userLayers";
 import { DialogModule } from "src/ui/dialogInfo";
+import { CoordTextBox } from "src/components/coordTextBox";
+import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.directive";
 
 @NgModule({
   imports: [
@@ -60,6 +62,9 @@ import { DialogModule } from "src/ui/dialogInfo";
     QuickTourModule,
     NehubaLayoutOverlayModule,
     DialogModule,
+
+    CoordTextBox,
+    ExperimentalFlagDirective
   ],
   declarations: [
     NehubaViewerContainerDirective,
diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts
index 0c4b2ca09fd7a427ca3d999e99623b86384c20e9..6e2f8251fa76b4d8910b3d3f7d012542a95024fb 100644
--- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts
+++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts
@@ -67,7 +67,7 @@ export class NehubaViewerUnit implements OnDestroy {
   public viewerPosInVoxel$ = new BehaviorSubject<number[]>(null)
   public viewerPosInReal$ = new BehaviorSubject<[number, number, number]>(null)
   public mousePosInVoxel$ = new BehaviorSubject<[number, number, number]>(null)
-  public mousePosInReal$ = new BehaviorSubject(null)
+  public mousePosInReal$ = new BehaviorSubject<[number, number, number]>(null)
 
   private exportNehuba: any
 
@@ -869,7 +869,7 @@ export class NehubaViewerUnit implements OnDestroy {
           if (this.#translateVoxelToReal) {
             
             const coordInReal = this.#translateVoxelToReal(coordInVoxel)
-            this.mousePosInReal$.next( coordInReal )
+            this.mousePosInReal$.next( coordInReal as [number, number, number] )
           }
         }),
 
diff --git a/src/viewerModule/nehuba/statusCard/statusCard.component.ts b/src/viewerModule/nehuba/statusCard/statusCard.component.ts
index 7d9a0886b2dd00c27ef17bb8a3ceb99d55997358..7dc07eb9a27de9fc9f78a6814becbad955449716 100644
--- a/src/viewerModule/nehuba/statusCard/statusCard.component.ts
+++ b/src/viewerModule/nehuba/statusCard/statusCard.component.ts
@@ -9,7 +9,7 @@ import { select, Store } from "@ngrx/store";
 import { LoggingService } from "src/logging";
 import { NehubaViewerUnit } from "../nehubaViewer/nehubaViewer.component";
 import { Observable, Subject, concat, of } from "rxjs";
-import { map, filter, takeUntil, switchMap, shareReplay, debounceTime } from "rxjs/operators";
+import { map, filter, takeUntil, switchMap, shareReplay, debounceTime, scan } from "rxjs/operators";
 import { Clipboard, MatBottomSheet, MatSnackBar } from "src/sharedModules/angularMaterial.exports"
 import { ARIA_LABELS, QUICKTOUR_DESC } from 'common/constants'
 import { FormControl, FormGroup } from "@angular/forms";
@@ -22,6 +22,12 @@ import { SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes";
 import { NEHUBA_CONFIG_SERVICE_TOKEN, NehubaConfigSvc } from "../config.service";
 import { DestroyDirective } from "src/util/directives/destroy.directive";
 import { getUuid } from "src/util/fn";
+import { Render, TAffine, isAffine } from "src/components/coordTextBox"
+
+type TSpace = {
+  label: string
+  affine: TAffine
+}
 
 @Component({
   selector : 'iav-cmp-viewer-nehuba-status',
@@ -33,6 +39,41 @@ import { getUuid } from "src/util/fn";
 })
 export class StatusCardComponent {
 
+  #newSpace = new Subject<TSpace>()
+  additionalSpace$ = this.#newSpace.pipe(
+    scan((acc, v) => acc.concat(v), [] as TSpace[])
+  )
+  readonly idAffStr = `[
+  [1, 0, 0, 0],
+  [0, 1, 0, 0],
+  [0, 0, 1, 0],
+  [0, 0, 0, 1]
+]
+`
+  readonly defaultLabel = `New Space`
+  reset(label: HTMLInputElement, affine: HTMLTextAreaElement){
+    label.value = this.defaultLabel
+    affine.value = this.idAffStr
+  }
+  add(label: HTMLInputElement, affine: HTMLTextAreaElement) {
+    try {
+      const aff = JSON.parse(affine.value)
+      if (!isAffine(aff)) {
+        throw new Error(`${affine.value} cannot be parsed into 4x4 affine`)
+      }
+      this.#newSpace.next({
+        label: label.value,
+        affine: aff
+      })
+    } catch (e) {
+      console.error(`Error: ${e.toString()}`)
+    }
+    
+  }
+
+  readonly renderMm: Render = v => v.map(i => `${i}mm`).join(", ")
+  readonly renderDefault: Render = v => v.map(i => i.toFixed(3)).join(", ")
+
   readonly #destroy$ = inject(DestroyDirective).destroyed$
 
   public nehubaViewer: NehubaViewerUnit
@@ -57,30 +98,25 @@ export class StatusCardComponent {
     zoom: number
     perspectiveOrientation: number[]
     perspectiveZoom: number
-}
+  }
 
-  public readonly navVal$ = this.nehubaViewer$.pipe(
+  readonly navigation$ = this.nehubaViewer$.pipe(
     filter(v => !!v),
-    switchMap(nehubaViewer => 
-      concat(
-        of(`nehubaViewer initialising`),
-        nehubaViewer.viewerPosInReal$.pipe(
-          filter(v => !!v),
-          map(real => real.map(v => `${ (v / 1e6).toFixed(3) }mm`).join(', '))
-        )
-      )
-    ),
+    switchMap(nv => nv.viewerPosInReal$.pipe(
+      map(vals => (vals || [0, 0, 0]).map(v => Number((v / 1e6).toFixed(3))))
+    )),
     shareReplay(1),
   )
-  public readonly mouseVal$ = this.nehubaViewer$.pipe(
+
+  readonly navVal$ = this.navigation$.pipe(
+    map(v => v.map(v => `${v}mm`).join(", "))
+  )
+  readonly mouseVal$ = this.nehubaViewer$.pipe(
     filter(v => !!v),
     switchMap(nehubaViewer => 
-      concat(
-        of(``),
-        nehubaViewer.mousePosInReal$.pipe(
-          filter(v => !!v),
-          map(real => real.map(v => `${ (v/1e6).toFixed(3) }mm`).join(', '))
-        )
+      nehubaViewer.mousePosInReal$.pipe(
+        filter(v => !!v),
+        map(real => real.map(v => Number((v/1e6).toFixed(3))))
       ),
     )
   )
diff --git a/src/viewerModule/nehuba/statusCard/statusCard.template.html b/src/viewerModule/nehuba/statusCard/statusCard.template.html
index 57da3b774db7221682614c74c76e1f7d6569d23f..2b17c8e751bf2220ca576e772f96eaea3a9114ea 100644
--- a/src/viewerModule/nehuba/statusCard/statusCard.template.html
+++ b/src/viewerModule/nehuba/statusCard/statusCard.template.html
@@ -47,48 +47,67 @@
 
       <!-- coord -->
       <div class="d-flex">
+        <coordinate-text-input
+          [coordinates]="navigation$ | async"
+          [render]="renderMm"
+          (enter)="textNavigateTo(physCoordInput.inputValue)"
+          label="Physical Coord"
+          #physCoordInput>
+
+          <ng-container ngProjectAs="[suffix]">
+            <button mat-icon-button
+              iav-stop="click"
+              [attr.aria-label]="COPY_NAVIGATION_STRING"
+              (click)="copyString(physCoordInput.inputValue)">
+              <i class="fas fa-copy"></i>
+            </button>
+
+            <button mat-icon-button
+              iav-stop="click"
+              sxplr-share-view
+              [attr.aria-label]="SHARE_BTN_ARIA_LABEL">
+              <i class="fas fa-share-square"></i>
+            </button>
+          </ng-container>
+        </coordinate-text-input>
+      </div>
 
-        <mat-form-field class="flex-grow-1">
-          <mat-label>
-            Physical Coord
-          </mat-label>
-          <input type="text"
-            matInput
-            (keydown.enter)="textNavigateTo(navInput.value)"
-            (keydown.tab)="textNavigateTo(navInput.value)"
-            [value]="navVal$ | async"
-            #navInput="matInput">
-
-          <button mat-icon-button
-            iav-stop="click"
-            matSuffix
-            [attr.aria-label]="COPY_NAVIGATION_STRING"
-            (click)="copyString(navInput.value)">
-            <i class="fas fa-copy"></i>
-          </button>
-
-          <button mat-icon-button
-            iav-stop="click"
-            matSuffix
-            sxplr-share-view
-            [attr.aria-label]="SHARE_BTN_ARIA_LABEL">
-            <i class="fas fa-share-square"></i>
-          </button>
-        </mat-form-field>
-
+      <!-- custom coord -->
+      <div class="d-flex" *ngFor="let f of additionalSpace$ | async">
+        <coordinate-text-input
+          [coordinates]="navigation$ | async"
+          [affine]="f.affine"
+          [label]="f.label"
+          [render]="renderDefault"
+          #customInput>
+
+          <ng-container ngProjectAs="[suffix]">
+            <button mat-icon-button
+              iav-stop="click"
+              [attr.aria-label]="COPY_NAVIGATION_STRING"
+              (click)="copyString(customInput.inputValue)">
+              <i class="fas fa-copy"></i>
+            </button>
+          </ng-container>
+        </coordinate-text-input>
       </div>
 
+      <ng-template sxplrExperimentalFlag [experimental]="true">
+        <button mat-button
+          [sxplr-dialog]="enterNewCoordTmpl"
+          [sxplr-dialog-size]="null">
+          Add Coord Space
+        </button>
+      </ng-template>
+
       <!-- cursor pos -->
-      <mat-form-field
-        class="w-100">
-        <mat-label>
-          Cursor Position
-        </mat-label>
-        <input type="text"
-          matInput
-          [readonly]="true"
-          [value]="mouseVal$ | async">
-      </mat-form-field>
+      <div class="d-flex">
+        <coordinate-text-input
+          [coordinates]="mouseVal$ | async"
+          [render]="renderMm"
+          label="Cursor Position">
+        </coordinate-text-input>
+      </div>
 
     </mat-card-content>
   </mat-card>
@@ -153,4 +172,39 @@
       </button>
     </div>
   </form>
-</ng-template>
\ No newline at end of file
+</ng-template>
+
+<ng-template #enterNewCoordTmpl>
+  <h2 mat-dialog-title>
+    Add a new coordinate space
+  </h2>
+  <mat-dialog-content>
+    <mat-form-field class="d-block">
+      <mat-label>
+        Label
+      </mat-label>
+      <input type="text" matInput [value]="defaultLabel" #labelInput>
+    </mat-form-field>
+
+    
+    <mat-form-field class="d-block">
+      <mat-label>
+        Affine
+      </mat-label>
+      <textarea matInput rows="7" #affineInput>{{ idAffStr }}</textarea>
+    </mat-form-field>
+
+    <mat-dialog-actions>
+      <button mat-button color="primary"
+        (click)="add(labelInput, affineInput)">
+        Add
+      </button>
+      <button mat-button
+        (click)="reset(labelInput, affineInput)">
+        Reset
+      </button>
+    </mat-dialog-actions>
+    
+    
+  </mat-dialog-content>
+</ng-template>