diff --git a/packages/@sanity/types/src/schema/preview.ts b/packages/@sanity/types/src/schema/preview.ts
index d297ce1c056..4e38ec051ce 100644
--- a/packages/@sanity/types/src/schema/preview.ts
+++ b/packages/@sanity/types/src/schema/preview.ts
@@ -1,5 +1,6 @@
 import {type ElementType, type ReactNode} from 'react'
 
+import {type UploadState} from '../upload'
 import {type SortOrdering} from './types'
 
 /** @public */
@@ -9,10 +10,7 @@ export interface PrepareViewOptions {
 }
 
 /** @public */
-export interface PreviewValue {
-  _id?: string
-  _createdAt?: string
-  _updatedAt?: string
+export interface UserPreparedPreviewValue {
   title?: string
   subtitle?: string
   description?: string
@@ -20,11 +18,20 @@ export interface PreviewValue {
   imageUrl?: string
 }
 
+/** @public */
+export interface PreviewValue extends UserPreparedPreviewValue {
+  _id?: string
+  _type?: string
+  _upload?: UploadState
+  _createdAt?: string
+  _updatedAt?: string
+}
+
 /** @public */
 export interface PreviewConfig<
   Select extends Record<string, string> = Record<string, string>,
   PrepareValue extends Record<keyof Select, any> = Record<keyof Select, any>,
 > {
   select?: Select
-  prepare?: (value: PrepareValue, viewOptions?: PrepareViewOptions) => PreviewValue
+  prepare?: (value: PrepareValue, viewOptions?: PrepareViewOptions) => UserPreparedPreviewValue
 }
diff --git a/packages/@sanity/types/src/upload/uploadState.ts b/packages/@sanity/types/src/upload/uploadState.ts
index 44fdbd0a9df..cf1253e945b 100644
--- a/packages/@sanity/types/src/upload/uploadState.ts
+++ b/packages/@sanity/types/src/upload/uploadState.ts
@@ -1,4 +1,4 @@
-/** @internal */
+/** @public */
 export interface UploadState {
   progress: number
   /** @deprecated use createdAt instead */
diff --git a/packages/sanity/package.json b/packages/sanity/package.json
index a09a9f596c1..0b42efbba94 100644
--- a/packages/sanity/package.json
+++ b/packages/sanity/package.json
@@ -167,6 +167,7 @@
     "@sanity/eventsource": "^5.0.0",
     "@sanity/export": "^3.41.0",
     "@sanity/icons": "^3.4.0",
+    "@sanity/id-utils": "^1.0.0",
     "@sanity/image-url": "^1.0.2",
     "@sanity/import": "^3.37.3",
     "@sanity/insert-menu": "1.0.11",
@@ -260,7 +261,9 @@
     "speakingurl": "^14.0.1",
     "tar-fs": "^2.1.1",
     "tar-stream": "^3.1.7",
+    "ts-brand": "^0.2.0",
     "use-device-pixel-ratio": "^1.1.0",
+    "use-effect-event": "^1.0.2",
     "use-hot-module-reload": "^2.0.0",
     "use-sync-external-store": "^1.2.0",
     "vite": "^4.5.1",
diff --git a/packages/sanity/src/core/comments/hooks/useNotificationTarget.ts b/packages/sanity/src/core/comments/hooks/useNotificationTarget.ts
index 1eba3829160..dafd19a6b19 100644
--- a/packages/sanity/src/core/comments/hooks/useNotificationTarget.ts
+++ b/packages/sanity/src/core/comments/hooks/useNotificationTarget.ts
@@ -39,7 +39,7 @@ export function useNotificationTarget(
 
   const previewStateObservable = useMemo(() => {
     if (!documentId || !schemaType) return of(null)
-    return getPreviewStateObservable(documentPreviewStore, schemaType, documentId, '')
+    return getPreviewStateObservable(documentPreviewStore, schemaType, documentId)
   }, [documentId, documentPreviewStore, schemaType])
   const previewState = useObservable(previewStateObservable)
 
diff --git a/packages/sanity/src/core/form/inputs/ReferenceInput/useReferenceInput.tsx b/packages/sanity/src/core/form/inputs/ReferenceInput/useReferenceInput.tsx
index c15d7a513d6..9a093eaec83 100644
--- a/packages/sanity/src/core/form/inputs/ReferenceInput/useReferenceInput.tsx
+++ b/packages/sanity/src/core/form/inputs/ReferenceInput/useReferenceInput.tsx
@@ -8,10 +8,11 @@ import {
   useMemo,
   useRef,
 } from 'react'
-import {usePerspective, useReleases} from 'sanity'
+import {useReleases} from 'sanity'
 
 import {type FIXME} from '../../../FIXME'
 import {useSchema} from '../../../hooks'
+import {useReleasesStack} from '../../../releases/store/useReleasesStack'
 import {useDocumentPreviewStore} from '../../../store'
 import {isNonNullable} from '../../../util'
 import {useFormValue} from '../../contexts/FormValue'
@@ -35,7 +36,7 @@ interface Options {
 export function useReferenceInput(options: Options) {
   const {path, schemaType, version} = options
   const schema = useSchema()
-  const perspective = usePerspective()
+
   const releases = useReleases()
   const documentPreviewStore = useDocumentPreviewStore()
   const {EditReferenceLinkComponent, onEditReference, activePath, initialValueTemplateItems} =
@@ -116,6 +117,7 @@ export function useReferenceInput(options: Options) {
     )
   }, [disableNew, initialValueTemplateItems, schemaType.to])
 
+  const releasesStack = useReleasesStack()
   const getReferenceInfo = useCallback(
     (id: string) =>
       adapter.getReferenceInfo(
@@ -124,17 +126,11 @@ export function useReferenceInput(options: Options) {
         schemaType,
         {version},
         {
-          bundleIds: releases.releasesIds,
-          bundleStack: perspective.bundlesPerspective,
+          releaseIds: releases.releasesIds,
+          bundleStack: releasesStack,
         },
       ),
-    [
-      documentPreviewStore,
-      schemaType,
-      version,
-      releases.releasesIds,
-      perspective.bundlesPerspective,
-    ],
+    [documentPreviewStore, schemaType, version, releases.releasesIds, releasesStack],
   )
 
   return {
diff --git a/packages/sanity/src/core/form/studio/inputs/client-adapters/reference.ts b/packages/sanity/src/core/form/studio/inputs/client-adapters/reference.ts
index aba54c92a58..835a01b170e 100644
--- a/packages/sanity/src/core/form/studio/inputs/client-adapters/reference.ts
+++ b/packages/sanity/src/core/form/studio/inputs/client-adapters/reference.ts
@@ -10,6 +10,7 @@ import {
   type VersionsRecord,
   type VersionTuple,
 } from '../../../../preview/utils/getPreviewStateObservable'
+import {type ReleaseId} from '../../../../releases'
 import {createSearch} from '../../../../search'
 import {
   collate,
@@ -48,7 +49,7 @@ export function getReferenceInfo(
   id: string,
   referenceType: ReferenceSchemaType,
   {version}: {version?: string} = {},
-  perspective: {bundleIds: string[]; bundleStack: string[]} = {bundleIds: [], bundleStack: []},
+  perspective: {releaseIds: ReleaseId[]; bundleStack: string[]} = {releaseIds: [], bundleStack: []},
 ): Observable<ReferenceInfo> {
   const {publishedId, draftId, versionId} = getIdPair(id, {version})
 
@@ -144,28 +145,30 @@ export function getReferenceInfo(
             refSchemaType,
           )
 
-          const versions$ = from(perspective.bundleIds).pipe(
-            mergeMap<string, Observable<VersionTuple>>((bundleId) =>
+          const versions$ = from(perspective.releaseIds).pipe(
+            mergeMap((bundleId) =>
               documentPreviewStore
                 .observePaths({_id: getVersionId(id, bundleId)}, previewPaths)
                 .pipe(
-                  // eslint-disable-next-line max-nested-callbacks
-                  map((result) =>
-                    result
-                      ? [
-                          bundleId,
-                          {
-                            snapshot: {
-                              _id: versionId,
-                              ...prepareForPreview(result, refSchemaType),
+                  map(
+                    // eslint-disable-next-line max-nested-callbacks
+                    (result): VersionTuple =>
+                      result
+                        ? [
+                            bundleId,
+                            {
+                              snapshot: {
+                                _id: versionId,
+                                ...prepareForPreview(result, refSchemaType),
+                              },
                             },
-                          },
-                        ]
-                      : [bundleId, {snapshot: null}],
+                          ]
+                        : [bundleId, {snapshot: undefined}],
                   ),
                 ),
             ),
-            scan((byBundleId, [bundleId, value]) => {
+
+            scan((byBundleId: VersionsRecord, [bundleId, value]) => {
               if (value.snapshot === null) {
                 return omit({...byBundleId}, [bundleId])
               }
diff --git a/packages/sanity/src/core/index.ts b/packages/sanity/src/core/index.ts
index fb778de8f60..fd23f110b76 100644
--- a/packages/sanity/src/core/index.ts
+++ b/packages/sanity/src/core/index.ts
@@ -26,17 +26,22 @@ export {
   AddedVersion,
   DiscardVersionDialog,
   getBundleIdFromReleaseDocumentId,
+  getPerspectiveTone,
   getPublishDateFromRelease,
+  getReleaseIdFromReleaseDocumentId,
   getReleaseTone,
   isDraftPerspective,
   isPublishedPerspective,
   isReleaseDocument,
   isReleaseScheduledOrScheduling,
   LATEST,
+  PUBLISHED_PERSPECTIVE,
   type ReleaseDocument,
+  useCurrentRelease,
   useDocumentVersions,
-  usePerspective,
   useReleases,
+  useReleasesStack,
+  useStudioPerspectiveState,
   useVersionOperations,
   VersionChip,
   versionDocumentExists,
diff --git a/packages/sanity/src/core/preview/createPathObserver.ts b/packages/sanity/src/core/preview/createPathObserver.ts
index 3400973382a..16c281f2d28 100644
--- a/packages/sanity/src/core/preview/createPathObserver.ts
+++ b/packages/sanity/src/core/preview/createPathObserver.ts
@@ -36,12 +36,7 @@ function observePaths(
   paths: PreviewPath[],
   observeFields: ObserveFieldsFn,
   apiConfig?: ApiConfig,
-): Observable<Record<string, unknown> | null> {
-  if (!value || typeof value !== 'object') {
-    // Reached a leaf. Return as is
-    return observableOf(value as null) // @todo
-  }
-
+): Observable<Record<string, unknown> | undefined> {
   const id = getDocumentId(value)
 
   const currentValue: Record<string, unknown> = id ? {...value, _id: id} : {...value}
@@ -70,7 +65,7 @@ function observePaths(
     return observeFields(id, nextHeads, refApiConfig).pipe(
       switchMap((snapshot) => {
         if (snapshot === null) {
-          return observableOf(null)
+          return observableOf(undefined)
         }
 
         return observePaths(
@@ -132,7 +127,7 @@ export function createPathObserver(options: {observeFields: ObserveFieldsFn}) {
     value: Previewable,
     paths: (FieldName | PreviewPath)[],
     apiConfig?: ApiConfig,
-  ): Observable<Record<string, unknown> | null> => {
+  ): Observable<Record<string, unknown> | undefined> => {
     return observePaths(value, normalizePaths(paths), observeFields, apiConfig)
   }
 }
diff --git a/packages/sanity/src/core/preview/createPreviewObserver.ts b/packages/sanity/src/core/preview/createPreviewObserver.ts
index 20bde34d05f..2966cd31652 100644
--- a/packages/sanity/src/core/preview/createPreviewObserver.ts
+++ b/packages/sanity/src/core/preview/createPreviewObserver.ts
@@ -18,7 +18,7 @@ import {
   type PreviewableType,
 } from './types'
 import {getPreviewPaths} from './utils/getPreviewPaths'
-import {invokePrepare, prepareForPreview} from './utils/prepareForPreview'
+import {prepareForPreview} from './utils/prepareForPreview'
 
 function isRecord(value: unknown): value is Record<string, unknown> {
   return isPlainObject(value)
@@ -99,7 +99,7 @@ export function createPreviewObserver(context: {
       return observePaths(value, paths, apiConfig).pipe(
         map((snapshot) => ({
           type: type,
-          snapshot: snapshot ? prepareForPreview(snapshot, type, viewOptions) : null,
+          snapshot: snapshot ? prepareForPreview(snapshot, type, viewOptions) : undefined,
         })),
       )
     }
@@ -110,8 +110,7 @@ export function createPreviewObserver(context: {
     // `file`s, and `document`s
     return of({
       type,
-      snapshot:
-        value && isRecord(value) ? invokePrepare(type, value, viewOptions).returnValue : null,
+      snapshot: value && isRecord(value) ? prepareForPreview(value, type, viewOptions) : undefined,
     })
   }
 }
diff --git a/packages/sanity/src/core/preview/documentPair.ts b/packages/sanity/src/core/preview/documentPair.ts
index 1d618af1a38..40bfa926242 100644
--- a/packages/sanity/src/core/preview/documentPair.ts
+++ b/packages/sanity/src/core/preview/documentPair.ts
@@ -39,7 +39,7 @@ export function createObservePathsDocumentPair(options: {
           // short circuit, neither draft nor published is available so no point in trying to get a snapshot
           return of({
             id: publishedId,
-            type: null,
+            type: undefined,
             draft: {
               availability: availability.draft,
               snapshot: undefined,
@@ -74,11 +74,11 @@ export function createObservePathsDocumentPair(options: {
               (isRecord(publishedSnapshot) &&
                 '_type' in publishedSnapshot &&
                 publishedSnapshot._type) ||
-              null
+              undefined
 
             return {
               id: publishedId,
-              type: typeof type === 'string' ? type : null,
+              type: typeof type === 'string' ? type : undefined,
               draft: {
                 availability: availability.draft,
                 snapshot: draftSnapshot as T,
diff --git a/packages/sanity/src/core/preview/types.ts b/packages/sanity/src/core/preview/types.ts
index 37d57011b17..39ddd478033 100644
--- a/packages/sanity/src/core/preview/types.ts
+++ b/packages/sanity/src/core/preview/types.ts
@@ -1,7 +1,6 @@
 import {
   type CrossDatasetType,
   type PreviewValue,
-  type Reference,
   type SanityDocumentLike,
   type SchemaType,
 } from '@sanity/types'
@@ -39,6 +38,11 @@ export type PreviewPath = FieldName[]
 /** @internal */
 export type Selection = [Id, FieldName[]]
 
+/** @internal */
+export interface PartialPreviewDocument {
+  _id: string
+  _type: string
+}
 /**
  * @hidden
  * @beta */
@@ -104,7 +108,7 @@ export interface DraftsModelDocumentAvailability {
  * @beta */
 export interface DraftsModelDocument<T extends SanityDocumentLike = SanityDocumentLike> {
   id: string
-  type: string | null
+  type: string | undefined
   draft: {
     availability: DocumentAvailability
     snapshot: T | undefined
@@ -135,7 +139,7 @@ export type InvalidationChannelEvent =
  * @beta */
 export interface PreparedSnapshot {
   type?: PreviewableType
-  snapshot: PreviewValue | null | undefined
+  snapshot: PreviewValue | undefined
 }
 
 /** @internal */
@@ -152,7 +156,7 @@ export interface ObservePathsFn {
     value: Previewable,
     paths: (string | PreviewPath)[],
     apiConfig?: ApiConfig,
-  ): Observable<PreviewValue | SanityDocumentLike | Reference | string | null>
+  ): Observable<Record<string, unknown> | undefined>
 }
 
 /**
diff --git a/packages/sanity/src/core/preview/utils/getPreviewStateObservable.ts b/packages/sanity/src/core/preview/utils/getPreviewStateObservable.ts
index 12c93ed5fcc..9403c05deff 100644
--- a/packages/sanity/src/core/preview/utils/getPreviewStateObservable.ts
+++ b/packages/sanity/src/core/preview/utils/getPreviewStateObservable.ts
@@ -1,19 +1,19 @@
 import {type PreviewValue, type SanityDocument, type SchemaType} from '@sanity/types'
 import {omit} from 'lodash'
-import {type ReactNode} from 'react'
 import {combineLatest, from, type Observable, of} from 'rxjs'
 import {map, mergeMap, scan, startWith} from 'rxjs/operators'
 import {type PreparedSnapshot} from 'sanity'
 
+import {type ReleaseId} from '../../releases'
 import {getDraftId, getPublishedId, getVersionId} from '../../util/draftUtils'
 import {type DocumentPreviewStore} from '../documentPreviewStore'
 
 /**
  * @internal
  */
-export type VersionsRecord = Record<string, PreparedSnapshot>
+export type VersionsRecord = Record<ReleaseId, PreparedSnapshot>
 
-export type VersionTuple = [bundleId: string, snapshot: PreparedSnapshot]
+export type VersionTuple = [bundleId: ReleaseId, snapshot: PreparedSnapshot]
 
 export interface PreviewState {
   isLoading?: boolean
@@ -34,78 +34,50 @@ export function getPreviewStateObservable(
   documentPreviewStore: DocumentPreviewStore,
   schemaType: SchemaType,
   documentId: string,
-  title: ReactNode,
-  perspective: {
-    /**
-     * An array of all existing bundle ids.
-     */
-    bundleIds: string[]
-
-    /**
-     * An array of release ids ordered chronologically to represent the state of documents at the
-     * given point in time.
-     */
-    bundleStack: string[]
-  } = {
-    bundleIds: [],
-    bundleStack: [],
-  },
+  /**
+   * What additional releases to fetch versions from
+   */
+  releases: ReleaseId[] = [],
 ): Observable<PreviewState> {
   const draft$ = isLiveEditEnabled(schemaType)
     ? of({snapshot: null})
     : documentPreviewStore.observeForPreview({_id: getDraftId(documentId)}, schemaType)
 
-  const versions$ = from(perspective.bundleIds).pipe(
-    mergeMap<string, Observable<VersionTuple>>((bundleId) =>
+  const versions$ = from(releases).pipe(
+    mergeMap((release) =>
       documentPreviewStore
-        .observeForPreview({_id: getVersionId(documentId, bundleId)}, schemaType)
-        .pipe(map((storeValue) => [bundleId, storeValue])),
+        .observeForPreview({_id: getVersionId(documentId, release)}, schemaType)
+        .pipe(map((storeValue): VersionTuple => [release, storeValue])),
     ),
-    scan<VersionTuple, VersionsRecord>((byBundleId, [bundleId, value]) => {
-      if (value.snapshot === null) {
-        return omit({...byBundleId}, [bundleId])
+    scan((byVersionId, [releaseId, value]) => {
+      if (value.snapshot === undefined) {
+        return omit({...byVersionId}, [releaseId])
       }
 
       return {
-        ...byBundleId,
-        [bundleId]: value,
+        ...byVersionId,
+        [releaseId]: value,
       }
     }, {}),
     startWith<VersionsRecord>({}),
   )
 
-  // Iterate the release stack in descending precedence, returning the highest precedence existing
-  // version document.
-  const version$ = versions$.pipe(
-    map((versions) => {
-      for (const bundleId of perspective.bundleStack) {
-        if (bundleId in versions) {
-          return versions[bundleId]
-        }
-      }
-      return {snapshot: null}
-    }),
-    startWith<PreparedSnapshot>({snapshot: null}),
-  )
-
   const published$ = documentPreviewStore.observeForPreview(
     {_id: getPublishedId(documentId)},
     schemaType,
   )
 
-  return combineLatest([draft$, published$, version$, versions$]).pipe(
-    map(([draft, published, version, versions]) => ({
-      draft: draft.snapshot ? {title, ...(draft.snapshot || {})} : null,
+  return combineLatest([draft$, published$, versions$]).pipe(
+    map(([draft, published, versions]) => ({
+      draft: draft.snapshot,
       isLoading: false,
-      published: published.snapshot ? {title, ...(published.snapshot || {})} : null,
-      version: version.snapshot ? {title, ...(version.snapshot || {})} : null,
+      published: published.snapshot,
       versions,
     })),
     startWith({
-      draft: null,
+      draft: undefined,
       isLoading: true,
-      published: null,
-      version: null,
+      published: undefined,
       versions: {},
     }),
   )
diff --git a/packages/sanity/src/core/preview/utils/prepareForPreview.ts b/packages/sanity/src/core/preview/utils/prepareForPreview.ts
index f07f16f421b..ef998e40b4d 100644
--- a/packages/sanity/src/core/preview/utils/prepareForPreview.ts
+++ b/packages/sanity/src/core/preview/utils/prepareForPreview.ts
@@ -4,6 +4,7 @@ import {
   type PreviewValue,
   type SchemaType,
   type TitledListValue,
+  type UserPreparedPreviewValue,
 } from '@sanity/types'
 import {debounce, flatten, get, isPlainObject, pick, uniqBy} from 'lodash'
 
@@ -13,14 +14,14 @@ import {type PreviewableType} from '../types'
 import {keysOf} from './keysOf'
 import {extractTextFromBlocks, isPortableTextPreviewValue} from './portableText'
 
-const PRESERVE_KEYS = ['_id', '_type', '_upload', '_createdAt', '_updatedAt']
+const PRESERVE_KEYS = ['_id', '_type', '_upload', '_createdAt', '_updatedAt'] as const
 const EMPTY: never[] = []
 
 type SelectedValue = Record<string, unknown>
 
 export type PrepareInvocationResult = {
   selectedValue?: SelectedValue
-  returnValue: null | PreviewValue
+  returnValue: UserPreparedPreviewValue | undefined
   errors: Error[]
 }
 
@@ -163,8 +164,8 @@ function assignType(type: string, error: Error) {
   return Object.assign(error, {type})
 }
 
-function validatePreparedValue(preparedValue: PreviewValue | null) {
-  if (!isPlainObject(preparedValue) || preparedValue === null) {
+function validatePreparedValue(preparedValue: UserPreparedPreviewValue | undefined) {
+  if (!isPlainObject(preparedValue) || preparedValue === undefined) {
     return [
       assignType(
         'returnValueError',
@@ -189,7 +190,7 @@ function validateReturnedPreview(result: PrepareInvocationResult) {
   }
 }
 
-function defaultPrepare(value: SelectedValue) {
+function defaultPrepare(value: SelectedValue): UserPreparedPreviewValue {
   return keysOf(value).reduce((acc: SelectedValue, fieldName: keyof SelectedValue) => {
     const val = value[fieldName]
     return {
@@ -207,14 +208,12 @@ export function invokePrepare(
   const prepare = type.preview?.prepare
   try {
     return {
-      returnValue: prepare
-        ? (prepare(value, viewOptions) as Record<string, unknown>)
-        : defaultPrepare(value),
+      returnValue: prepare ? prepare(value, viewOptions) : defaultPrepare(value),
       errors: EMPTY,
     }
   } catch (error) {
     return {
-      returnValue: null,
+      returnValue: undefined,
       errors: [assignType('prepareError', error)],
     }
   }
@@ -298,7 +297,9 @@ export function prepareForPreview(
   }
 
   const returnValueResult = validateReturnedPreview(prepareResult)
-  return returnValueResult.errors.length > 0
-    ? withErrors(returnValueResult, type, selectedValue)
-    : {...pick(rawValue, PRESERVE_KEYS), ...prepareResult.returnValue}
+  return (
+    returnValueResult.errors.length > 0
+      ? withErrors(returnValueResult, type, selectedValue)
+      : {...pick(rawValue, PRESERVE_KEYS), ...prepareResult.returnValue}
+  ) as PreviewValue
 }
diff --git a/packages/sanity/src/core/releases/components/dialog/CreateReleaseDialog.tsx b/packages/sanity/src/core/releases/components/dialog/CreateReleaseDialog.tsx
index b4e384692ce..7ff6d93a12c 100644
--- a/packages/sanity/src/core/releases/components/dialog/CreateReleaseDialog.tsx
+++ b/packages/sanity/src/core/releases/components/dialog/CreateReleaseDialog.tsx
@@ -9,8 +9,8 @@ import {CreatedRelease, type OriginInfo} from '../../__telemetry__/releases.tele
 import {type EditableReleaseDocument} from '../../store/types'
 import {useReleaseOperations} from '../../store/useReleaseOperations'
 import {DEFAULT_RELEASE_TYPE} from '../../util/const'
-import {createReleaseId} from '../../util/createReleaseId'
 import {getBundleIdFromReleaseDocumentId} from '../../util/getBundleIdFromReleaseDocumentId'
+import {generateReleaseDocumentId} from '../../util/releaseId'
 import {ReleaseForm} from './ReleaseForm'
 
 interface CreateReleaseDialogProps {
@@ -28,7 +28,7 @@ export function CreateReleaseDialog(props: CreateReleaseDialogProps): JSX.Elemen
 
   const [value, setValue] = useState((): EditableReleaseDocument => {
     return {
-      _id: createReleaseId(),
+      _id: generateReleaseDocumentId(),
       metadata: {
         releaseType: DEFAULT_RELEASE_TYPE,
       },
diff --git a/packages/sanity/src/core/releases/components/dialog/DiscardVersionDialog.tsx b/packages/sanity/src/core/releases/components/dialog/DiscardVersionDialog.tsx
index 1b6af447508..68587e06c9b 100644
--- a/packages/sanity/src/core/releases/components/dialog/DiscardVersionDialog.tsx
+++ b/packages/sanity/src/core/releases/components/dialog/DiscardVersionDialog.tsx
@@ -1,3 +1,4 @@
+import {DocumentId, getPublishedId, getVersionNameFromId, isVersionId} from '@sanity/id-utils'
 import {Box} from '@sanity/ui'
 import {useCallback, useState} from 'react'
 
@@ -6,11 +7,9 @@ import {LoadingBlock} from '../../../components'
 import {useDocumentOperation, useSchema} from '../../../hooks'
 import {useTranslation} from '../../../i18n'
 import {Preview} from '../../../preview'
-import {getPublishedId, getVersionFromId, isVersionId} from '../../../util/draftUtils'
-import {usePerspective, useVersionOperations} from '../../hooks'
+import {useVersionOperations} from '../../hooks'
 import {releasesLocaleNamespace} from '../../i18n'
-import {type ReleaseDocument} from '../../store'
-import {getBundleIdFromReleaseDocumentId} from '../../util/getBundleIdFromReleaseDocumentId'
+import {ReleaseId} from '../../util/releaseId'
 
 /**
  * @internal
@@ -20,11 +19,11 @@ export function DiscardVersionDialog(props: {
   documentId: string
   documentType: string
 }): JSX.Element {
-  const {onClose, documentId, documentType} = props
+  const {onClose, documentType} = props
+  const documentId = DocumentId(props.documentId)
   const {t} = useTranslation(releasesLocaleNamespace)
   const {discardChanges} = useDocumentOperation(getPublishedId(documentId), documentType)
 
-  const {currentGlobalBundle} = usePerspective()
   const {discardVersion} = useVersionOperations()
   const schema = useSchema()
   const [isDiscarding, setIsDiscarding] = useState(false)
@@ -35,11 +34,7 @@ export function DiscardVersionDialog(props: {
     setIsDiscarding(true)
 
     if (isVersionId(documentId)) {
-      await discardVersion(
-        getVersionFromId(documentId) ||
-          getBundleIdFromReleaseDocumentId((currentGlobalBundle as ReleaseDocument)._id),
-        documentId,
-      )
+      await discardVersion(ReleaseId(getVersionNameFromId(documentId)), documentId)
     } else {
       // on the document header you can also discard the draft
       discardChanges.execute()
@@ -48,7 +43,7 @@ export function DiscardVersionDialog(props: {
     setIsDiscarding(false)
 
     onClose()
-  }, [currentGlobalBundle, discardChanges, discardVersion, documentId, onClose])
+  }, [discardChanges, discardVersion, documentId, onClose])
 
   return (
     <Dialog
diff --git a/packages/sanity/src/core/releases/components/documentHeader/VersionChip.tsx b/packages/sanity/src/core/releases/components/documentHeader/VersionChip.tsx
index 4d3199333a1..47ab42051a3 100644
--- a/packages/sanity/src/core/releases/components/documentHeader/VersionChip.tsx
+++ b/packages/sanity/src/core/releases/components/documentHeader/VersionChip.tsx
@@ -17,6 +17,7 @@ import {getVersionId} from '../../../util/draftUtils'
 import {useVersionOperations} from '../../hooks/useVersionOperations'
 import {type ReleaseDocument} from '../../store/types'
 import {getBundleIdFromReleaseDocumentId} from '../../util/getBundleIdFromReleaseDocumentId'
+import {getReleaseIdFromReleaseDocumentId, type ReleaseDocumentId} from '../../util/releaseId'
 import {DiscardVersionDialog} from '../dialog/DiscardVersionDialog'
 import {ReleaseAvatar} from '../ReleaseAvatar'
 import {VersionContextMenu} from './contextMenu/VersionContextMenu'
@@ -134,8 +135,8 @@ export const VersionChip = memo(function VersionChip(props: {
   }, [setIsCreateReleaseDialogOpen])
 
   const handleAddVersion = useCallback(
-    async (targetRelease: string) => {
-      await createVersion(getBundleIdFromReleaseDocumentId(targetRelease), docId)
+    async (targetRelease: ReleaseDocumentId) => {
+      await createVersion(getReleaseIdFromReleaseDocumentId(targetRelease), docId)
       close()
     },
     [createVersion, docId, close],
diff --git a/packages/sanity/src/core/releases/components/documentHeader/contextMenu/VersionContextMenu.tsx b/packages/sanity/src/core/releases/components/documentHeader/contextMenu/VersionContextMenu.tsx
index 673ebfcd04c..c6431137e7c 100644
--- a/packages/sanity/src/core/releases/components/documentHeader/contextMenu/VersionContextMenu.tsx
+++ b/packages/sanity/src/core/releases/components/documentHeader/contextMenu/VersionContextMenu.tsx
@@ -9,6 +9,7 @@ import {MenuItem} from '../../../../../ui-components/menuItem/MenuItem'
 import {useTranslation} from '../../../../i18n/hooks/useTranslation'
 import {isPublishedId} from '../../../../util/draftUtils'
 import {type ReleaseDocument} from '../../../store/types'
+import {type ReleaseDocumentId} from '../../../util/releaseId'
 import {isReleaseScheduledOrScheduling} from '../../../util/util'
 import {VersionContextMenuItem} from './VersionContextMenuItem'
 
@@ -26,7 +27,7 @@ export const VersionContextMenu = memo(function VersionContextMenu(props: {
   isVersion: boolean
   onDiscard: () => void
   onCreateRelease: () => void
-  onCreateVersion: (targetId: string) => void
+  onCreateVersion: (targetId: ReleaseDocumentId) => void
   disabled?: boolean
   locked?: boolean
 }) {
diff --git a/packages/sanity/src/core/releases/components/documentHeader/dialog/CopyToNewReleaseDialog.tsx b/packages/sanity/src/core/releases/components/documentHeader/dialog/CopyToNewReleaseDialog.tsx
index 2798299014d..0ee757c9117 100644
--- a/packages/sanity/src/core/releases/components/documentHeader/dialog/CopyToNewReleaseDialog.tsx
+++ b/packages/sanity/src/core/releases/components/documentHeader/dialog/CopyToNewReleaseDialog.tsx
@@ -11,7 +11,7 @@ import {CreatedRelease} from '../../../__telemetry__/releases.telemetry'
 import {type EditableReleaseDocument} from '../../../store/types'
 import {useReleaseOperations} from '../../../store/useReleaseOperations'
 import {DEFAULT_RELEASE_TYPE} from '../../../util/const'
-import {createReleaseId} from '../../../util/createReleaseId'
+import {generateReleaseDocumentId, type ReleaseDocumentId} from '../../../util/releaseId'
 import {ReleaseForm} from '../../dialog/ReleaseForm'
 import {ReleaseAvatar} from '../../ReleaseAvatar'
 
@@ -21,7 +21,7 @@ export function CopyToNewReleaseDialog(props: {
   documentType: string
   tone: BadgeTone
   title: string
-  onCreateVersion: (releaseId: string) => void
+  onCreateVersion: (releaseId: ReleaseDocumentId) => void
 }): JSX.Element {
   const {onClose, documentId, documentType, tone, title, onCreateVersion} = props
   const {t} = useTranslation()
@@ -30,7 +30,7 @@ export function CopyToNewReleaseDialog(props: {
   const schema = useSchema()
   const schemaType = schema.get(documentType)
 
-  const [newReleaseId] = useState(createReleaseId())
+  const [newReleaseId] = useState(generateReleaseDocumentId())
 
   const [value, setValue] = useState((): EditableReleaseDocument => {
     return {
diff --git a/packages/sanity/src/core/releases/hooks/__tests__/utils.test.ts b/packages/sanity/src/core/releases/hooks/__tests__/utils.test.ts
index 79acfd79365..aa26ecb4c1d 100644
--- a/packages/sanity/src/core/releases/hooks/__tests__/utils.test.ts
+++ b/packages/sanity/src/core/releases/hooks/__tests__/utils.test.ts
@@ -2,9 +2,10 @@ import {describe, expect, it} from 'vitest'
 
 import {RELEASE_DOCUMENT_TYPE} from '../../store/constants'
 import {type ReleaseDocument} from '../../store/types'
-import {createReleaseId} from '../../util/createReleaseId'
 import {getBundleIdFromReleaseDocumentId} from '../../util/getBundleIdFromReleaseDocumentId'
-import {getReleasesPerspective, sortReleases} from '../utils'
+import {type SelectableReleasePerspective} from '../../util/perspective'
+import {generateReleaseDocumentId, ReleaseDocumentId} from '../../util/releaseId'
+import {getReleasesStack, sortReleases} from '../utils'
 
 function createReleaseMock(
   value: Partial<
@@ -13,10 +14,10 @@ function createReleaseMock(
     }
   >,
 ): ReleaseDocument {
-  const id = value._id || createReleaseId()
+  const id = value._id || generateReleaseDocumentId()
   const name = getBundleIdFromReleaseDocumentId(id)
   return {
-    _id: id,
+    _id: ReleaseDocumentId(id),
     _type: RELEASE_DOCUMENT_TYPE,
     _createdAt: new Date().toISOString(),
     _updatedAt: new Date().toISOString(),
@@ -35,14 +36,14 @@ describe('sortReleases()', () => {
   it('should return the asap releases ordered by createdAt', () => {
     const releases: ReleaseDocument[] = [
       createReleaseMock({
-        _id: '_.releases.asap1',
+        _id: ReleaseDocumentId('_.releases.asap1'),
         _createdAt: '2024-10-24T00:00:00Z',
         metadata: {
           releaseType: 'asap',
         },
       }),
       createReleaseMock({
-        _id: '_.releases.asap2',
+        _id: ReleaseDocumentId('_.releases.asap2'),
         _createdAt: '2024-10-25T00:00:00Z',
         metadata: {
           releaseType: 'asap',
@@ -58,21 +59,21 @@ describe('sortReleases()', () => {
   it('should return the scheduled releases ordered by intendedPublishAt or publishAt', () => {
     const releases: ReleaseDocument[] = [
       createReleaseMock({
-        _id: '_.releases.future2',
+        _id: ReleaseDocumentId('_.releases.future2'),
         metadata: {
           releaseType: 'scheduled',
           intendedPublishAt: '2024-11-25T00:00:00Z',
         },
       }),
       createReleaseMock({
-        _id: '_.releases.future1',
+        _id: ReleaseDocumentId('_.releases.future1'),
         metadata: {
           releaseType: 'scheduled',
           intendedPublishAt: '2024-11-23T00:00:00Z',
         },
       }),
       createReleaseMock({
-        _id: '_.releases.future4',
+        _id: ReleaseDocumentId('_.releases.future4'),
         state: 'scheduled',
         publishAt: '2024-11-31T00:00:00Z',
         metadata: {
@@ -81,7 +82,7 @@ describe('sortReleases()', () => {
         },
       }),
       createReleaseMock({
-        _id: '_.releases.future3',
+        _id: ReleaseDocumentId('_.releases.future3'),
         state: 'scheduled',
         publishAt: '2024-11-26T00:00:00Z',
         metadata: {
@@ -99,14 +100,14 @@ describe('sortReleases()', () => {
   it('should return the undecided releases ordered by createdAt', () => {
     const releases: ReleaseDocument[] = [
       createReleaseMock({
-        _id: '_.releases.undecided1',
+        _id: ReleaseDocumentId('_.releases.undecided1'),
         _createdAt: '2024-10-25T00:00:00Z',
         metadata: {
           releaseType: 'undecided',
         },
       }),
       createReleaseMock({
-        _id: '_.releases.undecided2',
+        _id: ReleaseDocumentId('_.releases.undecided2'),
         _createdAt: '2024-10-26T00:00:00Z',
         metadata: {
           releaseType: 'undecided',
@@ -122,28 +123,28 @@ describe('sortReleases()', () => {
   it("should gracefully combine all release types, and sort them by 'undecided', 'scheduled', 'asap'", () => {
     const releases = [
       createReleaseMock({
-        _id: '_.releases.asap2',
+        _id: ReleaseDocumentId('_.releases.asap2'),
         _createdAt: '2024-10-25T00:00:00Z',
         metadata: {
           releaseType: 'asap',
         },
       }),
       createReleaseMock({
-        _id: '_.releases.asap1',
+        _id: ReleaseDocumentId('_.releases.asap1'),
         _createdAt: '2024-10-24T00:00:00Z',
         metadata: {
           releaseType: 'asap',
         },
       }),
       createReleaseMock({
-        _id: '_.releases.undecided2',
+        _id: ReleaseDocumentId('_.releases.undecided2'),
         _createdAt: '2024-10-26T00:00:00Z',
         metadata: {
           releaseType: 'undecided',
         },
       }),
       createReleaseMock({
-        _id: '_.releases.future4',
+        _id: ReleaseDocumentId('_.releases.future4'),
         state: 'scheduled',
         publishAt: '2024-11-31T00:00:00Z',
         metadata: {
@@ -152,7 +153,7 @@ describe('sortReleases()', () => {
         },
       }),
       createReleaseMock({
-        _id: '_.releases.future1',
+        _id: ReleaseDocumentId('_.releases.future1'),
         metadata: {
           releaseType: 'scheduled',
           intendedPublishAt: '2024-11-23T00:00:00Z',
@@ -170,28 +171,28 @@ describe('sortReleases()', () => {
 describe('getReleasesPerspective()', () => {
   const releases = [
     createReleaseMock({
-      _id: '_.releases.asap2',
+      _id: ReleaseDocumentId('_.releases.asap2'),
       _createdAt: '2024-10-25T00:00:00Z',
       metadata: {
         releaseType: 'asap',
       },
     }),
     createReleaseMock({
-      _id: '_.releases.asap1',
+      _id: ReleaseDocumentId('_.releases.asap1'),
       _createdAt: '2024-10-24T00:00:00Z',
       metadata: {
         releaseType: 'asap',
       },
     }),
     createReleaseMock({
-      _id: '_.releases.undecided2',
+      _id: ReleaseDocumentId('_.releases.undecided2'),
       _createdAt: '2024-10-26T00:00:00Z',
       metadata: {
         releaseType: 'undecided',
       },
     }),
     createReleaseMock({
-      _id: '_.releases.future4',
+      _id: ReleaseDocumentId('_.releases.future4'),
       state: 'scheduled',
       publishAt: '2024-11-31T00:00:00Z',
       metadata: {
@@ -200,7 +201,7 @@ describe('getReleasesPerspective()', () => {
       },
     }),
     createReleaseMock({
-      _id: '_.releases.future1',
+      _id: ReleaseDocumentId('_.releases.future1'),
       metadata: {
         releaseType: 'scheduled',
         intendedPublishAt: '2024-11-23T00:00:00Z',
@@ -219,13 +220,13 @@ describe('getReleasesPerspective()', () => {
     {
       perspective: 'bundle.undecided2',
       excluded: ['future1', 'drafts'],
-      expected: ['undecided2', 'future4', 'asap2', 'asap1'],
+      expected: ['undecided2', 'future4', 'asap2', 'asap1'] as SelectableReleasePerspective[],
     },
   ]
   it.each(testCases)(
     'should return the correct release stack for %s',
     ({perspective, excluded, expected}) => {
-      const result = getReleasesPerspective({releases, perspective, excluded})
+      const result = getReleasesStack({releases, perspective, excluded})
       expect(result).toEqual(expected)
     },
   )
diff --git a/packages/sanity/src/core/releases/hooks/index.ts b/packages/sanity/src/core/releases/hooks/index.ts
index 0ed88aee8c3..642e8468382 100644
--- a/packages/sanity/src/core/releases/hooks/index.ts
+++ b/packages/sanity/src/core/releases/hooks/index.ts
@@ -1,3 +1,4 @@
+export * from './useCurrentRelease'
 export * from './useDocumentVersions'
-export * from './usePerspective'
+export * from './useStudioPerspectiveState'
 export * from './useVersionOperations'
diff --git a/packages/sanity/src/core/releases/hooks/useCurrentRelease.ts b/packages/sanity/src/core/releases/hooks/useCurrentRelease.ts
new file mode 100644
index 00000000000..7cc51825576
--- /dev/null
+++ b/packages/sanity/src/core/releases/hooks/useCurrentRelease.ts
@@ -0,0 +1,9 @@
+import {useReleases} from '../store'
+import {getReleaseIdFromReleaseDocumentId} from '../util/releaseId'
+import {useStudioPerspectiveState} from './useStudioPerspectiveState'
+
+export function useCurrentRelease() {
+  const {current} = useStudioPerspectiveState()
+  const releases = useReleases()
+  return releases.data.find((release) => getReleaseIdFromReleaseDocumentId(release._id) === current)
+}
diff --git a/packages/sanity/src/core/releases/hooks/usePerspective.tsx b/packages/sanity/src/core/releases/hooks/usePerspective.tsx
deleted file mode 100644
index 146a58f2232..00000000000
--- a/packages/sanity/src/core/releases/hooks/usePerspective.tsx
+++ /dev/null
@@ -1,173 +0,0 @@
-import {useCallback, useMemo} from 'react'
-import {useRouter} from 'sanity/router'
-
-import {resolveBundlePerspective} from '../../util/resolvePerspective'
-import {type ReleaseDocument} from '../store/types'
-import {useReleases} from '../store/useReleases'
-import {LATEST} from '../util/const'
-import {getBundleIdFromReleaseDocumentId} from '../util/getBundleIdFromReleaseDocumentId'
-import {isPublishedPerspective} from '../util/util'
-import {getReleasesPerspective} from './utils'
-
-/**
- * @internal
- */
-export type CurrentPerspective = ReleaseDocument | 'published' | typeof LATEST
-
-/**
- * @internal
- */
-export interface PerspectiveValue {
-  /* The current perspective */
-  perspective: 'published' | `bundle.${string}` | undefined
-
-  /* The excluded perspectives */
-  excludedPerspectives: string[]
-  /* Return the current global release */
-  currentGlobalBundle: CurrentPerspective
-  /* Change the perspective in the studio based on the perspective name */
-  setPerspective: (perspectiveId: string) => void
-  /* change the perspective in the studio based on a release ID */
-  setPerspectiveFromReleaseDocumentId: (releaseDocumentId: string) => void
-  setPerspectiveFromReleaseId: (releaseId: string) => void
-  /* Add/remove excluded perspectives */
-  toggleExcludedPerspective: (perspectiveId: string) => void
-  /* Check if a perspective is excluded */
-  isPerspectiveExcluded: (perspectiveId: string) => boolean
-  /**
-   * The stacked array of releases ids ordered chronologically to represent the state of documents at the given point in time.
-   */
-  bundlesPerspective: string[]
-  /* */
-  currentGlobalBundleId: string
-}
-
-const EMPTY_ARRAY: string[] = []
-/**
- * TODO: Improve distinction between global and pane perspectives.
- *
- * @internal
- */
-export function usePerspective(): PerspectiveValue {
-  const router = useRouter()
-  const {data: releases} = useReleases()
-  // TODO: Actually validate the perspective value, if it's not a valid perspective, we should fallback to undefined
-  const perspective = router.stickyParams.perspective as
-    | 'published'
-    | `bundle.${string}`
-    | undefined
-
-  const excludedPerspectives = useMemo(
-    () => router.stickyParams.excludedPerspectives?.split(',') || EMPTY_ARRAY,
-    [router.stickyParams.excludedPerspectives],
-  )
-
-  // TODO: Should it be possible to set the perspective within a pane, rather than globally?
-  const setPerspective = useCallback(
-    (releaseId: string | undefined) => {
-      let perspectiveParam = ''
-
-      if (releaseId === 'published') {
-        perspectiveParam = 'published'
-      } else if (releaseId !== 'drafts') {
-        perspectiveParam = `bundle.${releaseId}`
-      }
-
-      router.navigateStickyParams({
-        excludedPerspectives: '',
-        perspective: perspectiveParam,
-      })
-    },
-    [router],
-  )
-
-  const selectedBundle =
-    perspective && releases
-      ? releases.find(
-          (release: ReleaseDocument) =>
-            `bundle.${getBundleIdFromReleaseDocumentId(release._id)}` === perspective,
-        )
-      : LATEST
-
-  // TODO: Improve naming; this may not be global.
-  const currentGlobalBundle: CurrentPerspective = useMemo(
-    () => (perspective === 'published' ? perspective : selectedBundle || LATEST),
-    [perspective, selectedBundle],
-  )
-
-  const setPerspectiveFromReleaseId = useCallback(
-    (releaseId: string) => setPerspective(releaseId),
-    [setPerspective],
-  )
-
-  const setPerspectiveFromReleaseDocumentId = useCallback(
-    (releaseId: string) => setPerspectiveFromReleaseId(getBundleIdFromReleaseDocumentId(releaseId)),
-    [setPerspectiveFromReleaseId],
-  )
-
-  const bundlesPerspective = useMemo(
-    () =>
-      getReleasesPerspective({
-        releases,
-        perspective,
-        excluded: (excludedPerspectives.map(resolveBundlePerspective) as string[]) || [],
-      }),
-    [releases, perspective, excludedPerspectives],
-  )
-
-  const toggleExcludedPerspective = useCallback(
-    (excluded: string) => {
-      if (excluded === LATEST._id) return
-      const existingPerspectives = excludedPerspectives || []
-
-      const excludedPerspectiveId = isPublishedPerspective(excluded)
-        ? 'published'
-        : `bundle.${excluded}`
-
-      const nextExcludedPerspectives = existingPerspectives.includes(excludedPerspectiveId)
-        ? existingPerspectives.filter((id) => id !== excludedPerspectiveId)
-        : [...existingPerspectives, excludedPerspectiveId]
-
-      router.navigateStickyParams({excludedPerspectives: nextExcludedPerspectives.toString()})
-    },
-    [excludedPerspectives, router],
-  )
-
-  const isPerspectiveExcluded = useCallback(
-    (perspectiveId: string) =>
-      Boolean(
-        excludedPerspectives?.includes(
-          isPublishedPerspective(perspectiveId) ? 'published' : `bundle.${perspectiveId}`,
-        ),
-      ),
-    [excludedPerspectives],
-  )
-
-  return useMemo(
-    () => ({
-      perspective,
-      excludedPerspectives,
-      setPerspective,
-      setPerspectiveFromReleaseDocumentId: setPerspectiveFromReleaseDocumentId,
-      setPerspectiveFromReleaseId: setPerspectiveFromReleaseId,
-      toggleExcludedPerspective,
-      currentGlobalBundle,
-      currentGlobalBundleId: isPublishedPerspective(currentGlobalBundle)
-        ? 'published'
-        : currentGlobalBundle._id,
-      bundlesPerspective,
-      isPerspectiveExcluded,
-    }),
-    [
-      perspective,
-      excludedPerspectives,
-      setPerspective,
-      setPerspectiveFromReleaseDocumentId,
-      setPerspectiveFromReleaseId,
-      toggleExcludedPerspective,
-      currentGlobalBundle,
-      bundlesPerspective,
-      isPerspectiveExcluded,
-    ],
-  )
-}
diff --git a/packages/sanity/src/core/releases/hooks/useStudioPerspectiveState.ts b/packages/sanity/src/core/releases/hooks/useStudioPerspectiveState.ts
new file mode 100644
index 00000000000..d63602ccba0
--- /dev/null
+++ b/packages/sanity/src/core/releases/hooks/useStudioPerspectiveState.ts
@@ -0,0 +1,111 @@
+/* eslint-disable no-nested-ternary */
+import {useCallback, useMemo} from 'react'
+import {useRouter} from 'sanity/router'
+import {useEffectEvent} from 'use-effect-event'
+
+import {
+  DRAFTS_PERSPECTIVE,
+  type DraftsPerspective,
+  PUBLISHED_PERSPECTIVE,
+  SelectableReleasePerspective,
+} from '../util/perspective'
+import {type ReleaseId} from '../util/releaseId'
+
+export interface StudioPerspectiveState {
+  current: SelectableReleasePerspective | undefined
+  excluded: SelectableReleasePerspective[]
+  toggle: (perspective: SelectableReleasePerspective) => void
+  include: (perspective: SelectableReleasePerspective) => void
+  exclude: (perspective: SelectableReleasePerspective) => void
+  setCurrent: (perspective: DraftsPerspective | SelectableReleasePerspective) => void
+}
+
+const EMPTY: never[] = []
+
+const RELEASE_PARAM_PREFIX = 'release.'
+function encodeReleasePerspective(releaseId: ReleaseId) {
+  return RELEASE_PARAM_PREFIX + releaseId
+}
+function decodeReleasePerspective(param: string) {
+  if (!param.startsWith(RELEASE_PARAM_PREFIX)) {
+    throw new Error(`Expected release perspective parameter to start with ${RELEASE_PARAM_PREFIX}`)
+  }
+  return param.slice(RELEASE_PARAM_PREFIX.length)
+}
+
+function parsePerspectiveParam(param: string | undefined) {
+  if (!param) {
+    return undefined
+  }
+  return param === 'drafts'
+    ? undefined
+    : param === 'published'
+      ? PUBLISHED_PERSPECTIVE
+      : SelectableReleasePerspective(decodeReleasePerspective(param))
+}
+
+export function useStudioPerspectiveState(): StudioPerspectiveState {
+  const router = useRouter()
+  const setCurrent = useCallback(
+    (nextRelease: DraftsPerspective | SelectableReleasePerspective) => {
+      router.navigateStickyParams({
+        // drafts is the default perspective so will not be written to the url
+        perspective:
+          nextRelease === DRAFTS_PERSPECTIVE
+            ? undefined
+            : nextRelease === PUBLISHED_PERSPECTIVE
+              ? 'published'
+              : encodeReleasePerspective(nextRelease as ReleaseId /*Why typescript why?*/),
+        excludedPerspectives: undefined,
+      })
+    },
+    [router],
+  )
+  const excluded = parseExcludedReleases(router.stickyParams.excludedPerspectives)
+  const current = useMemo(() => {
+    return parsePerspectiveParam(router.stickyParams.perspective)
+  }, [router.stickyParams.perspective])
+
+  const exclude = useEffectEvent((toExclude: SelectableReleasePerspective) => {
+    if (excluded.includes(toExclude)) {
+      return
+    }
+    router.navigateStickyParams({excludedPerspectives: [toExclude, ...excluded].join(',')})
+  })
+
+  const include = useEffectEvent((toInclude: SelectableReleasePerspective) => {
+    if (!excluded.includes(toInclude)) {
+      return
+    }
+    router.navigateStickyParams({
+      excludedReleases: excluded.filter((release) => release === toInclude).join(','),
+    })
+  })
+
+  const toggle = useEffectEvent((toToggle: SelectableReleasePerspective) => {
+    if (excluded.includes(toToggle)) {
+      include(toToggle)
+    } else {
+      exclude(toToggle)
+    }
+  })
+
+  return useMemo(
+    () => ({
+      current: current,
+      excluded,
+      toggle,
+      setCurrent,
+      include,
+      exclude,
+    }),
+    [current, exclude, excluded, include, setCurrent, toggle],
+  )
+}
+
+function parseExcludedReleases(input: string | undefined) {
+  if (!input) {
+    return EMPTY
+  }
+  return input.split(',').map((id) => SelectableReleasePerspective(id))
+}
diff --git a/packages/sanity/src/core/releases/hooks/useVersionOperations.tsx b/packages/sanity/src/core/releases/hooks/useVersionOperations.tsx
index adbcf80c48e..3cfb7117093 100644
--- a/packages/sanity/src/core/releases/hooks/useVersionOperations.tsx
+++ b/packages/sanity/src/core/releases/hooks/useVersionOperations.tsx
@@ -4,26 +4,27 @@ import {useToast} from '@sanity/ui'
 import {Translate, useTranslation} from '../../i18n'
 import {AddedVersion} from '../__telemetry__/releases.telemetry'
 import {useReleaseOperations} from '../store/useReleaseOperations'
+import {type ReleaseId} from '../util/releaseId'
 import {getCreateVersionOrigin} from '../util/util'
-import {usePerspective} from './usePerspective'
+import {useStudioPerspectiveState} from './useStudioPerspectiveState'
 
 /** @internal */
 export function useVersionOperations(): {
-  createVersion: (releaseId: string, documentId: string) => Promise<void>
-  discardVersion: (releaseId: string, documentId: string) => Promise<void>
+  createVersion: (releaseId: ReleaseId, documentId: string) => Promise<void>
+  discardVersion: (releaseId: ReleaseId, documentId: string) => Promise<void>
 } {
   const telemetry = useTelemetry()
   const {createVersion, discardVersion} = useReleaseOperations()
 
-  const {setPerspectiveFromReleaseId} = usePerspective()
+  const {setCurrent} = useStudioPerspectiveState()
   const toast = useToast()
   const {t} = useTranslation()
 
-  const handleCreateVersion = async (releaseId: string, documentId: string) => {
+  const handleCreateVersion = async (releaseId: ReleaseId, documentId: string) => {
     const origin = getCreateVersionOrigin(documentId)
     try {
       await createVersion(releaseId, documentId)
-      setPerspectiveFromReleaseId(releaseId)
+      setCurrent(releaseId)
       telemetry.log(AddedVersion, {
         documentOrigin: origin,
       })
diff --git a/packages/sanity/src/core/releases/hooks/utils.ts b/packages/sanity/src/core/releases/hooks/utils.ts
index adf392c2f94..5a4ddde884f 100644
--- a/packages/sanity/src/core/releases/hooks/utils.ts
+++ b/packages/sanity/src/core/releases/hooks/utils.ts
@@ -1,7 +1,10 @@
-import {DRAFTS_FOLDER} from '../../util/draftUtils'
-import {resolveBundlePerspective} from '../../util/resolvePerspective'
 import {type ReleaseDocument} from '../store/types'
-import {getBundleIdFromReleaseDocumentId} from '../util/getBundleIdFromReleaseDocumentId'
+import {
+  PUBLISHED_PERSPECTIVE,
+  type PublishedPerspective,
+  type SelectableReleasePerspective,
+} from '../util/perspective'
+import {getReleaseIdFromReleaseDocumentId, type ReleaseId} from '../util/releaseId'
 
 export function sortReleases(releases: ReleaseDocument[] = []): ReleaseDocument[] {
   // The order should always be:
@@ -48,32 +51,28 @@ export function sortReleases(releases: ReleaseDocument[] = []): ReleaseDocument[
   })
 }
 
-export function getReleasesPerspective({
+export function getReleasesStack({
   releases,
-  perspective,
+  current,
   excluded,
 }: {
   releases: ReleaseDocument[]
-  perspective: string | undefined // Includes the bundle.<releaseName> or 'published'
-  excluded: string[]
-}): string[] {
-  if (!perspective?.startsWith('bundle.')) {
-    return []
-  }
-  const perspectiveId = resolveBundlePerspective(perspective)
-  if (!perspectiveId) {
+  current: SelectableReleasePerspective | undefined
+  excluded: SelectableReleasePerspective[]
+}): SelectableReleasePerspective[] {
+  if (!current) {
     return []
   }
 
-  const sorted = sortReleases(releases).map((release) =>
-    getBundleIdFromReleaseDocumentId(release._id),
+  const sorted: (ReleaseId | PublishedPerspective)[] = sortReleases(releases).map((release) =>
+    getReleaseIdFromReleaseDocumentId(release._id),
   )
-  const selectedIndex = sorted.indexOf(perspectiveId)
+  const selectedIndex = sorted.indexOf(current)
   if (selectedIndex === -1) {
     return []
   }
   return sorted
     .slice(selectedIndex)
-    .concat(DRAFTS_FOLDER)
+    .concat(PUBLISHED_PERSPECTIVE)
     .filter((name) => !excluded.includes(name))
 }
diff --git a/packages/sanity/src/core/releases/index.ts b/packages/sanity/src/core/releases/index.ts
index 22de4d47251..ef78587f91b 100644
--- a/packages/sanity/src/core/releases/index.ts
+++ b/packages/sanity/src/core/releases/index.ts
@@ -5,7 +5,8 @@ export * from './components'
 export * from './hooks'
 export * from './store'
 export * from './util/const'
-export * from './util/createReleaseId'
 export * from './util/getBundleIdFromReleaseDocumentId'
 export * from './util/getReleaseTone'
+export * from './util/perspective'
+export * from './util/releaseId'
 export * from './util/util'
diff --git a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx
index 2e26abd8cc5..59836efccda 100644
--- a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx
+++ b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx
@@ -7,14 +7,13 @@ import {css, styled} from 'styled-components'
 import {MenuButton} from '../../../ui-components'
 import {useTranslation} from '../../i18n'
 import {CreateReleaseDialog} from '../components/dialog/CreateReleaseDialog'
-import {usePerspective} from '../hooks'
+import {useStudioPerspectiveState} from '../hooks'
 import {type ReleaseDocument, type ReleaseType} from '../store/types'
 import {useReleases} from '../store/useReleases'
-import {
-  getRangePosition,
-  GlobalPerspectiveMenuItem,
-  type LayerRange,
-} from './GlobalPerspectiveMenuItem'
+import {PUBLISHED_PERSPECTIVE} from '../util/perspective'
+import {getReleaseIdFromReleaseDocumentId} from '../util/releaseId'
+import {GlobalPublishedPerspectiveMenuItem} from './GlobalPublishedPerspectiveMenuItem'
+import {getRangePosition, type LayerRange} from './GlobalReleasePerspectiveMenuItem'
 import {ReleaseTypeMenuSection} from './ReleaseTypeMenuSection'
 import {useScrollIndicatorVisibility} from './useScrollIndicatorVisibility'
 
@@ -44,7 +43,7 @@ const ASAP_RANGE_OFFSET = 2
 
 export function GlobalPerspectiveMenu(): JSX.Element {
   const {loading, data: releases} = useReleases()
-  const {currentGlobalBundleId} = usePerspective()
+  const {current} = useStudioPerspectiveState()
   const [createBundleDialogOpen, setCreateBundleDialogOpen] = useState(false)
   const styledMenuRef = useRef<HTMLDivElement>(null)
 
@@ -82,7 +81,7 @@ export function GlobalPerspectiveMenu(): JSX.Element {
     firstIndex = 0
     // }
 
-    if (currentGlobalBundleId === 'published') {
+    if (current === 'published') {
       lastIndex = 0
     }
 
@@ -109,7 +108,7 @@ export function GlobalPerspectiveMenu(): JSX.Element {
           // }
         }
 
-        if (_id === currentGlobalBundleId) {
+        if (getReleaseIdFromReleaseDocumentId(_id) === current) {
           lastIndex = index
         }
       })
@@ -122,7 +121,7 @@ export function GlobalPerspectiveMenu(): JSX.Element {
       lastIndex,
       offsets,
     }
-  }, [currentGlobalBundleId, sortedReleaseTypeReleases])
+  }, [current, sortedReleaseTypeReleases])
 
   const releasesList = useMemo(() => {
     if (loading) {
@@ -137,9 +136,9 @@ export function GlobalPerspectiveMenu(): JSX.Element {
       <Box>
         <StyledBox ref={setScrollContainer} onScroll={onScroll}>
           <StyledPublishedBox $removePadding={!releases.length}>
-            <GlobalPerspectiveMenuItem
+            <GlobalPublishedPerspectiveMenuItem
               rangePosition={isRangeVisible ? getRangePosition(range, 0) : undefined}
-              release={'published'}
+              perspective={PUBLISHED_PERSPECTIVE}
             />
           </StyledPublishedBox>
           <>
diff --git a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx
deleted file mode 100644
index 51863539b3f..00000000000
--- a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-import {DotIcon, EyeClosedIcon, EyeOpenIcon, LockIcon} from '@sanity/icons'
-// eslint-disable-next-line no-restricted-imports -- custom use for MenuItem & Button not supported by ui-components
-import {Box, Button, Flex, MenuItem, Stack, Text} from '@sanity/ui'
-import {type CSSProperties, forwardRef, type MouseEvent, useCallback, useMemo} from 'react'
-import {css, styled} from 'styled-components'
-
-import {Tooltip} from '../../../ui-components/tooltip'
-import {useTranslation} from '../../i18n/hooks/useTranslation'
-import {formatRelativeLocale} from '../../util/formatRelativeLocale'
-import {ReleaseAvatar} from '../components/ReleaseAvatar'
-import {usePerspective} from '../hooks/usePerspective'
-import {type ReleaseDocument} from '../store/types'
-import {getBundleIdFromReleaseDocumentId} from '../util/getBundleIdFromReleaseDocumentId'
-import {getReleaseTone} from '../util/getReleaseTone'
-import {
-  getPublishDateFromRelease,
-  isPublishedPerspective,
-  isReleaseScheduledOrScheduling,
-} from '../util/util'
-import {GlobalPerspectiveMenuItemIndicator} from './PerspectiveLayerIndicator'
-
-export interface LayerRange {
-  firstIndex: number
-  lastIndex: number
-  offsets: {
-    asap: number
-    scheduled: number
-    undecided: number
-  }
-}
-
-const ToggleLayerButton = styled(Button)<{$visible: boolean}>(
-  ({$visible}) => css`
-    --card-fg-color: inherit;
-    --card-icon-color: inherit;
-
-    background-color: inherit;
-    opacity: ${$visible ? 0 : 1};
-
-    @media (hover: hover) {
-      &:not([data-disabled='true']):hover {
-        --card-fg-color: inherit;
-        --card-icon-color: inherit;
-      }
-    }
-
-    [data-ui='MenuItem']:hover & {
-      opacity: 1;
-    }
-  `,
-)
-
-const ExcludedLayerDot = () => (
-  <Box padding={3}>
-    <Text size={1}>
-      <DotIcon
-        style={
-          {
-            opacity: 0,
-          } as CSSProperties
-        }
-      />
-    </Text>
-  </Box>
-)
-
-type rangePosition = 'first' | 'within' | 'last' | undefined
-
-export function getRangePosition(range: LayerRange, index: number): rangePosition {
-  const {firstIndex, lastIndex} = range
-
-  if (firstIndex === lastIndex) return undefined
-  if (index === firstIndex) return 'first'
-  if (index === lastIndex) return 'last'
-  if (index > firstIndex && index < lastIndex) return 'within'
-
-  return undefined
-}
-
-export const GlobalPerspectiveMenuItem = forwardRef<
-  HTMLDivElement,
-  {
-    release: ReleaseDocument | 'published'
-    rangePosition: rangePosition
-  }
->((props, ref) => {
-  const {release, rangePosition} = props
-  const {
-    currentGlobalBundleId,
-    setPerspectiveFromReleaseDocumentId,
-    setPerspective,
-    toggleExcludedPerspective,
-    isPerspectiveExcluded,
-  } = usePerspective()
-  const isReleasePublishedPerspective = isPublishedPerspective(release)
-  const isUnnamedRelease = !isReleasePublishedPerspective && !release.metadata.title
-  const releaseId = isReleasePublishedPerspective ? 'published' : release._id
-  const active = releaseId === currentGlobalBundleId
-  const first = rangePosition === 'first'
-  const within = rangePosition === 'within'
-  const last = rangePosition === 'last'
-  const inRange = first || within || last
-
-  const releasePerspectiveId = isReleasePublishedPerspective
-    ? releaseId
-    : getBundleIdFromReleaseDocumentId(releaseId)
-  const isReleasePerspectiveExcluded = isPerspectiveExcluded(releasePerspectiveId)
-
-  const {t} = useTranslation()
-
-  const displayTitle = useMemo(() => {
-    if (isUnnamedRelease) {
-      return t('release.placeholder-untitled-release')
-    }
-
-    return isReleasePublishedPerspective ? t('release.navbar.published') : release.metadata?.title
-  }, [isReleasePublishedPerspective, isUnnamedRelease, release, t])
-
-  const handleToggleReleaseVisibility = useCallback(
-    (event: MouseEvent<HTMLDivElement>) => {
-      event.stopPropagation()
-      toggleExcludedPerspective(releasePerspectiveId)
-    },
-    [toggleExcludedPerspective, releasePerspectiveId],
-  )
-
-  const handleOnReleaseClick = useCallback(
-    () =>
-      isReleasePublishedPerspective
-        ? setPerspective(releaseId)
-        : setPerspectiveFromReleaseDocumentId(releaseId),
-    [releaseId, isReleasePublishedPerspective, setPerspective, setPerspectiveFromReleaseDocumentId],
-  )
-
-  const canReleaseBeExcluded =
-    !isPublishedPerspective(release) && !isReleaseScheduledOrScheduling(release) && inRange && !last
-
-  return (
-    <GlobalPerspectiveMenuItemIndicator
-      $isPublished={isReleasePublishedPerspective}
-      $first={first}
-      $last={last}
-      $inRange={inRange}
-      ref={ref}
-    >
-      <MenuItem onClick={handleOnReleaseClick} padding={1} pressed={active}>
-        <Flex align="flex-start" gap={1}>
-          <Box
-            flex="none"
-            style={{
-              position: 'relative',
-              zIndex: 1,
-            }}
-          >
-            <Text size={1}>
-              {isReleasePerspectiveExcluded ? (
-                <ExcludedLayerDot />
-              ) : (
-                <ReleaseAvatar tone={getReleaseTone(release)} />
-              )}
-              {/* )} */}
-            </Text>
-          </Box>
-          <Stack
-            flex={1}
-            paddingY={2}
-            paddingRight={2}
-            space={2}
-            style={{
-              opacity: isReleasePerspectiveExcluded ? 0.5 : undefined,
-            }}
-          >
-            <Text size={1} weight="medium">
-              {displayTitle}
-            </Text>
-            {!isPublishedPerspective(release) &&
-              release.metadata.releaseType === 'scheduled' &&
-              (release.publishAt || release.metadata.intendedPublishAt) && (
-                <Text muted size={1}>
-                  {formatRelativeLocale(getPublishDateFromRelease(release), new Date())}
-                </Text>
-              )}
-          </Stack>
-          <Box flex="none">
-            {canReleaseBeExcluded && (
-              <Tooltip portal content={t('release.layer.hide')} placement="bottom">
-                <ToggleLayerButton
-                  $visible={!isReleasePerspectiveExcluded}
-                  forwardedAs="div"
-                  icon={isReleasePerspectiveExcluded ? EyeClosedIcon : EyeOpenIcon}
-                  mode="bleed"
-                  onClick={handleToggleReleaseVisibility}
-                  padding={2}
-                />
-              </Tooltip>
-            )}
-            {!isPublishedPerspective(release) && isReleaseScheduledOrScheduling(release) && (
-              <Box padding={2}>
-                <Text size={1}>
-                  <LockIcon />
-                </Text>
-              </Box>
-            )}
-          </Box>
-        </Flex>
-      </MenuItem>
-    </GlobalPerspectiveMenuItemIndicator>
-  )
-})
-
-GlobalPerspectiveMenuItem.displayName = 'GlobalPerspectiveMenuItem'
diff --git a/packages/sanity/src/core/releases/navbar/GlobalPublishedPerspectiveMenuItem.tsx b/packages/sanity/src/core/releases/navbar/GlobalPublishedPerspectiveMenuItem.tsx
new file mode 100644
index 00000000000..c75346bb77d
--- /dev/null
+++ b/packages/sanity/src/core/releases/navbar/GlobalPublishedPerspectiveMenuItem.tsx
@@ -0,0 +1,107 @@
+import {DotIcon} from '@sanity/icons'
+// eslint-disable-next-line no-restricted-imports -- custom use for MenuItem & Button not supported by ui-components
+import {Box, Button, Text} from '@sanity/ui'
+import {type CSSProperties, forwardRef, type MouseEvent, useCallback} from 'react'
+import {css, styled} from 'styled-components'
+
+import {useTranslation} from '../../i18n/hooks/useTranslation'
+import {useStudioPerspectiveState} from '../hooks/useStudioPerspectiveState'
+import {getPerspectiveTone} from '../util/getReleaseTone'
+import {type PublishedPerspective} from '../util/perspective'
+import {PerspectiveMenuItem} from './PerspectiveMenuItem'
+
+export interface LayerRange {
+  firstIndex: number
+  lastIndex: number
+  offsets: {
+    asap: number
+    scheduled: number
+    undecided: number
+  }
+}
+
+const ToggleLayerButton = styled(Button)<{$visible: boolean}>(
+  ({$visible}) => css`
+    --card-fg-color: inherit;
+    --card-icon-color: inherit;
+
+    background-color: inherit;
+    opacity: ${$visible ? 0 : 1};
+
+    @media (hover: hover) {
+      &:not([data-disabled='true']):hover {
+        --card-fg-color: inherit;
+        --card-icon-color: inherit;
+      }
+    }
+
+    [data-ui='MenuItem']:hover & {
+      opacity: 1;
+    }
+  `,
+)
+
+const ExcludedLayerDot = () => (
+  <Box padding={3}>
+    <Text size={1}>
+      <DotIcon
+        style={
+          {
+            opacity: 0,
+          } as CSSProperties
+        }
+      />
+    </Text>
+  </Box>
+)
+
+type rangePosition = 'first' | 'within' | 'last' | undefined
+
+export const GlobalPublishedPerspectiveMenuItem = forwardRef<
+  HTMLDivElement,
+  {
+    perspective: PublishedPerspective
+    rangePosition: rangePosition
+  }
+>((props, ref) => {
+  const {perspective, rangePosition} = props
+  const {current, setCurrent, toggle, excluded} = useStudioPerspectiveState()
+
+  const active = perspective === current
+  const first = rangePosition === 'first'
+  const within = rangePosition === 'within'
+  const last = rangePosition === 'last'
+  const inRange = first || within || last
+
+  const {t} = useTranslation()
+
+  const isReleasePerspectiveExcluded = excluded.includes(perspective)
+
+  const handleToggleReleaseVisibility = useCallback(
+    (event: MouseEvent<HTMLDivElement>) => {
+      event.stopPropagation()
+      toggle(perspective)
+    },
+    [perspective, toggle],
+  )
+
+  const handleOnReleaseClick = useCallback(() => setCurrent(perspective), [perspective, setCurrent])
+
+  const canReleaseBeExcluded = inRange && !last
+
+  return (
+    <PerspectiveMenuItem
+      onClick={handleOnReleaseClick}
+      tone={getPerspectiveTone(perspective)}
+      onToggleVisibility={handleToggleReleaseVisibility}
+      title={t('release.navbar.published')}
+      active={active}
+      excluded={isReleasePerspectiveExcluded}
+      canBeExcluded={canReleaseBeExcluded}
+      rangePosition={rangePosition}
+      extraPadding
+    />
+  )
+})
+
+GlobalPublishedPerspectiveMenuItem.displayName = 'GlobalPerspectiveMenuItem'
diff --git a/packages/sanity/src/core/releases/navbar/GlobalReleasePerspectiveMenuItem.tsx b/packages/sanity/src/core/releases/navbar/GlobalReleasePerspectiveMenuItem.tsx
new file mode 100644
index 00000000000..c162a4ccec3
--- /dev/null
+++ b/packages/sanity/src/core/releases/navbar/GlobalReleasePerspectiveMenuItem.tsx
@@ -0,0 +1,93 @@
+// eslint-disable-next-line no-restricted-imports -- custom use for MenuItem & Button not supported by ui-components
+import {forwardRef, type MouseEvent, useCallback, useMemo} from 'react'
+
+import {useTranslation} from '../../i18n/hooks/useTranslation'
+import {useStudioPerspectiveState} from '../hooks/useStudioPerspectiveState'
+import {type ReleaseDocument} from '../store/types'
+import {getReleaseTone} from '../util/getReleaseTone'
+import {getReleaseIdFromReleaseDocumentId} from '../util/releaseId'
+import {isPublishedPerspective, isReleaseScheduledOrScheduling} from '../util/util'
+import {PerspectiveMenuItem} from './PerspectiveMenuItem'
+
+export interface LayerRange {
+  firstIndex: number
+  lastIndex: number
+  offsets: {
+    asap: number
+    scheduled: number
+    undecided: number
+  }
+}
+
+type rangePosition = 'first' | 'within' | 'last' | undefined
+
+export function getRangePosition(range: LayerRange, index: number): rangePosition {
+  const {firstIndex, lastIndex} = range
+
+  if (firstIndex === lastIndex) return undefined
+  if (index === firstIndex) return 'first'
+  if (index === lastIndex) return 'last'
+  if (index > firstIndex && index < lastIndex) return 'within'
+
+  return undefined
+}
+
+export const GlobalReleasePerspectiveMenuItem = forwardRef<
+  HTMLDivElement,
+  {
+    release: ReleaseDocument
+    rangePosition: rangePosition
+  }
+>((props, ref) => {
+  const {release, rangePosition} = props
+  const {current, setCurrent, toggle, excluded} = useStudioPerspectiveState()
+
+  const isReleasePublishedPerspective = current && isPublishedPerspective(current)
+  const isUnnamedRelease = !isReleasePublishedPerspective && !release.metadata.title
+
+  const active = getReleaseIdFromReleaseDocumentId(release._id) === current
+  const first = rangePosition === 'first'
+  const within = rangePosition === 'within'
+  const last = rangePosition === 'last'
+  const inRange = first || within || last
+
+  const releaseId = getReleaseIdFromReleaseDocumentId(release._id)
+
+  const {t} = useTranslation()
+
+  const displayTitle = useMemo(() => {
+    if (isUnnamedRelease) {
+      return t('release.placeholder-untitled-release')
+    }
+
+    return isReleasePublishedPerspective ? t('release.navbar.published') : release.metadata?.title
+  }, [isReleasePublishedPerspective, isUnnamedRelease, release, t])
+
+  const handleToggleReleaseVisibility = useCallback(
+    (event: MouseEvent<HTMLDivElement>) => {
+      event.stopPropagation()
+      toggle(releaseId)
+    },
+    [toggle, releaseId],
+  )
+
+  const handleOnReleaseClick = useCallback(() => setCurrent(releaseId), [releaseId, setCurrent])
+
+  const canReleaseBeExcluded = !isReleaseScheduledOrScheduling(release) && inRange && !last
+
+  return (
+    <PerspectiveMenuItem
+      canBeExcluded={canReleaseBeExcluded}
+      onToggleVisibility={handleToggleReleaseVisibility}
+      rangePosition={rangePosition}
+      tone={getReleaseTone(release)}
+      title={displayTitle}
+      onClick={handleOnReleaseClick}
+      active={active}
+      excluded={excluded.includes(releaseId)}
+      locked={isReleaseScheduledOrScheduling(release)}
+    />
+  )
+})
+
+GlobalReleasePerspectiveMenuItem.displayName = 'GlobalPerspectiveMenuItem'
diff --git a/packages/sanity/src/core/releases/navbar/PerspectiveLayerIndicator.tsx b/packages/sanity/src/core/releases/navbar/PerspectiveLayerIndicator.tsx
index f195d7b6ea3..d602bd5f1aa 100644
--- a/packages/sanity/src/core/releases/navbar/PerspectiveLayerIndicator.tsx
+++ b/packages/sanity/src/core/releases/navbar/PerspectiveLayerIndicator.tsx
@@ -10,9 +10,9 @@ export const GlobalPerspectiveMenuItemIndicator = styled.div<{
   $inRange: boolean
   $last: boolean
   $first: boolean
-  $isPublished: boolean
+  $extraPadding?: boolean
 }>(
-  ({$inRange, $last, $first, $isPublished}) => css`
+  ({$inRange, $last, $first, $extraPadding}) => css`
     position: relative;
 
     --indicator-left: ${INDICATOR_LEFT_OFFSET}px;
@@ -32,7 +32,7 @@ export const GlobalPerspectiveMenuItemIndicator = styled.div<{
         left: var(--indicator-left);
         bottom: -var(--indicator-bottom);
         width: var(--indicator-width);
-        height: ${$isPublished
+        height: ${$extraPadding
           ? 'calc(var(--indicator-bottom) + 12px)'
           : 'var(--indicator-bottom)'};
         background-color: var(--indicator-color);
diff --git a/packages/sanity/src/core/releases/navbar/PerspectiveMenuItem.tsx b/packages/sanity/src/core/releases/navbar/PerspectiveMenuItem.tsx
new file mode 100644
index 00000000000..8d7f548baf6
--- /dev/null
+++ b/packages/sanity/src/core/releases/navbar/PerspectiveMenuItem.tsx
@@ -0,0 +1,172 @@
+import {DotIcon, EyeClosedIcon, EyeOpenIcon, LockIcon} from '@sanity/icons'
+// eslint-disable-next-line no-restricted-imports -- custom use for MenuItem & Button not supported by ui-components
+import {type BadgeTone, Box, Button, Flex, MenuItem, Stack, Text} from '@sanity/ui'
+import {type CSSProperties, forwardRef, type MouseEvent} from 'react'
+import {css, styled} from 'styled-components'
+
+import {Tooltip} from '../../../ui-components/tooltip'
+import {useTranslation} from '../../i18n/hooks/useTranslation'
+import {formatRelativeLocale} from '../../util/formatRelativeLocale'
+import {ReleaseAvatar} from '../components/ReleaseAvatar'
+import {GlobalPerspectiveMenuItemIndicator} from './PerspectiveLayerIndicator'
+
+export interface LayerRange {
+  firstIndex: number
+  lastIndex: number
+  offsets: {
+    asap: number
+    scheduled: number
+    undecided: number
+  }
+}
+
+const ToggleLayerButton = styled(Button)<{$visible: boolean}>(
+  ({$visible}) => css`
+    --card-fg-color: inherit;
+    --card-icon-color: inherit;
+
+    background-color: inherit;
+    opacity: ${$visible ? 0 : 1};
+
+    @media (hover: hover) {
+      &:not([data-disabled='true']):hover {
+        --card-fg-color: inherit;
+        --card-icon-color: inherit;
+      }
+    }
+
+    [data-ui='MenuItem']:hover & {
+      opacity: 1;
+    }
+  `,
+)
+
+const ExcludedLayerDot = () => (
+  <Box padding={3}>
+    <Text size={1}>
+      <DotIcon
+        style={
+          {
+            opacity: 0,
+          } as CSSProperties
+        }
+      />
+    </Text>
+  </Box>
+)
+
+type rangePosition = 'first' | 'within' | 'last' | undefined
+
+export function getRangePosition(range: LayerRange, index: number): rangePosition {
+  const {firstIndex, lastIndex} = range
+
+  if (firstIndex === lastIndex) return undefined
+  if (index === firstIndex) return 'first'
+  if (index === lastIndex) return 'last'
+  if (index > firstIndex && index < lastIndex) return 'within'
+
+  return undefined
+}
+
+export const PerspectiveMenuItem = forwardRef<
+  HTMLDivElement,
+  {
+    title: string
+    canBeExcluded: boolean
+    active?: boolean
+    locked?: boolean
+    date?: Date
+    tone: BadgeTone
+    excluded?: boolean
+    extraPadding?: boolean
+    rangePosition: rangePosition
+    onToggleVisibility: (event: MouseEvent<HTMLDivElement>) => void
+    onClick: () => void
+  }
+>((props, ref) => {
+  const {
+    canBeExcluded,
+    locked,
+    date,
+    rangePosition,
+    active,
+    tone,
+    title,
+    excluded,
+    extraPadding,
+    onToggleVisibility,
+    onClick,
+  } = props
+
+  const first = rangePosition === 'first'
+  const within = rangePosition === 'within'
+  const last = rangePosition === 'last'
+  const inRange = first || within || last
+
+  const {t} = useTranslation()
+
+  return (
+    <GlobalPerspectiveMenuItemIndicator
+      $extraPadding={extraPadding}
+      $first={first}
+      $last={last}
+      $inRange={inRange}
+      ref={ref}
+    >
+      <MenuItem onClick={onClick} padding={1} pressed={active}>
+        <Flex align="flex-start" gap={1}>
+          <Box
+            flex="none"
+            style={{
+              position: 'relative',
+              zIndex: 1,
+            }}
+          >
+            <Text size={1}>{excluded ? <ExcludedLayerDot /> : <ReleaseAvatar tone={tone} />}</Text>
+          </Box>
+          <Stack
+            flex={1}
+            paddingY={2}
+            paddingRight={2}
+            space={2}
+            style={{
+              opacity: excluded ? 0.5 : undefined,
+            }}
+          >
+            <Text size={1} weight="medium">
+              {title}
+            </Text>
+            {date && (
+              <Text muted size={1}>
+                {formatRelativeLocale(date, new Date())}
+              </Text>
+            )}
+          </Stack>
+          <Box flex="none">
+            {canBeExcluded && (
+              <Tooltip portal content={t('release.layer.hide')} placement="bottom">
+                <ToggleLayerButton
+                  $visible={!excluded}
+                  forwardedAs="div"
+                  icon={excluded ? EyeClosedIcon : EyeOpenIcon}
+                  mode="bleed"
+                  onClick={onToggleVisibility}
+                  padding={2}
+                />
+              </Tooltip>
+            )}
+            {locked && (
+              <Box padding={2}>
+                <Text size={1}>
+                  <LockIcon />
+                </Text>
+              </Box>
+            )}
+          </Box>
+        </Flex>
+      </MenuItem>
+    </GlobalPerspectiveMenuItemIndicator>
+  )
+})
+
+PerspectiveMenuItem.displayName = 'GlobalPerspectiveMenuItem'
diff --git a/packages/sanity/src/core/releases/navbar/ReleaseTypeMenuSection.tsx b/packages/sanity/src/core/releases/navbar/ReleaseTypeMenuSection.tsx
index 4d632305e58..bf709b1e4f1 100644
--- a/packages/sanity/src/core/releases/navbar/ReleaseTypeMenuSection.tsx
+++ b/packages/sanity/src/core/releases/navbar/ReleaseTypeMenuSection.tsx
@@ -2,13 +2,13 @@ import {Flex, Label} from '@sanity/ui'
 import {useCallback} from 'react'
 
 import {useTranslation} from '../../i18n/hooks/useTranslation'
-import {usePerspective} from '../hooks/usePerspective'
+import {useStudioPerspectiveState} from '../hooks/useStudioPerspectiveState'
 import {type ReleaseDocument, type ReleaseType} from '../store/types'
 import {
   getRangePosition,
-  GlobalPerspectiveMenuItem,
+  GlobalReleasePerspectiveMenuItem,
   type LayerRange,
-} from './GlobalPerspectiveMenuItem'
+} from './GlobalReleasePerspectiveMenuItem'
 import {GlobalPerspectiveMenuLabelIndicator} from './PerspectiveLayerIndicator'
 import {type ScrollElement} from './useScrollIndicatorVisibility'
 
@@ -30,14 +30,14 @@ export function ReleaseTypeMenuSection({
   currentGlobalBundleMenuItemRef: React.RefObject<ScrollElement>
 }): JSX.Element | null {
   const {t} = useTranslation()
-  const {currentGlobalBundleId} = usePerspective()
+  const {current} = useStudioPerspectiveState()
 
   const getMenuItemRef = useCallback(
     (releaseId: string) =>
-      releaseId === currentGlobalBundleId
+      releaseId === current
         ? (currentGlobalBundleMenuItemRef as React.RefObject<HTMLDivElement>)
         : undefined,
-    [currentGlobalBundleId, currentGlobalBundleMenuItemRef],
+    [current, currentGlobalBundleMenuItemRef],
   )
 
   if (releases.length === 0) return null
@@ -59,7 +59,7 @@ export function ReleaseTypeMenuSection({
       </GlobalPerspectiveMenuLabelIndicator>
       <Flex direction="column" gap={1}>
         {releases.map((release, index) => (
-          <GlobalPerspectiveMenuItem
+          <GlobalReleasePerspectiveMenuItem
             release={release}
             key={release._id}
             ref={getMenuItemRef(release._id)}
diff --git a/packages/sanity/src/core/releases/navbar/ReleasesNav.tsx b/packages/sanity/src/core/releases/navbar/ReleasesNav.tsx
index be8cd8ca344..6f417087a9b 100644
--- a/packages/sanity/src/core/releases/navbar/ReleasesNav.tsx
+++ b/packages/sanity/src/core/releases/navbar/ReleasesNav.tsx
@@ -9,11 +9,11 @@ import {IntentLink, useRouterState} from 'sanity/router'
 import {Tooltip} from '../../../ui-components'
 import {ToolLink} from '../../studio'
 import {ReleaseAvatar} from '../components/ReleaseAvatar'
-import {usePerspective} from '../hooks/usePerspective'
+import {useStudioPerspectiveState} from '../hooks/useStudioPerspectiveState'
 import {RELEASES_INTENT, RELEASES_TOOL_NAME} from '../plugin'
-import {LATEST} from '../util/const'
 import {getBundleIdFromReleaseDocumentId} from '../util/getBundleIdFromReleaseDocumentId'
 import {getReleaseTone} from '../util/getReleaseTone'
+import {PUBLISHED_PERSPECTIVE} from '../util/perspective'
 import {isDraftPerspective, isPublishedPerspective} from '../util/util'
 import {GlobalPerspectiveMenu} from './GlobalPerspectiveMenu'
 
@@ -37,10 +37,10 @@ export function ReleasesNav(): JSX.Element {
     ),
   )
 
-  const {currentGlobalBundle, setPerspective} = usePerspective()
+  const {currentGlobalRelease, current, setCurrent} = useStudioPerspectiveState()
   const {t} = useTranslation()
 
-  const handleClearPerspective = () => setPerspective(LATEST._id)
+  const handleClearPerspective = () => setCurrent(PUBLISHED_PERSPECTIVE)
 
   const releasesToolLink = useMemo(
     () => (
@@ -62,21 +62,21 @@ export function ReleasesNav(): JSX.Element {
   )
 
   const currentGlobalPerspectiveLabel = useMemo(() => {
-    if (!currentGlobalBundle || isDraftPerspective(currentGlobalBundle)) return null
+    if (!currentGlobalRelease || isDraftPerspective(current)) return null
 
     let displayTitle
-    if (isPublishedPerspective(currentGlobalBundle)) {
+    if (isPublishedPerspective(current)) {
       displayTitle = t('release.chip.published')
     } else {
       displayTitle =
-        currentGlobalBundle.metadata?.title || t('release.placeholder-untitled-release')
+        currentGlobalRelease.metadata?.title || t('release.placeholder-untitled-release')
     }
 
     const visibleLabelChildren = () => {
       const labelContent = (
         <Flex align="flex-start" gap={0}>
           <Box flex="none">
-            <ReleaseAvatar padding={2} tone={getReleaseTone(currentGlobalBundle)} />
+            <ReleaseAvatar padding={2} tone={getReleaseTone(currentGlobalRelease)} />
           </Box>
           <Stack flex={1} paddingY={2} paddingRight={2} space={2}>
             <Text size={1} textOverflow="ellipsis" weight="medium">
@@ -86,7 +86,7 @@ export function ReleasesNav(): JSX.Element {
         </Flex>
       )
 
-      if (isPublishedPerspective(currentGlobalBundle)) {
+      if (isPublishedPerspective(current)) {
         return <Card tone="inherit">{labelContent}</Card>
       }
 
@@ -94,7 +94,7 @@ export function ReleasesNav(): JSX.Element {
         <IntentLink
           {...intentProps}
           intent={RELEASES_INTENT}
-          params={{id: getBundleIdFromReleaseDocumentId(currentGlobalBundle._id!)}}
+          params={{id: getBundleIdFromReleaseDocumentId(currentGlobalRelease._id!)}}
         >
           {children}
         </IntentLink>
@@ -116,7 +116,7 @@ export function ReleasesNav(): JSX.Element {
     }
 
     return <AnimatedMotionDiv>{visibleLabelChildren()}</AnimatedMotionDiv>
-  }, [currentGlobalBundle, t])
+  }, [currentGlobalRelease, current, t])
 
   return (
     <Card flex="none" border marginRight={1} radius="full" tone="inherit" style={{margin: -1}}>
@@ -124,7 +124,7 @@ export function ReleasesNav(): JSX.Element {
         <Box flex="none">{releasesToolLink}</Box>
         <AnimatePresence>{currentGlobalPerspectiveLabel}</AnimatePresence>
         <GlobalPerspectiveMenu />
-        {!isDraftPerspective(currentGlobalBundle) && (
+        {!isDraftPerspective(current) && (
           <div>
             <Button
               icon={CloseIcon}
diff --git a/packages/sanity/src/core/releases/navbar/__tests__/ReleasesNav.test.tsx b/packages/sanity/src/core/releases/navbar/__tests__/ReleasesNav.test.tsx
index b66c9df3301..58ae7d4a471 100644
--- a/packages/sanity/src/core/releases/navbar/__tests__/ReleasesNav.test.tsx
+++ b/packages/sanity/src/core/releases/navbar/__tests__/ReleasesNav.test.tsx
@@ -2,7 +2,7 @@ import {fireEvent, render, screen} from '@testing-library/react'
 import {beforeEach, describe, expect, it, type Mock, vi} from 'vitest'
 
 import {createTestProvider} from '../../../../../test/testUtils/TestProvider'
-import {usePerspective} from '../../hooks/usePerspective'
+import {useStudioPerspectiveState} from '../../hooks/useStudioPerspectiveState'
 import {LATEST} from '../../util/const'
 import {ReleasesNav} from '../ReleasesNav'
 
@@ -24,7 +24,7 @@ vi.mock('../../../store/releases/useBundles', () => ({
   }),
 }))
 
-const mockUsePerspective = usePerspective as Mock<typeof usePerspective>
+const mockUsePerspective = useStudioPerspectiveState as Mock<typeof useStudioPerspectiveState>
 
 const renderTest = async () => {
   const wrapper = await createTestProvider({
@@ -38,8 +38,8 @@ const mockSetPerspective = vi.fn()
 describe('ReleasesNav', () => {
   beforeEach(() => {
     mockUsePerspective.mockReturnValue({
-      currentGlobalBundle: LATEST,
-      setPerspective: mockSetPerspective,
+      currentGlobalRelease: LATEST,
+      setCurrent: mockSetPerspective,
     })
   })
 
@@ -66,11 +66,11 @@ describe('ReleasesNav', () => {
 
   it('should have clear button to unset perspective when a perspective is chosen', async () => {
     mockUsePerspective.mockReturnValue({
-      currentGlobalBundle: {
+      currentGlobalRelease: {
         _id: '_.releases.a-release',
         metadata: {title: 'Test Release'},
       },
-      setPerspective: mockSetPerspective,
+      setCurrent: mockSetPerspective,
     })
 
     await renderTest()
@@ -82,13 +82,13 @@ describe('ReleasesNav', () => {
 
   it('should list the title of the chosen perspective', async () => {
     mockUsePerspective.mockReturnValue({
-      currentGlobalBundle: {
+      currentGlobalRelease: {
         _id: '_.releases.a-release',
         metadata: {
           title: 'Test Bundle',
         },
       },
-      setPerspective: mockSetPerspective,
+      setCurrent: mockSetPerspective,
     })
 
     await renderTest()
@@ -98,11 +98,11 @@ describe('ReleasesNav', () => {
 
   it('should show release avatar for chosen perspective', async () => {
     mockUsePerspective.mockReturnValue({
-      currentGlobalBundle: {
+      currentGlobalRelease: {
         _id: '_.releases.a-release',
         metadata: {title: 'Test Bundle', releaseType: 'asap'},
       },
-      setPerspective: mockSetPerspective,
+      setCurrent: mockSetPerspective,
     })
 
     await renderTest()
diff --git a/packages/sanity/src/core/releases/store/constants.ts b/packages/sanity/src/core/releases/store/constants.ts
index 02d29f8a6ec..b168d2a846e 100644
--- a/packages/sanity/src/core/releases/store/constants.ts
+++ b/packages/sanity/src/core/releases/store/constants.ts
@@ -2,3 +2,14 @@
 // eslint-disable-next-line @typescript-eslint/prefer-as-const
 export const RELEASE_DOCUMENT_TYPE: 'system.release' = 'system.release'
 export const RELEASE_DOCUMENTS_PATH = '_.releases'
+export const PATH_SEPARATOR = '.'
+export const RELEASE_DOCUMENTS_PATH_PREFIX = `${RELEASE_DOCUMENTS_PATH}${PATH_SEPARATOR}`
+
+/** @internal */
+export const DRAFTS_FOLDER = 'drafts'
+/** @internal */
+export const VERSION_FOLDER = 'versions'
+/** @internal */
+export const DRAFTS_PREFIX = `${DRAFTS_FOLDER}${PATH_SEPARATOR}`
+/** @internal */
+export const VERSION_PREFIX = `${VERSION_FOLDER}${PATH_SEPARATOR}`
diff --git a/packages/sanity/src/core/releases/store/index.ts b/packages/sanity/src/core/releases/store/index.ts
index f8dc7aaebca..b629c6aab18 100644
--- a/packages/sanity/src/core/releases/store/index.ts
+++ b/packages/sanity/src/core/releases/store/index.ts
@@ -1,3 +1,4 @@
 export * from './types'
 export * from './useReleaseOperations'
 export * from './useReleases'
+export * from './useReleasesStack'
diff --git a/packages/sanity/src/core/releases/store/types.ts b/packages/sanity/src/core/releases/store/types.ts
index a1fdc2b80b8..abb8902d056 100644
--- a/packages/sanity/src/core/releases/store/types.ts
+++ b/packages/sanity/src/core/releases/store/types.ts
@@ -2,6 +2,7 @@ import {type Dispatch} from 'react'
 import {type Observable} from 'rxjs'
 
 import {type PartialExcept} from '../../util'
+import {type ReleaseDocumentId} from '../util/releaseId'
 import {RELEASE_DOCUMENT_TYPE} from './constants'
 import {type MetadataWrapper} from './createReleaseMetadataAggregator'
 import {type ReleasesReducerAction, type ReleasesReducerState} from './reducer'
@@ -30,7 +31,7 @@ export interface ReleaseDocument {
    * typically
    * _.releases.<name>
    */
-  _id: string
+  _id: ReleaseDocumentId
   _type: typeof RELEASE_DOCUMENT_TYPE
   _createdAt: string
   _updatedAt: string
diff --git a/packages/sanity/src/core/releases/store/useReleases.ts b/packages/sanity/src/core/releases/store/useReleases.ts
index 6a8c1ff42dd..1c778757e06 100644
--- a/packages/sanity/src/core/releases/store/useReleases.ts
+++ b/packages/sanity/src/core/releases/store/useReleases.ts
@@ -2,7 +2,7 @@ import {useMemo} from 'react'
 import {useObservable} from 'react-rx'
 
 import {sortReleases} from '../hooks/utils'
-import {getBundleIdFromReleaseDocumentId} from '../util/getBundleIdFromReleaseDocumentId'
+import {getReleaseIdFromReleaseDocumentId, type ReleaseId} from '../util/releaseId'
 import {type ReleasesReducerAction} from './reducer'
 import {type ReleaseDocument} from './types'
 import {useReleasesStore} from './useReleasesStore'
@@ -15,7 +15,7 @@ interface ReleasesState {
   /**
    * Sorted array of release IDs, excluding archived releases
    */
-  releasesIds: string[]
+  releasesIds: ReleaseId[]
   /**
    * Array of archived releases
    */
@@ -50,7 +50,7 @@ export function useReleases(): ReleasesState {
     [state.releases],
   )
   const releasesIds = useMemo(
-    () => releasesAsArray.map((release) => getBundleIdFromReleaseDocumentId(release._id)),
+    () => releasesAsArray.map((release) => getReleaseIdFromReleaseDocumentId(release._id)),
     [releasesAsArray],
   )
   return {
diff --git a/packages/sanity/src/core/releases/store/useReleasesStack.ts b/packages/sanity/src/core/releases/store/useReleasesStack.ts
new file mode 100644
index 00000000000..3bd46aa0f81
--- /dev/null
+++ b/packages/sanity/src/core/releases/store/useReleasesStack.ts
@@ -0,0 +1,14 @@
+import {useMemo} from 'react'
+
+import {useStudioPerspectiveState} from '../hooks'
+import {getReleasesStack} from '../hooks/utils'
+import {useReleases} from './useReleases'
+
+export function useReleasesStack() {
+  const {data: releases} = useReleases()
+  const {current, excluded} = useStudioPerspectiveState()
+  return useMemo(
+    () => getReleasesStack({releases, current, excluded}),
+    [current, excluded, releases],
+  )
+}
diff --git a/packages/sanity/src/core/releases/tool/detail/ReleaseDashboardDetails.tsx b/packages/sanity/src/core/releases/tool/detail/ReleaseDashboardDetails.tsx
index 69dea809a6e..bbbd3555a5a 100644
--- a/packages/sanity/src/core/releases/tool/detail/ReleaseDashboardDetails.tsx
+++ b/packages/sanity/src/core/releases/tool/detail/ReleaseDashboardDetails.tsx
@@ -11,10 +11,12 @@ import {
 import {useCallback} from 'react'
 
 import {useTranslation} from '../../../i18n'
-import {usePerspective} from '../../hooks'
+import {useStudioPerspectiveState} from '../../hooks/useStudioPerspectiveState'
 import {releasesLocaleNamespace} from '../../i18n'
 import {type ReleaseDocument} from '../../index'
 import {getReleaseTone} from '../../util/getReleaseTone'
+import {DRAFTS_PERSPECTIVE} from '../../util/perspective'
+import {getReleaseIdFromReleaseDocumentId} from '../../util/releaseId'
 import {ReleaseDetailsEditor} from './ReleaseDetailsEditor'
 import {ReleaseTypePicker} from './ReleaseTypePicker'
 
@@ -23,16 +25,17 @@ export function ReleaseDashboardDetails({release}: {release: ReleaseDocument}) {
 
   const {t: tRelease} = useTranslation(releasesLocaleNamespace)
 
-  const {currentGlobalBundleId, setPerspective, setPerspectiveFromReleaseDocumentId} =
-    usePerspective()
+  const {current, setCurrent} = useStudioPerspectiveState()
+  const selected = getReleaseIdFromReleaseDocumentId(_id) === current
 
   const handlePinRelease = useCallback(() => {
-    if (_id === currentGlobalBundleId) {
-      setPerspective('drafts')
+    if (selected) {
+      // toggle off
+      setCurrent(DRAFTS_PERSPECTIVE)
     } else {
-      setPerspectiveFromReleaseDocumentId(_id)
+      setCurrent(getReleaseIdFromReleaseDocumentId(_id))
     }
-  }, [_id, currentGlobalBundleId, setPerspective, setPerspectiveFromReleaseDocumentId])
+  }, [_id, selected, setCurrent])
 
   return (
     <Container width={3}>
@@ -40,12 +43,12 @@ export function ReleaseDashboardDetails({release}: {release: ReleaseDocument}) {
         <Flex gap={1}>
           <Button
             disabled={state === 'archived' || state === 'published'}
-            icon={_id === currentGlobalBundleId ? PinFilledIcon : PinIcon}
+            icon={selected ? PinFilledIcon : PinIcon}
             mode="bleed"
             onClick={handlePinRelease}
             padding={2}
             radius="full"
-            selected={_id === currentGlobalBundleId}
+            selected={selected}
             space={2}
             text={tRelease('dashboard.details.pin-release')}
             tone={getReleaseTone(release)}
diff --git a/packages/sanity/src/core/releases/tool/overview/ReleasesOverview.tsx b/packages/sanity/src/core/releases/tool/overview/ReleasesOverview.tsx
index 0f95b50cf04..c687011d809 100644
--- a/packages/sanity/src/core/releases/tool/overview/ReleasesOverview.tsx
+++ b/packages/sanity/src/core/releases/tool/overview/ReleasesOverview.tsx
@@ -15,9 +15,9 @@ import {
 import {useTranslation} from '../../../i18n'
 import useTimeZone from '../../../scheduledPublishing/hooks/useTimeZone'
 import {CreateReleaseDialog} from '../../components/dialog/CreateReleaseDialog'
-import {usePerspective} from '../../hooks/usePerspective'
+import {useStudioPerspectiveState} from '../../hooks/useStudioPerspectiveState'
 import {releasesLocaleNamespace} from '../../i18n'
-import {type ReleaseDocument, useReleases} from '../../index'
+import {getReleaseIdFromReleaseDocumentId, type ReleaseDocument, useReleases} from '../../index'
 import {type ReleasesMetadata, useReleasesMetadata} from '../../store/useReleasesMetadata'
 import {getReleaseTone} from '../../util/getReleaseTone'
 import {ReleaseMenuButton} from '../components/ReleaseMenuButton/ReleaseMenuButton'
@@ -134,16 +134,19 @@ export function ReleasesOverview() {
   const {t} = useTranslation(releasesLocaleNamespace)
   const {t: tCore} = useTranslation()
   const {timeZone} = useTimeZone()
-  const {currentGlobalBundleId} = usePerspective()
+  const {current} = useStudioPerspectiveState()
 
   const getRowProps = useCallback(
     (datum: TableRelease): Partial<TableRowProps> =>
       datum.isDeleted
         ? {tone: 'transparent'}
         : {
-            tone: currentGlobalBundleId === datum._id ? getReleaseTone(datum) : 'default',
+            tone:
+              current === getReleaseIdFromReleaseDocumentId(datum._id)
+                ? getReleaseTone(datum)
+                : 'default',
           },
-    [currentGlobalBundleId],
+    [current],
   )
 
   const scrollContainerRef = useRef<HTMLDivElement | null>(null)
diff --git a/packages/sanity/src/core/releases/tool/overview/ReleasesOverviewColumnDefs.tsx b/packages/sanity/src/core/releases/tool/overview/ReleasesOverviewColumnDefs.tsx
index 3b1f40ee291..855857de9bd 100644
--- a/packages/sanity/src/core/releases/tool/overview/ReleasesOverviewColumnDefs.tsx
+++ b/packages/sanity/src/core/releases/tool/overview/ReleasesOverviewColumnDefs.tsx
@@ -9,10 +9,12 @@ import {Button, Tooltip} from '../../../../ui-components'
 import {RelativeTime} from '../../../components'
 import {Translate, useTranslation} from '../../../i18n'
 import {ReleaseAvatar} from '../../components/ReleaseAvatar'
-import {usePerspective} from '../../hooks/usePerspective'
+import {useStudioPerspectiveState} from '../../hooks/useStudioPerspectiveState'
 import {releasesLocaleNamespace} from '../../i18n'
 import {getBundleIdFromReleaseDocumentId} from '../../util/getBundleIdFromReleaseDocumentId'
 import {getReleaseTone} from '../../util/getReleaseTone'
+import {DRAFTS_PERSPECTIVE} from '../../util/perspective'
+import {getReleaseIdFromReleaseDocumentId} from '../../util/releaseId'
 import {getPublishDateFromRelease, isReleaseScheduledOrScheduling} from '../../util/util'
 import {type TableRowProps} from '../components/Table/Table'
 import {Headers} from '../components/Table/TableHeader'
@@ -48,18 +50,17 @@ const ReleaseNameCell: Column<TableRelease>['cell'] = ({cellProps, datum: releas
   const router = useRouter()
   const {t} = useTranslation(releasesLocaleNamespace)
   const {t: tCore} = useTranslation()
-  const {currentGlobalBundleId, setPerspective, setPerspectiveFromReleaseDocumentId} =
-    usePerspective()
+  const {current, setCurrent} = useStudioPerspectiveState()
   const {state, _id} = release
   const isArchived = state === 'archived'
-
+  const releaseId = getReleaseIdFromReleaseDocumentId(_id)
   const handlePinRelease = useCallback(() => {
-    if (_id === currentGlobalBundleId) {
-      setPerspective('drafts')
+    if (releaseId === current) {
+      setCurrent(DRAFTS_PERSPECTIVE)
     } else {
-      setPerspectiveFromReleaseDocumentId(_id)
+      setCurrent(getReleaseIdFromReleaseDocumentId(_id))
     }
-  }, [_id, currentGlobalBundleId, setPerspective, setPerspectiveFromReleaseDocumentId])
+  }, [_id, current, releaseId, setCurrent])
 
   const cardProps: TableRowProps = release.isDeleted
     ? {tone: 'transparent'}
@@ -70,7 +71,7 @@ const ReleaseNameCell: Column<TableRelease>['cell'] = ({cellProps, datum: releas
         tone: 'inherit',
       }
 
-  const isReleasePinned = _id === currentGlobalBundleId
+  const isReleasePinned = releaseId === current
   const pinButtonIcon = isReleasePinned ? PinFilledIcon : PinIcon
   const displayTitle = release.metadata.title || tCore('release.placeholder-untitled-release')
 
@@ -96,7 +97,7 @@ const ReleaseNameCell: Column<TableRelease>['cell'] = ({cellProps, datum: releas
             onClick={handlePinRelease}
             padding={2}
             round
-            selected={_id === currentGlobalBundleId}
+            selected={releaseId === current}
           />
           <Card {...cardProps} padding={2} radius={2} flex={1}>
             <Flex align="center" gap={2}>
diff --git a/packages/sanity/src/core/releases/util/const.ts b/packages/sanity/src/core/releases/util/const.ts
index f8670e8e365..58430a8bb61 100644
--- a/packages/sanity/src/core/releases/util/const.ts
+++ b/packages/sanity/src/core/releases/util/const.ts
@@ -1,11 +1,13 @@
 /*  TEMPORARY  DUMMY DATA */
 
+import {DRAFTS_PERSPECTIVE} from './perspective'
+
 /**
  * @internal
  */
 export const LATEST = {
   // this exists implicitly
-  _id: 'drafts',
+  _id: DRAFTS_PERSPECTIVE,
   metadata: {
     title: 'Latest',
   },
diff --git a/packages/sanity/src/core/releases/util/getReleaseTone.ts b/packages/sanity/src/core/releases/util/getReleaseTone.ts
index 6747b91cf58..454982ae686 100644
--- a/packages/sanity/src/core/releases/util/getReleaseTone.ts
+++ b/packages/sanity/src/core/releases/util/getReleaseTone.ts
@@ -1,18 +1,15 @@
 import {type BadgeTone} from '@sanity/ui'
 
 import {type ReleaseDocument} from '../store/types'
-import {type LATEST} from './const'
-import {isDraftPerspective, isPublishedPerspective} from './util'
+import {PUBLISHED_PERSPECTIVE, type SelectableReleasePerspective} from './perspective'
+import {isPublishedPerspective} from './util'
 
 /** @internal */
-export function getReleaseTone(release: ReleaseDocument | 'published' | typeof LATEST): BadgeTone {
+export function getReleaseTone(release: ReleaseDocument): BadgeTone {
   /* conflicts with the type scheduled, maybe confusion with published?
  if (release.publishedAt !== undefined) {
     return 'positive'
   }*/
-  if (isPublishedPerspective(release)) return 'positive'
-  if (isDraftPerspective(release)) return 'default'
-
   if (release.state === 'archived') {
     return 'default'
   }
@@ -30,3 +27,14 @@ export function getReleaseTone(release: ReleaseDocument | 'published' | typeof L
   }
   return 'default'
 }
+
+/** @internal */
+export function getPerspectiveTone(
+  perspective: SelectableReleasePerspective = PUBLISHED_PERSPECTIVE,
+): BadgeTone {
+  /* conflicts with the type scheduled, maybe confusion with published?
+ if (release.publishedAt !== undefined) {
+    return 'positive'
+  }*/
+  return isPublishedPerspective(perspective) ? 'positive' : 'default'
+}
diff --git a/packages/sanity/src/core/releases/util/perspective.ts b/packages/sanity/src/core/releases/util/perspective.ts
new file mode 100644
index 00000000000..0fa5e790ec9
--- /dev/null
+++ b/packages/sanity/src/core/releases/util/perspective.ts
@@ -0,0 +1,22 @@
+/* eslint-disable @typescript-eslint/no-redeclare */
+import {type Brand, make} from 'ts-brand'
+
+import {ReleaseId} from './releaseId'
+
+export type DraftsPerspective = Brand<'drafts', 'draftsPerspective'>
+export type PublishedPerspective = Brand<'published', 'publishedPerspective'>
+/** @internal */
+export const DRAFTS_PERSPECTIVE = 'drafts' as DraftsPerspective
+/** @internal */
+export const PUBLISHED_PERSPECTIVE = 'published' as PublishedPerspective
+
+export type ReleasePerspective = DraftsPerspective | PublishedPerspective | ReleaseId
+export type SelectableReleasePerspective = PublishedPerspective | ReleaseId
+
+export const SelectableReleasePerspective = make<SelectableReleasePerspective>((id) => {
+  return id === DRAFTS_PERSPECTIVE ? id : ReleaseId(id)
+})
+
+export const ReleasePerspective = make<ReleasePerspective>((id) => {
+  return id === DRAFTS_PERSPECTIVE || id === PUBLISHED_PERSPECTIVE ? id : ReleaseId(id)
+})
diff --git a/packages/sanity/src/core/releases/util/releaseId.ts b/packages/sanity/src/core/releases/util/releaseId.ts
new file mode 100644
index 00000000000..15f7b93e04a
--- /dev/null
+++ b/packages/sanity/src/core/releases/util/releaseId.ts
@@ -0,0 +1,95 @@
+/* eslint-disable @typescript-eslint/no-redeclare */
+import {type DocumentId, isVersionId} from '@sanity/id-utils'
+import {customAlphabet} from 'nanoid'
+import {type Brand, make} from 'ts-brand'
+
+import {getPublishedId} from '../../util'
+import {
+  PATH_SEPARATOR,
+  RELEASE_DOCUMENTS_PATH,
+  RELEASE_DOCUMENTS_PATH_PREFIX,
+  VERSION_PREFIX,
+} from '../store/constants'
+
+/**
+ * @internal
+ */
+export type ReleaseId = Brand<string, 'releaseId'>
+/**
+ * @internal
+ */
+export type ReleaseDocumentId = Brand<string, 'releaseDocumentId'>
+
+/**
+ * @internal
+ */
+export const ReleaseId = make<ReleaseId>((input: string) => {
+  validateBundleId(input)
+  return input
+})
+/**
+ * @internal
+ */
+export const ReleaseDocumentId = make<ReleaseDocumentId>((input: string) => {
+  validateReleaseDocumentId(input)
+  return input
+})
+
+function validateBundleId(input: string) {
+  // todo: consider validation here
+  //if (!input.startsWith('r') || input.length !== 9) {
+  //  throw new Error(
+  //    `'Invalid release id "${input}" – Release Ids should be 9 characters and start with 'r'`,
+  //  )
+  // }
+}
+
+function validateReleaseDocumentId(input: string) {
+  if (!input.startsWith(RELEASE_DOCUMENTS_PATH_PREFIX)) {
+    throw new Error(
+      `'Invalid release document id "${input}" – Release document Ids must be in the ${RELEASE_DOCUMENTS_PATH} path`,
+    )
+  }
+  validateBundleId(input.slice(RELEASE_DOCUMENTS_PATH_PREFIX.length))
+}
+
+/**
+ * ~24 years (or 7.54e+8 seconds) needed, in order to have a 1% probability of at least one collision if 10 ID's are generated every hour.
+ */
+const _generateReleaseId = customAlphabet(
+  'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
+  8,
+)
+
+export function generateReleaseId() {
+  return ReleaseId(_generateReleaseId())
+}
+
+/**
+ * Create a unique release id. This is used as the release id for documents included in the release.
+ * @internal
+ */
+export function generateReleaseDocumentId() {
+  return ReleaseDocumentId(`${RELEASE_DOCUMENTS_PATH}.r${_generateReleaseId()}`)
+}
+
+/**
+ * @internal
+ * @param releaseId - the document id of the release
+ */
+export function getReleaseIdFromReleaseDocumentId(releaseId: ReleaseDocumentId) {
+  return ReleaseId(releaseId.slice(RELEASE_DOCUMENTS_PATH_PREFIX.length))
+}
+
+/**  @internal */
+export function getVersionId(id: DocumentId, release: ReleaseId): string {
+  if (isVersionId(id)) {
+    const [, versionId, ...publishedId] = id.split(PATH_SEPARATOR)
+    if (versionId === release) return id
+    return `${VERSION_PREFIX}${release}${PATH_SEPARATOR}${publishedId}`
+  }
+
+  const publishedId = getPublishedId(id)
+
+  return `${VERSION_PREFIX}${release}${PATH_SEPARATOR}${publishedId}`
+}
diff --git a/packages/sanity/src/core/releases/util/util.ts b/packages/sanity/src/core/releases/util/util.ts
index 078ef6f92fe..6d9ab5078c0 100644
--- a/packages/sanity/src/core/releases/util/util.ts
+++ b/packages/sanity/src/core/releases/util/util.ts
@@ -5,10 +5,18 @@ import {
   isVersionId,
   resolveBundlePerspective,
 } from '../../util'
-import {type CurrentPerspective} from '../hooks/usePerspective'
-import {type VersionOriginTypes} from '../index'
+import {
+  type ReleasePerspective,
+  type SelectableReleasePerspective,
+  type VersionOriginTypes,
+} from '../index'
 import {type ReleaseDocument} from '../store/types'
-import {LATEST} from './const'
+import {
+  DRAFTS_PERSPECTIVE,
+  type DraftsPerspective,
+  PUBLISHED_PERSPECTIVE,
+  type PublishedPerspective,
+} from './perspective'
 
 /**
  * @beta
@@ -76,13 +84,17 @@ export function getPublishDateFromRelease(release: ReleaseDocument): Date {
 }
 
 /** @internal */
-export function isPublishedPerspective(bundle: CurrentPerspective | string): bundle is 'published' {
-  return bundle === 'published'
+export function isPublishedPerspective(
+  perspective: SelectableReleasePerspective,
+): perspective is PublishedPerspective {
+  return perspective === PUBLISHED_PERSPECTIVE
 }
 
 /** @internal */
-export function isDraftPerspective(bundle: CurrentPerspective | string): bundle is typeof LATEST {
-  return bundle === LATEST
+export function isDraftPerspective(
+  perspective: ReleasePerspective | string,
+): perspective is DraftsPerspective {
+  return perspective === DRAFTS_PERSPECTIVE
 }
 
 /** @internal */
diff --git a/packages/sanity/src/core/studio/components/navbar/StudioNavbar.tsx b/packages/sanity/src/core/studio/components/navbar/StudioNavbar.tsx
index 53f0222e966..a7fd9063be0 100644
--- a/packages/sanity/src/core/studio/components/navbar/StudioNavbar.tsx
+++ b/packages/sanity/src/core/studio/components/navbar/StudioNavbar.tsx
@@ -19,7 +19,7 @@ import {Button, TooltipDelayGroupProvider} from '../../../../ui-components'
 import {type NavbarProps} from '../../../config/studio/types'
 import {isDev} from '../../../environment'
 import {useTranslation} from '../../../i18n'
-import {usePerspective} from '../../../releases'
+import {getPerspectiveTone, useCurrentRelease, useStudioPerspectiveState} from '../../../releases'
 import {getReleaseTone} from '../../../releases/util/getReleaseTone'
 import {useToolMenuComponent} from '../../studio-components-hooks'
 import {useWorkspace} from '../../workspace'
@@ -85,7 +85,8 @@ export function StudioNavbar(props: Omit<NavbarProps, 'renderDefault'>) {
     searchOpen,
   } = useContext(NavbarContext)
 
-  const {currentGlobalBundle} = usePerspective()
+  const {current} = useStudioPerspectiveState()
+  const currentGlobalRelease = useCurrentRelease()
 
   const ToolMenu = useToolMenuComponent()
 
@@ -189,7 +190,11 @@ export function StudioNavbar(props: Omit<NavbarProps, 'renderDefault'>) {
     <FreeTrialProvider>
       <RootLayer zOffset={100} data-search-open={searchFullscreenOpen}>
         <RootCard
-          tone={getReleaseTone(currentGlobalBundle)}
+          tone={
+            currentGlobalRelease
+              ? getReleaseTone(currentGlobalRelease)
+              : getPerspectiveTone(current)
+          }
           borderBottom
           data-testid="studio-navbar"
           data-ui="Navbar"
diff --git a/packages/sanity/src/core/studio/components/navbar/search/components/filters/common/ReferencePreviewTitle.tsx b/packages/sanity/src/core/studio/components/navbar/search/components/filters/common/ReferencePreviewTitle.tsx
index d43381032c0..a9d59daaf60 100644
--- a/packages/sanity/src/core/studio/components/navbar/search/components/filters/common/ReferencePreviewTitle.tsx
+++ b/packages/sanity/src/core/studio/components/navbar/search/components/filters/common/ReferencePreviewTitle.tsx
@@ -16,7 +16,7 @@ export function ReferencePreviewTitle({
   const documentPreviewStore = useDocumentPreviewStore()
 
   const observable = useMemo(
-    () => getPreviewStateObservable(documentPreviewStore, schemaType, documentId, ''),
+    () => getPreviewStateObservable(documentPreviewStore, schemaType, documentId),
     [documentId, documentPreviewStore, schemaType],
   )
   const {draft, published, isLoading} = useObservable(observable, {
diff --git a/packages/sanity/src/core/studio/components/navbar/search/components/searchResults/item/SearchResultItemPreview.tsx b/packages/sanity/src/core/studio/components/navbar/search/components/searchResults/item/SearchResultItemPreview.tsx
index 00dffc26b09..4c955ef7cde 100644
--- a/packages/sanity/src/core/studio/components/navbar/search/components/searchResults/item/SearchResultItemPreview.tsx
+++ b/packages/sanity/src/core/studio/components/navbar/search/components/searchResults/item/SearchResultItemPreview.tsx
@@ -3,7 +3,7 @@ import {type SchemaType} from '@sanity/types'
 import {Badge, Box, Flex} from '@sanity/ui'
 import {useMemo} from 'react'
 import {useObservable} from 'react-rx'
-import {getPublishedId, useReleases} from 'sanity'
+import {getPublishedId, getReleaseIdFromReleaseDocumentId, useReleases} from 'sanity'
 import {styled} from 'styled-components'
 
 import {type GeneralPreviewLayoutKey} from '../../../../../../../components'
@@ -15,7 +15,6 @@ import {
   getPreviewValueWithFallback,
   SanityDefaultPreview,
 } from '../../../../../../../preview'
-import {usePerspective} from '../../../../../../../releases/hooks/usePerspective'
 import {type DocumentPresence, useDocumentPreviewStore} from '../../../../../../../store'
 
 interface SearchResultItemPreviewProps {
@@ -51,16 +50,17 @@ export function SearchResultItemPreview({
   showBadge = true,
 }: SearchResultItemPreviewProps) {
   const documentPreviewStore = useDocumentPreviewStore()
-  const releases = useReleases()
-  const {bundlesPerspective} = usePerspective()
+  const {loading: releasesIsLoading, data: releases} = useReleases()
 
   const observable = useMemo(
     () =>
-      getPreviewStateObservable(documentPreviewStore, schemaType, getPublishedId(documentId), '', {
-        bundleIds: releases.releasesIds,
-        bundleStack: bundlesPerspective,
-      }),
-    [documentPreviewStore, schemaType, documentId, releases.releasesIds, bundlesPerspective],
+      getPreviewStateObservable(
+        documentPreviewStore,
+        schemaType,
+        getPublishedId(documentId),
+        releases.map((release) => getReleaseIdFromReleaseDocumentId(release._id)),
+      ),
+    [documentPreviewStore, schemaType, documentId, releases],
   )
 
   const {
@@ -77,7 +77,7 @@ export function SearchResultItemPreview({
     versions: {},
   })
 
-  const isLoading = previewIsLoading || releases.loading
+  const isLoading = previewIsLoading || releasesIsLoading
 
   const sanityDocument = useMemo(() => {
     return {
diff --git a/packages/sanity/src/core/studio/components/navbar/search/contexts/search/SearchProvider.tsx b/packages/sanity/src/core/studio/components/navbar/search/contexts/search/SearchProvider.tsx
index 616b2435e5f..d65c8dc98b4 100644
--- a/packages/sanity/src/core/studio/components/navbar/search/contexts/search/SearchProvider.tsx
+++ b/packages/sanity/src/core/studio/components/navbar/search/contexts/search/SearchProvider.tsx
@@ -4,7 +4,7 @@ import {SearchContext} from 'sanity/_singletons'
 
 import {type CommandListHandle} from '../../../../../../components'
 import {useSchema} from '../../../../../../hooks'
-import {usePerspective} from '../../../../../../releases/hooks/usePerspective'
+import {useStudioPerspectiveState} from '../../../../../../releases/hooks/useStudioPerspectiveState'
 import {type SearchTerms} from '../../../../../../search'
 import {useCurrentUser} from '../../../../../../store'
 import {resolvePerspectiveOptions} from '../../../../../../util/resolvePerspective'
@@ -32,7 +32,7 @@ interface SearchProviderProps {
 export function SearchProvider({children, fullscreen}: SearchProviderProps) {
   const [onClose, setOnClose] = useState<(() => void) | null>(null)
   const [searchCommandList, setSearchCommandList] = useState<CommandListHandle | null>(null)
-  const {bundlesPerspective} = usePerspective()
+  const {bundlesPerspective} = useStudioPerspectiveState()
   const schema = useSchema()
   const currentUser = useCurrentUser()
   const {
diff --git a/packages/sanity/src/core/tasks/hooks/useDocumentPreviewValues.ts b/packages/sanity/src/core/tasks/hooks/useDocumentPreviewValues.ts
index 2f5ff558094..d7b4ed0cdea 100644
--- a/packages/sanity/src/core/tasks/hooks/useDocumentPreviewValues.ts
+++ b/packages/sanity/src/core/tasks/hooks/useDocumentPreviewValues.ts
@@ -26,7 +26,7 @@ export function useDocumentPreviewValues(options: PreviewHookOptions): PreviewHo
 
   const previewStateObservable = useMemo(() => {
     if (!documentId || !schemaType) return of(null)
-    return getPreviewStateObservable(documentPreviewStore, schemaType, documentId, '')
+    return getPreviewStateObservable(documentPreviewStore, schemaType, documentId)
   }, [documentId, documentPreviewStore, schemaType])
   const previewState = useObservable(previewStateObservable)
 
diff --git a/packages/sanity/src/structure/components/paneHeaderActions/PaneHeaderCreateButton.tsx b/packages/sanity/src/structure/components/paneHeaderActions/PaneHeaderCreateButton.tsx
index 464c4fe39d4..e10868f2b76 100644
--- a/packages/sanity/src/structure/components/paneHeaderActions/PaneHeaderCreateButton.tsx
+++ b/packages/sanity/src/structure/components/paneHeaderActions/PaneHeaderCreateButton.tsx
@@ -3,15 +3,14 @@ import {type Schema} from '@sanity/types'
 import {Menu} from '@sanity/ui'
 import {type ComponentProps, type ForwardedRef, forwardRef, useMemo} from 'react'
 import {
-  getBundleIdFromReleaseDocumentId,
   type InitialValueTemplateItem,
+  isDraftPerspective,
   isPublishedPerspective,
-  LATEST,
   type Template,
   type TemplatePermissionsResult,
   useGetI18nText,
-  usePerspective,
   useSchema,
+  useStudioPerspectiveState,
   useTemplatePermissions,
   useTemplates,
   useTranslation,
@@ -37,23 +36,22 @@ const getIntent = (
   item: InitialValueTemplateItem,
   version?: string,
 ): PaneHeaderIntentProps | null => {
-  const isBundleIntent = version && version !== LATEST._id && !isPublishedPerspective(version)
+  const isReleaseIntent =
+    version && !isDraftPerspective(version) && !isPublishedPerspective(version)
   const typeName = templates.find((t) => t.id === item.templateId)?.schemaType
   if (!typeName) return null
 
   const baseParams = {
     template: item.templateId,
     type: typeName,
-    version: isBundleIntent ? version : undefined,
+    version: isReleaseIntent ? version : undefined,
     id: item.initialDocumentId,
   }
 
   return {
     type: 'create',
     params: item.parameters ? [baseParams, item.parameters] : baseParams,
-    searchParams: isBundleIntent
-      ? [['perspective', `bundle.${getBundleIdFromReleaseDocumentId(version)}`]]
-      : undefined,
+    searchParams: isReleaseIntent ? [['perspective', version]] : undefined,
   }
 }
 
@@ -64,7 +62,7 @@ interface PaneHeaderCreateButtonProps {
 export function PaneHeaderCreateButton({templateItems}: PaneHeaderCreateButtonProps) {
   const schema = useSchema()
   const templates = useTemplates()
-  const {currentGlobalBundleId} = usePerspective()
+  const {current} = useStudioPerspectiveState()
 
   const {t} = useTranslation(structureLocaleNamespace)
   const getI18nText = useGetI18nText([...templateItems, ...templates])
@@ -115,7 +113,7 @@ export function PaneHeaderCreateButton({templateItems}: PaneHeaderCreateButtonPr
     const firstItem = templateItems[0]
     const permissions = permissionsById[firstItem.id]
     const disabled = !permissions?.granted
-    const intent = getIntent(schema, templates, firstItem, currentGlobalBundleId)
+    const intent = getIntent(schema, templates, firstItem, current)
     if (!intent) return null
 
     return (
@@ -153,7 +151,7 @@ export function PaneHeaderCreateButton({templateItems}: PaneHeaderCreateButtonPr
           {templateItems.map((item, itemIndex) => {
             const permissions = permissionsById[item.id]
             const disabled = !permissions?.granted
-            const intent = getIntent(schema, templates, item, currentGlobalBundleId)
+            const intent = getIntent(schema, templates, item, current)
             const template = templates.find((i) => i.id === item.templateId)
             if (!template || !intent) return null
 
diff --git a/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx b/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx
index d427adf48aa..a8779431abc 100644
--- a/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx
+++ b/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx
@@ -1,7 +1,6 @@
 import {type SanityDocument, type SchemaType} from '@sanity/types'
 import {Flex} from '@sanity/ui'
-import {isNumber, isString} from 'lodash'
-import {type ComponentType, isValidElement, useMemo} from 'react'
+import {type ComponentType, useMemo} from 'react'
 import {useObservable} from 'react-rx'
 import {
   type DocumentPresence,
@@ -12,10 +11,9 @@ import {
   type GeneralPreviewLayoutKey,
   getPreviewStateObservable,
   getPreviewValueWithFallback,
-  isRecord,
   SanityDefaultPreview,
-  usePerspective,
   useReleases,
+  useStudioPerspectiveState,
 } from 'sanity'
 
 import {TooltipDelayGroupProvider} from '../../../ui-components'
@@ -38,29 +36,18 @@ export interface PaneItemPreviewProps {
  */
 export function PaneItemPreview(props: PaneItemPreviewProps) {
   const {icon, layout, presence, schemaType, value} = props
-  const title =
-    (isRecord(value.title) && isValidElement(value.title)) ||
-    isString(value.title) ||
-    isNumber(value.title)
-      ? value.title
-      : null
 
   const releases = useReleases()
-  const {bundlesPerspective, perspective} = usePerspective()
+  const {current} = useStudioPerspectiveState()
   const previewStateObservable = useMemo(
     () =>
-      getPreviewStateObservable(props.documentPreviewStore, schemaType, value._id, title, {
-        bundleIds: releases.releasesIds,
-        bundleStack: bundlesPerspective,
-      }),
-    [
-      props.documentPreviewStore,
-      schemaType,
-      value._id,
-      title,
-      releases.releasesIds,
-      bundlesPerspective,
-    ],
+      getPreviewStateObservable(
+        props.documentPreviewStore,
+        schemaType,
+        value._id,
+        releases.releasesIds,
+      ),
+    [props.documentPreviewStore, schemaType, value._id, releases.releasesIds],
   )
 
   const {
@@ -75,7 +62,7 @@ export function PaneItemPreview(props: PaneItemPreviewProps) {
     published: null,
     version: null,
     versions: {},
-    perspective,
+    current: current,
   })
 
   const isLoading = previewIsLoading || releases.loading
@@ -93,7 +80,7 @@ export function PaneItemPreview(props: PaneItemPreviewProps) {
 
   return (
     <SanityDefaultPreview
-      {...getPreviewValueWithFallback({value, draft, published, version, perspective})}
+      {...getPreviewValueWithFallback({value, draft, published, version})}
       isPlaceholder={isLoading}
       icon={icon}
       layout={layout}
diff --git a/packages/sanity/src/structure/components/structureTool/StructureTitle.tsx b/packages/sanity/src/structure/components/structureTool/StructureTitle.tsx
index 5941df863bc..dc20dc5c348 100644
--- a/packages/sanity/src/structure/components/structureTool/StructureTitle.tsx
+++ b/packages/sanity/src/structure/components/structureTool/StructureTitle.tsx
@@ -4,8 +4,8 @@ import {
   resolveBundlePerspective,
   unstable_useValuePreview as useValuePreview,
   useEditState,
-  usePerspective,
   useSchema,
+  useStudioPerspectiveState,
   useTranslation,
 } from 'sanity'
 
@@ -22,13 +22,13 @@ interface StructureTitleProps {
 // TODO: Fix state jank when editing different versions inside panes.
 const DocumentTitle = (props: {documentId: string; documentType: string}) => {
   const {documentId, documentType} = props
-  const {perspective} = usePerspective()
+  const {current} = useStudioPerspectiveState()
 
   const editState = useEditState(
     documentId,
     documentType,
     'default',
-    resolveBundlePerspective(perspective),
+    resolveBundlePerspective(current),
   )
   const schema = useSchema()
   const {t} = useTranslation(structureLocaleNamespace)
diff --git a/packages/sanity/src/structure/panes/document/DocumentPane.tsx b/packages/sanity/src/structure/panes/document/DocumentPane.tsx
index ed3cad8f7b9..5cc53198830 100644
--- a/packages/sanity/src/structure/panes/document/DocumentPane.tsx
+++ b/packages/sanity/src/structure/panes/document/DocumentPane.tsx
@@ -8,8 +8,8 @@ import {
   SourceProvider,
   Translate,
   useDocumentType,
-  usePerspective,
   useSource,
+  useStudioPerspectiveState,
   useTemplatePermissions,
   useTemplates,
   useTranslation,
@@ -45,7 +45,7 @@ export const DocumentPane = memo(function DocumentPane(props: DocumentPaneProvid
 function DocumentPaneInner(props: DocumentPaneProviderProps) {
   const {pane, paneKey} = props
   const {resolveNewDocumentOptions} = useSource().document
-  const {perspective} = usePerspective()
+  const {current} = useStudioPerspectiveState()
   const paneRouter = usePaneRouter()
   const options = usePaneOptions(pane.options, paneRouter.params)
   const {documentType, isLoaded: isDocumentLoaded} = useDocumentType(options.id, options.type)
@@ -132,7 +132,7 @@ function DocumentPaneInner(props: DocumentPaneProviderProps) {
     <DocumentPaneProvider
       // this needs to be here to avoid formState from being re-used across (incompatible) document types
       // see https://github.com/sanity-io/sanity/discussions/3794 for a description of the problem
-      key={`${documentType}-${options.id}-${perspective || ''}`}
+      key={`${documentType}-${options.id}-${current || ''}`}
       {...providerProps}
     >
       {/* NOTE: this is a temporary location for this provider until we */}
diff --git a/packages/sanity/src/structure/panes/document/DocumentPaneProvider.tsx b/packages/sanity/src/structure/panes/document/DocumentPaneProvider.tsx
index ffbeb7bf329..fcae5db68a9 100644
--- a/packages/sanity/src/structure/panes/document/DocumentPaneProvider.tsx
+++ b/packages/sanity/src/structure/panes/document/DocumentPaneProvider.tsx
@@ -33,16 +33,17 @@ import {
   toMutationPatches,
   useConnectionState,
   useCopyPaste,
+  useCurrentRelease,
   useDocumentOperation,
   useDocumentValuePermissions,
   useDocumentVersions,
   useEditState,
   useFormState,
   useInitialValue,
-  usePerspective,
   usePresenceStore,
   useSchema,
   useSource,
+  useStudioPerspectiveState,
   useTemplates,
   useTimelineSelector,
   useTimelineStore,
@@ -103,9 +104,10 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
   const documentId = getPublishedId(documentIdRaw)
   const documentType = options.type
   const params = useUnique(paneRouter.params) || EMPTY_PARAMS
-  const {perspective, currentGlobalBundle} = usePerspective()
+  const {current} = useStudioPerspectiveState()
+  const currentGlobalRelease = useCurrentRelease()
 
-  const bundlePerspective = resolveBundlePerspective(perspective)
+  const bundlePerspective = resolveBundlePerspective(current)
 
   /* Version and the global perspective should match.
    * If user clicks on add document, and then switches to another version, he should click again on create document.
@@ -150,7 +152,7 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
     case typeof bundlePerspective !== 'undefined':
       value = editState.version || editState.draft || editState.published || value
       break
-    case perspective === 'published':
+    case current === 'published':
       value = editState.published || editState.draft || value
       break
     default:
@@ -172,7 +174,7 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
       case Boolean(params):
         version = 'revision'
         break
-      case perspective === 'published':
+      case current === 'published':
         version = 'published'
         break
       default:
@@ -180,7 +182,7 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
     }
 
     return version
-  }, [bundlePerspective, params, perspective, value._id])
+  }, [bundlePerspective, params, current, value._id])
 
   const actionsPerspective = useMemo(() => getDocumentPerspective(), [getDocumentPerspective])
 
@@ -598,11 +600,11 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
     // in cases where the document has drafts but the schema is live edit,
     // there is a risk of data loss, so we disable editing in this case
     const isLiveEditAndDraft = Boolean(liveEdit && editState.draft)
-    const isSystemPerspectiveApplied = perspective && typeof bundlePerspective === 'undefined'
+    const isSystemPerspectiveApplied = current && typeof bundlePerspective === 'undefined'
 
     const isReleaseLocked =
-      typeof currentGlobalBundle === 'object' && 'state' in currentGlobalBundle
-        ? isReleaseScheduledOrScheduling(currentGlobalBundle)
+      typeof currentGlobalRelease === 'object' && 'state' in currentGlobalRelease
+        ? isReleaseScheduledOrScheduling(currentGlobalRelease)
         : false
 
     return (
@@ -630,9 +632,9 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
     editState.transactionSyncLock?.enabled,
     editState.draft,
     liveEdit,
-    perspective,
+    current,
     bundlePerspective,
-    currentGlobalBundle,
+    currentGlobalRelease,
     existsInBundle,
     ready,
     revTime,
diff --git a/packages/sanity/src/structure/panes/document/comments/CommentsWrapper.tsx b/packages/sanity/src/structure/panes/document/comments/CommentsWrapper.tsx
index 5965b43b748..baade00b928 100644
--- a/packages/sanity/src/structure/panes/document/comments/CommentsWrapper.tsx
+++ b/packages/sanity/src/structure/panes/document/comments/CommentsWrapper.tsx
@@ -5,7 +5,7 @@ import {
   CommentsProvider,
   getVersionId,
   useCommentsEnabled,
-  usePerspective,
+  useStudioPerspectiveState,
 } from 'sanity'
 
 import {usePaneRouter} from '../../../components'
@@ -40,7 +40,7 @@ function CommentsProviderWrapper(props: CommentsWrapperProps) {
   const {enabled} = useCommentsEnabled()
   const {connectionState, onPathOpen, inspector, openInspector, version} = useDocumentPane()
   const {params, setParams, createPathWithParams} = usePaneRouter()
-  const {perspective} = usePerspective()
+  const {current} = useStudioPerspectiveState()
   const versionOrPublishedId = useMemo(
     () => (version ? getVersionId(documentId, version) : documentId),
     [documentId, version],
@@ -97,7 +97,7 @@ function CommentsProviderWrapper(props: CommentsWrapperProps) {
       selectedCommentId={selectedCommentId}
       sortOrder="desc"
       type="field"
-      perspective={perspective}
+      perspective={current}
     >
       {children}
     </CommentsProvider>
diff --git a/packages/sanity/src/structure/panes/document/document-layout/useBundleDeletedToast.tsx b/packages/sanity/src/structure/panes/document/document-layout/useBundleDeletedToast.tsx
index 24f49c2a6a3..4762068666f 100644
--- a/packages/sanity/src/structure/panes/document/document-layout/useBundleDeletedToast.tsx
+++ b/packages/sanity/src/structure/panes/document/document-layout/useBundleDeletedToast.tsx
@@ -1,31 +1,23 @@
 import {Text, useToast} from '@sanity/ui'
 import {useEffect} from 'react'
-import {
-  isDraftPerspective,
-  isPublishedPerspective,
-  Translate,
-  usePerspective,
-  useReleases,
-  useTranslation,
-} from 'sanity'
+import {Translate, useCurrentRelease, useReleases, useTranslation} from 'sanity'
 
 export const useBundleDeletedToast = () => {
-  const {currentGlobalBundle} = usePerspective()
+  const currentRelease = useCurrentRelease()
   const {data: bundles} = useReleases()
   const toast = useToast()
   const {t} = useTranslation()
 
   useEffect(() => {
-    if (isPublishedPerspective(currentGlobalBundle) || isDraftPerspective(currentGlobalBundle))
-      return
+    if (!currentRelease) return
 
-    const hasCheckedOutBundleBeenArchived = currentGlobalBundle.state === 'archived'
+    const hasCheckedOutBundleBeenArchived = currentRelease.state === 'archived'
 
     if (hasCheckedOutBundleBeenArchived) {
       const {
         metadata: {title: deletedBundleTitle},
         _id: deletedBundleId,
-      } = currentGlobalBundle
+      } = currentRelease
 
       toast.push({
         id: `bundle-deleted-toast-${deletedBundleId}`,
@@ -42,5 +34,5 @@ export const useBundleDeletedToast = () => {
         duration: 10000,
       })
     }
-  }, [bundles?.length, toast, t, currentGlobalBundle])
+  }, [bundles?.length, toast, t, currentRelease])
 }
diff --git a/packages/sanity/src/structure/panes/document/documentPanel/DocumentPanel.tsx b/packages/sanity/src/structure/panes/document/documentPanel/DocumentPanel.tsx
index 94293fb61fc..078e9637197 100644
--- a/packages/sanity/src/structure/panes/document/documentPanel/DocumentPanel.tsx
+++ b/packages/sanity/src/structure/panes/document/documentPanel/DocumentPanel.tsx
@@ -4,9 +4,9 @@ import {
   isDraftPerspective,
   isPublishedPerspective,
   isReleaseScheduledOrScheduling,
-  type ReleaseDocument,
   ScrollContainer,
-  usePerspective,
+  useCurrentRelease,
+  useStudioPerspectiveState,
   VirtualizerScrollInstanceProvider,
 } from 'sanity'
 import {css, styled} from 'styled-components'
@@ -69,10 +69,10 @@ export const DocumentPanel = function DocumentPanel(props: DocumentPanelProps) {
     permissions,
     isPermissionsLoading,
     existsInBundle,
-    documentType,
   } = useDocumentPane()
   const {collapsed: layoutCollapsed} = usePaneLayout()
   const {collapsed} = usePane()
+  const currentRelease = useCurrentRelease()
   const parentPortal = usePortal()
   const {features} = useStructureTool()
   const portalRef = useRef<HTMLDivElement | null>(null)
@@ -140,23 +140,18 @@ export const DocumentPanel = function DocumentPanel(props: DocumentPanelProps) {
   }, [isInspectOpen, displayed, value])
 
   const showInspector = Boolean(!collapsed && inspector)
-  const {currentGlobalBundle} = usePerspective()
+  const {current} = useStudioPerspectiveState()
 
   const currentPerspectiveIsRelease =
-    !isPublishedPerspective(currentGlobalBundle) && !isDraftPerspective(currentGlobalBundle)
+    current && !isPublishedPerspective(current) && !isDraftPerspective(current)
   const isScheduledRelease =
-    typeof currentGlobalBundle === 'object' && 'state' in currentGlobalBundle
-      ? isReleaseScheduledOrScheduling(currentGlobalBundle)
+    typeof current === 'object' && 'state' in current
+      ? isReleaseScheduledOrScheduling(current)
       : false
 
   const banners = useMemo(() => {
-    if ((!existsInBundle && currentPerspectiveIsRelease) || isScheduledRelease) {
-      return (
-        <AddToReleaseBanner
-          documentId={value._id}
-          currentRelease={currentGlobalBundle as ReleaseDocument}
-        />
-      )
+    if (currentRelease && (!existsInBundle || isScheduledRelease)) {
+      return <AddToReleaseBanner documentId={value._id} currentRelease={currentRelease} />
     }
 
     if (activeView.type === 'form' && isLiveEdit && ready) {
@@ -184,8 +179,7 @@ export const DocumentPanel = function DocumentPanel(props: DocumentPanelProps) {
     )
   }, [
     activeView.type,
-    currentGlobalBundle,
-    currentPerspectiveIsRelease,
+    currentRelease,
     displayed,
     documentId,
     existsInBundle,
diff --git a/packages/sanity/src/structure/panes/document/documentPanel/banners/AddToReleaseBanner.tsx b/packages/sanity/src/structure/panes/document/documentPanel/banners/AddToReleaseBanner.tsx
index f1bf0f8b56b..3684adf88a7 100644
--- a/packages/sanity/src/structure/panes/document/documentPanel/banners/AddToReleaseBanner.tsx
+++ b/packages/sanity/src/structure/panes/document/documentPanel/banners/AddToReleaseBanner.tsx
@@ -3,11 +3,12 @@ import {Flex, Text} from '@sanity/ui'
 import {type CSSProperties, useCallback} from 'react'
 import {
   formatRelativeLocale,
-  getBundleIdFromReleaseDocumentId,
+  getPerspectiveTone,
   getPublishDateFromRelease,
+  getReleaseIdFromReleaseDocumentId,
   getReleaseTone,
   isReleaseScheduledOrScheduling,
-  LATEST,
+  PUBLISHED_PERSPECTIVE,
   type ReleaseDocument,
   Translate,
   useTranslation,
@@ -25,7 +26,9 @@ export function AddToReleaseBanner({
   documentId: string
   currentRelease: ReleaseDocument
 }): JSX.Element {
-  const tone = getReleaseTone(currentRelease ?? LATEST)
+  const tone = currentRelease
+    ? getReleaseTone(currentRelease)
+    : getPerspectiveTone(PUBLISHED_PERSPECTIVE)
   const {t} = useTranslation(structureLocaleNamespace)
   const {t: tCore} = useTranslation()
 
@@ -37,7 +40,7 @@ export function AddToReleaseBanner({
 
   const handleAddToRelease = useCallback(async () => {
     if (currentRelease._id) {
-      await createVersion(getBundleIdFromReleaseDocumentId(currentRelease._id), documentId)
+      await createVersion(getReleaseIdFromReleaseDocumentId(currentRelease._id), documentId)
     }
   }, [createVersion, currentRelease._id, documentId])
 
diff --git a/packages/sanity/src/structure/panes/document/documentPanel/banners/DeletedDocumentBanners.tsx b/packages/sanity/src/structure/panes/document/documentPanel/banners/DeletedDocumentBanners.tsx
index 98bbb9d537f..0377839522d 100644
--- a/packages/sanity/src/structure/panes/document/documentPanel/banners/DeletedDocumentBanners.tsx
+++ b/packages/sanity/src/structure/panes/document/documentPanel/banners/DeletedDocumentBanners.tsx
@@ -2,12 +2,10 @@ import {DocumentRemoveIcon, ReadOnlyIcon} from '@sanity/icons'
 import {Text} from '@sanity/ui'
 import {useCallback} from 'react'
 import {
-  isDraftPerspective,
-  isPublishedPerspective,
   type ReleaseDocument,
   Translate,
   useDocumentOperation,
-  usePerspective,
+  useStudioPerspectiveState,
   useTimelineSelector,
   useTranslation,
 } from 'sanity'
@@ -19,14 +17,10 @@ import {Banner} from './Banner'
 
 export function DeletedDocumentBanners() {
   const {isDeleted, isDeleting} = useDocumentPane()
-  const {currentGlobalBundle} = usePerspective()
+  const {currentGlobalRelease} = useStudioPerspectiveState()
 
-  if (
-    !isPublishedPerspective(currentGlobalBundle) &&
-    !isDraftPerspective(currentGlobalBundle) &&
-    currentGlobalBundle.state === 'archived'
-  ) {
-    return <ArchivedReleaseBanner release={currentGlobalBundle as ReleaseDocument} />
+  if (currentGlobalRelease?.state === 'archived') {
+    return <ArchivedReleaseBanner release={currentGlobalRelease as ReleaseDocument} />
   }
   if (isDeleted && !isDeleting) return <DeletedDocumentBanner />
 }
diff --git a/packages/sanity/src/structure/panes/document/documentPanel/banners/ReferenceChangedBanner.tsx b/packages/sanity/src/structure/panes/document/documentPanel/banners/ReferenceChangedBanner.tsx
index 7e0f2b060a2..74363530d7f 100644
--- a/packages/sanity/src/structure/panes/document/documentPanel/banners/ReferenceChangedBanner.tsx
+++ b/packages/sanity/src/structure/panes/document/documentPanel/banners/ReferenceChangedBanner.tsx
@@ -11,7 +11,7 @@ import {
   getPublishedId,
   resolveBundlePerspective,
   useDocumentPreviewStore,
-  usePerspective,
+  useStudioPerspectiveState,
   useTranslation,
 } from 'sanity'
 
@@ -34,7 +34,7 @@ interface ParentReferenceInfo {
 
 export const ReferenceChangedBanner = memo(() => {
   const documentPreviewStore = useDocumentPreviewStore()
-  const {perspective} = usePerspective()
+  const {current} = useStudioPerspectiveState()
   const {params, groupIndex, routerPanesState, replaceCurrent, BackLink} = usePaneRouter()
   const routerReferenceId = routerPanesState[groupIndex]?.[0].id
   const parentGroup = routerPanesState[groupIndex - 1] as RouterPaneGroup | undefined
@@ -46,7 +46,7 @@ export const ReferenceChangedBanner = memo(() => {
   }, [params?.parentRefPath])
   const {t} = useTranslation(structureLocaleNamespace)
 
-  const bundlePerspective = resolveBundlePerspective(perspective)
+  const bundlePerspective = resolveBundlePerspective(current)
 
   /**
    * Loads information regarding the reference field of the parent pane. This
diff --git a/packages/sanity/src/structure/panes/document/documentPanel/banners/__tests__/DeletedDocumentBanners.test.tsx b/packages/sanity/src/structure/panes/document/documentPanel/banners/__tests__/DeletedDocumentBanners.test.tsx
index 0d32a522ea8..570be7bf703 100644
--- a/packages/sanity/src/structure/panes/document/documentPanel/banners/__tests__/DeletedDocumentBanners.test.tsx
+++ b/packages/sanity/src/structure/panes/document/documentPanel/banners/__tests__/DeletedDocumentBanners.test.tsx
@@ -1,5 +1,5 @@
 import {render, screen, waitFor} from '@testing-library/react'
-import {LATEST, type ReleaseDocument, usePerspective, useReleases} from 'sanity'
+import {LATEST, type ReleaseDocument, useReleases, useStudioPerspectiveState} from 'sanity'
 import {useDocumentPane} from 'sanity/structure'
 import {describe, expect, it, type Mock, vi} from 'vitest'
 
@@ -25,7 +25,7 @@ vi.mock('../../../../../../core/store/_legacy/history/useTimelineSelector', () =
 
 const mockUseDocumentPane = useDocumentPane as Mock<typeof useDocumentPane>
 const mockUseReleases = useReleases as Mock<typeof useReleases>
-const mockUsePerspective = usePerspective as Mock<typeof usePerspective>
+const mockUsePerspective = useStudioPerspectiveState as Mock<typeof useStudioPerspectiveState>
 
 const renderTest = async () => {
   const wrapper = await createTestProvider({resources: [structureUsEnglishLocaleBundle]})
@@ -35,8 +35,8 @@ const renderTest = async () => {
 
 describe('DeletedDocumentBanners', () => {
   it('does not show either banner when document is not deleted', async () => {
-    mockUsePerspective.mockReturnValue({currentGlobalBundle: {_id: 'test'}} as ReturnType<
-      typeof usePerspective
+    mockUsePerspective.mockReturnValue({currentGlobalRelease: {_id: 'test'}} as ReturnType<
+      typeof useStudioPerspectiveState
     >)
     mockUseReleases.mockReturnValue({
       data: [],
@@ -58,8 +58,8 @@ describe('DeletedDocumentBanners', () => {
 
   it('prefers to show release deleted banner when document was in a release', async () => {
     const mockReleaseDocument = {_id: 'test', state: 'archived'} as ReleaseDocument
-    mockUsePerspective.mockReturnValue({currentGlobalBundle: mockReleaseDocument} as ReturnType<
-      typeof usePerspective
+    mockUsePerspective.mockReturnValue({currentGlobalRelease: mockReleaseDocument} as ReturnType<
+      typeof useStudioPerspectiveState
     >)
     mockUseReleases.mockReturnValue({
       data: [mockReleaseDocument],
@@ -85,9 +85,9 @@ describe('DeletedDocumentBanners', () => {
     const mockBundleDocument: ReleaseDocument = {_id: 'test', state: 'archived'} as ReleaseDocument
 
     mockUsePerspective.mockReturnValue({
-      currentGlobalBundle: LATEST,
-      setPerspective: vi.fn(),
-    } as ReturnType<typeof usePerspective>)
+      currentGlobalRelease: LATEST,
+      setCurrent: vi.fn(),
+    } as ReturnType<typeof useStudioPerspectiveState>)
 
     mockUseReleases.mockReturnValue({
       data: [mockBundleDocument],
diff --git a/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveList.tsx b/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveList.tsx
index 543ec821541..8ec2e7f7b0c 100644
--- a/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveList.tsx
+++ b/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveList.tsx
@@ -1,21 +1,26 @@
 import {Text} from '@sanity/ui'
-import {memo, useCallback, useMemo} from 'react'
+import {memo, useMemo} from 'react'
 import {
   formatRelativeLocale,
   getPublishDateFromRelease,
   getReleaseTone,
   getVersionFromId,
   isReleaseScheduledOrScheduling,
+  PUBLISHED_PERSPECTIVE,
   type ReleaseDocument,
   Translate,
   useDateTimeFormat,
-  usePerspective,
   useReleases,
+  useStudioPerspectiveState,
   useTranslation,
   VersionChip,
   versionDocumentExists,
 } from 'sanity'
 
+import {
+  DRAFTS_PERSPECTIVE,
+  getReleaseIdFromReleaseDocumentId,
+} from '../../../../../../core/releases'
 import {useDocumentPane} from '../../../useDocumentPane'
 
 type FilterReleases = {
@@ -64,9 +69,9 @@ const TooltipContent = ({release}: {release: ReleaseDocument}) => {
 }
 
 export const DocumentPerspectiveList = memo(function DocumentPerspectiveList() {
-  const {perspective} = usePerspective()
+  const {current} = useStudioPerspectiveState()
   const {t} = useTranslation()
-  const {setPerspective} = usePerspective()
+  const {setCurrent} = useStudioPerspectiveState()
   const dateTimeFormat = useDateTimeFormat({
     dateStyle: 'medium',
     timeStyle: 'short',
@@ -92,13 +97,6 @@ export const DocumentPerspectiveList = memo(function DocumentPerspectiveList() {
     )
   }, [documentVersions, releases])
 
-  const handleBundleChange = useCallback(
-    (bundleId: string) => () => {
-      setPerspective(bundleId)
-    },
-    [setPerspective],
-  )
-
   return (
     <>
       <VersionChip
@@ -116,7 +114,9 @@ export const DocumentPerspectiveList = memo(function DocumentPerspectiveList() {
           </Text>
         }
         disabled={!editState?.published}
-        onClick={handleBundleChange('published')}
+        onClick={() => () => {
+          setCurrent(PUBLISHED_PERSPECTIVE)
+        }}
         selected={
           /** the publish is selected when:
            * when the document displayed is a published document, but has no draft and the perspective that is
@@ -124,8 +124,8 @@ export const DocumentPerspectiveList = memo(function DocumentPerspectiveList() {
            * when the perspective is published
            */
           !!(
-            (editState?.published?._id === displayed?._id && !editState?.draft && perspective) ||
-            perspective === 'published'
+            (editState?.published?._id === displayed?._id && !editState?.draft && current) ||
+            current === 'published'
           )
         }
         text={t('release.chip.published')}
@@ -172,18 +172,16 @@ export const DocumentPerspectiveList = memo(function DocumentPerspectiveList() {
            * when the document is not published and the displayed version is draft,
            * when there is no draft (new document),
            */
-          !!(
-            editState?.draft?._id === displayed?._id ||
-            !perspective ||
-            (!editState?.published &&
-              editState?.draft &&
-              editState?.draft?._id === displayed?._id) ||
-            (!editState?.published && !editState?.draft)
-          )
+          editState?.draft?._id === displayed?._id ||
+          !current ||
+          (!editState?.published && editState?.draft && editState?.draft?._id === displayed?._id) ||
+          (!editState?.published && !editState?.draft)
         }
         text={t('release.chip.draft')}
         tone="caution"
-        onClick={handleBundleChange('drafts')}
+        onClick={() => () => {
+          setCurrent(DRAFTS_PERSPECTIVE)
+        }}
         contextValues={{
           documentId: editState?.draft?._id || editState?.published?._id || editState?.id || '',
           menuReleaseId: editState?.draft?._id || editState?.published?._id || editState?.id || '',
@@ -202,7 +200,9 @@ export const DocumentPerspectiveList = memo(function DocumentPerspectiveList() {
             key={release._id}
             tooltipContent={<TooltipContent release={release} />}
             selected={release.name === getVersionFromId(displayed?._id || '')}
-            onClick={handleBundleChange(release.name)}
+            onClick={() => () => {
+              setCurrent(getReleaseIdFromReleaseDocumentId(release._id))
+            }}
             text={release.metadata.title || t('release.placeholder-untitled-release')}
             tone={getReleaseTone(release)}
             locked={isReleaseScheduledOrScheduling(release)}
diff --git a/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/__tests__/DocumentPerspectiveList.test.tsx b/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/__tests__/DocumentPerspectiveList.test.tsx
index 38d3f54a10a..b3d14be74f5 100644
--- a/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/__tests__/DocumentPerspectiveList.test.tsx
+++ b/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/__tests__/DocumentPerspectiveList.test.tsx
@@ -1,6 +1,6 @@
 import {render, screen} from '@testing-library/react'
 import {type HTMLProps} from 'react'
-import {type ReleaseDocument, usePerspective, useReleases} from 'sanity'
+import {type ReleaseDocument, useReleases, useStudioPerspectiveState} from 'sanity'
 import {type IntentLinkProps} from 'sanity/router'
 import {beforeEach, describe, expect, it, type Mock, type MockedFunction, vi} from 'vitest'
 
@@ -46,7 +46,7 @@ const mockUseDocumentPane = useDocumentPane as MockedFunction<
   () => Partial<DocumentPaneContextValue>
 >
 
-const mockUsePerspective = usePerspective as Mock<typeof usePerspective>
+const mockUsePerspective = useStudioPerspectiveState as Mock<typeof useStudioPerspectiveState>
 const mockUseReleases = useReleases as Mock<typeof useReleases>
 
 describe('DocumentPerspectiveList', () => {
@@ -68,16 +68,16 @@ describe('DocumentPerspectiveList', () => {
   beforeEach(() => {
     vi.clearAllMocks()
     mockUsePerspective.mockReturnValue({
-      currentGlobalBundle: mockCurrent,
-      setPerspective: vi.fn(),
+      currentGlobalRelease: mockCurrent,
+      setCurrent: vi.fn(),
       bundlesPerspective: ['drafts'],
-      currentGlobalBundleId: '_.releases.spring-drop',
-      excludedPerspectives: [],
+      current: '_.releases.spring-drop',
+      excluded: [],
       isPerspectiveExcluded: vi.fn().mockReturnValue(false),
-      perspective: undefined,
-      toggleExcludedPerspective: vi.fn(),
+      current: undefined,
+      toggle: vi.fn(),
       setPerspectiveFromReleaseDocumentId: vi.fn(),
-      setPerspectiveFromReleaseId: vi.fn(),
+      setCurrent: vi.fn(),
     })
 
     mockUseDocumentPane.mockReturnValue({
diff --git a/packages/sanity/src/structure/panes/document/inspectors/validation/index.ts b/packages/sanity/src/structure/panes/document/inspectors/validation/index.ts
index 93bd08349bd..b02b2c8d9b8 100644
--- a/packages/sanity/src/structure/panes/document/inspectors/validation/index.ts
+++ b/packages/sanity/src/structure/panes/document/inspectors/validation/index.ts
@@ -8,7 +8,7 @@ import {
   isValidationError,
   isValidationWarning,
   resolveBundlePerspective,
-  usePerspective,
+  useStudioPerspectiveState,
   useTranslation,
   useValidationStatus,
 } from 'sanity'
@@ -19,11 +19,11 @@ import {ValidationInspector} from './ValidationInspector'
 function useMenuItem(props: DocumentInspectorUseMenuItemProps): DocumentInspectorMenuItem {
   const {documentId, documentType} = props
   const {t} = useTranslation('validation')
-  const {perspective} = usePerspective()
+  const {current} = useStudioPerspectiveState()
   const {validation: validationMarkers} = useValidationStatus(
     documentId,
     documentType,
-    resolveBundlePerspective(perspective),
+    resolveBundlePerspective(current),
   )
 
   const validation: FormNodeValidation[] = useMemo(
diff --git a/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusBar.tsx b/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusBar.tsx
index 9101b9c8e39..cc8c55820f0 100644
--- a/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusBar.tsx
+++ b/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusBar.tsx
@@ -5,8 +5,8 @@ import {
   isDraftPerspective,
   isPublishedPerspective,
   isSanityCreateLinked,
-  usePerspective,
   useSanityCreateConfig,
+  useStudioPerspectiveState,
   useTimelineSelector,
 } from 'sanity'
 
@@ -30,7 +30,7 @@ const CONTAINER_BREAKPOINT = 480 // px
 export function DocumentStatusBar(props: DocumentStatusBarProps) {
   const {actionsBoxRef, createLinkMetadata} = props
   const {editState, timelineStore, onChange: onDocumentChange, existsInBundle} = useDocumentPane()
-  const {currentGlobalBundle} = usePerspective()
+  const {current} = useStudioPerspectiveState()
   const {title} = useDocumentTitle()
 
   const CreateLinkedActions = useSanityCreateConfig().components?.documentLinkedActions
@@ -64,8 +64,8 @@ export function DocumentStatusBar(props: DocumentStatusBarProps) {
     actions = <HistoryStatusBarActions />
   } else if (
     (existsInBundle && showingVersion) ||
-    isDraftPerspective(currentGlobalBundle) ||
-    isPublishedPerspective(currentGlobalBundle)
+    isDraftPerspective(current) ||
+    isPublishedPerspective(current)
   ) {
     actions = <DocumentStatusBarActions />
   }
diff --git a/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusLine.tsx b/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusLine.tsx
index 393421b6dd8..42b65f8354d 100644
--- a/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusLine.tsx
+++ b/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusLine.tsx
@@ -4,17 +4,17 @@ import {useObservable} from 'react-rx'
 import {of} from 'rxjs'
 import {
   DocumentStatus,
-  getBundleIdFromReleaseDocumentId,
   getPreviewStateObservable,
-  isDraftPerspective,
+  getReleaseIdFromReleaseDocumentId,
   isPublishedPerspective,
   useDocumentPreviewStore,
-  usePerspective,
   useReleases,
   useSchema,
+  useStudioPerspectiveState,
   useSyncState,
 } from 'sanity'
 
+import {useCurrentRelease} from '../../../../core/releases/hooks/useCurrentRelease'
 import {Tooltip} from '../../../../ui-components'
 import {useDocumentPane} from '../useDocumentPane'
 import {DocumentStatusPulse} from './DocumentStatusPulse'
@@ -29,19 +29,17 @@ export function DocumentStatusLine() {
   const schema = useSchema()
   const schemaType = schema.get(documentType)
   const releases = useReleases()
-  const {currentGlobalBundle, bundlesPerspective} = usePerspective()
-  const previewStateObservable = useMemo(
-    () =>
-      schemaType
-        ? getPreviewStateObservable(documentPreviewStore, schemaType, value._id, 'Untitled', {
-            bundleIds: (releases.data ?? []).map((release) =>
-              getBundleIdFromReleaseDocumentId(release._id),
-            ),
-            bundleStack: bundlesPerspective,
-          })
-        : of({versions: {}}),
-    [documentPreviewStore, schemaType, value._id, releases.data, bundlesPerspective],
-  )
+
+  const {current: currentPerspective} = useStudioPerspectiveState()
+  const currentRelease = useCurrentRelease()
+  const previewStateObservable = useMemo(() => {
+    const releaseIds = (releases.data ?? []).map((release) =>
+      getReleaseIdFromReleaseDocumentId(release._id),
+    )
+    return schemaType
+      ? getPreviewStateObservable(documentPreviewStore, schemaType, value._id, releaseIds)
+      : of({versions: {}})
+  }, [documentPreviewStore, schemaType, value._id, releases.data])
   const {versions} = useObservable(previewStateObservable, {versions: {}})
 
   const syncState = useSyncState(documentId, documentType, {version: editState?.bundleId})
@@ -76,21 +74,17 @@ export function DocumentStatusLine() {
   }, [syncState.isSyncing, lastUpdated])
 
   const getMode = () => {
-    if (isPublishedPerspective(currentGlobalBundle)) {
-      return 'published'
-    }
-    if (editState?.version) {
-      return 'version'
-    }
-    if (editState?.draft) {
+    if (!currentPerspective) {
       return 'draft'
     }
-    return 'published'
+    if (isPublishedPerspective(currentPerspective)) {
+      return 'published'
+    }
+    return 'version'
   }
   const mode = getMode()
 
-  const isReleasePerspective =
-    !isPublishedPerspective(currentGlobalBundle) && !isDraftPerspective(currentGlobalBundle)
+  const isReleasePerspective = currentPerspective && !isPublishedPerspective(currentPerspective)
 
   if (status) {
     return <DocumentStatusPulse status={status || undefined} />
@@ -112,12 +106,9 @@ export function DocumentStatusLine() {
           draft={mode === 'draft' ? editState?.draft : undefined}
           published={mode === 'published' ? editState?.published : undefined}
           versions={
-            mode === 'version' &&
-            isReleasePerspective &&
-            currentGlobalBundle.name &&
-            editState?.version
+            mode === 'version' && isReleasePerspective && currentRelease?.name && editState?.version
               ? {
-                  [currentGlobalBundle.name]: {snapshot: editState?.version},
+                  [currentRelease.name]: {snapshot: editState?.version},
                 }
               : undefined
           }
diff --git a/packages/sanity/src/structure/panes/document/useDocumentTitle.ts b/packages/sanity/src/structure/panes/document/useDocumentTitle.ts
index a14f02415b6..6b1158c721c 100644
--- a/packages/sanity/src/structure/panes/document/useDocumentTitle.ts
+++ b/packages/sanity/src/structure/panes/document/useDocumentTitle.ts
@@ -1,7 +1,7 @@
 import {
   isPublishedPerspective,
   unstable_useValuePreview as useValuePreview,
-  usePerspective,
+  useStudioPerspectiveState,
 } from 'sanity'
 
 import {useDocumentPane} from './useDocumentPane'
@@ -27,9 +27,9 @@ interface UseDocumentTitle {
  */
 export function useDocumentTitle(): UseDocumentTitle {
   const {connectionState, schemaType, title, editState} = useDocumentPane()
-  const {perspective} = usePerspective()
+  const {current} = useStudioPerspectiveState()
   const documentValue =
-    perspective && isPublishedPerspective(perspective)
+    current && isPublishedPerspective(current)
       ? editState?.published
       : editState?.version || editState?.draft || editState?.published
   const subscribed = Boolean(documentValue)
diff --git a/packages/sanity/src/structure/panes/documentList/DocumentListPane.tsx b/packages/sanity/src/structure/panes/documentList/DocumentListPane.tsx
index 48c1cca7a50..af7273891ae 100644
--- a/packages/sanity/src/structure/panes/documentList/DocumentListPane.tsx
+++ b/packages/sanity/src/structure/panes/documentList/DocumentListPane.tsx
@@ -6,8 +6,8 @@ import {debounce, map, type Observable, of, tap, timer} from 'rxjs'
 import {
   type GeneralPreviewLayoutKey,
   useI18nText,
-  usePerspective,
   useReleases,
+  useReleasesStack,
   useSchema,
   useTranslation,
   useUnique,
@@ -76,7 +76,7 @@ export const DocumentListPane = memo(function DocumentListPane(props: DocumentLi
   const {childItemId, isActive, pane, paneKey, sortOrder: sortOrderRaw, layout} = props
   const schema = useSchema()
   const releases = useReleases()
-  const {bundlesPerspective} = usePerspective()
+  const stack = useReleasesStack()
   const {displayOptions, options} = pane
   const {apiVersion, filter} = options
   const params = useShallowUnique(options.params || EMPTY_RECORD)
@@ -113,7 +113,7 @@ export const DocumentListPane = memo(function DocumentListPane(props: DocumentLi
   } = useDocumentList({
     apiVersion,
     filter,
-    perspective: bundlesPerspective,
+    perspective: stack,
     params,
     searchQuery: searchQuery?.trim(),
     sortOrder,
diff --git a/packages/sanity/src/structure/panes/documentList/sheetList/useDocumentSheetColumns.tsx b/packages/sanity/src/structure/panes/documentList/sheetList/useDocumentSheetColumns.tsx
index 3f648aa1186..049129ba23d 100644
--- a/packages/sanity/src/structure/panes/documentList/sheetList/useDocumentSheetColumns.tsx
+++ b/packages/sanity/src/structure/panes/documentList/sheetList/useDocumentSheetColumns.tsx
@@ -30,9 +30,9 @@ const PreviewCell = (props: {
   }
 }) => {
   const {documentPreviewStore, row, schemaType} = props
-  const title = 'Document title'
+
   const previewStateObservable = useMemo(
-    () => getPreviewStateObservable(documentPreviewStore, schemaType, row.original._id, title),
+    () => getPreviewStateObservable(documentPreviewStore, schemaType, row.original._id),
     [documentPreviewStore, row.original._id, schemaType],
   )
   const {draft, published, isLoading} = useObservable(previewStateObservable, {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3a0e6b83c69..e9e62b1c40c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1458,6 +1458,9 @@ importers:
       '@sanity/icons':
         specifier: 3.5.0-corel.0
         version: 3.5.0-corel.0(react@18.3.1)
+      '@sanity/id-utils':
+        specifier: ^1.0.0
+        version: 1.0.0
       '@sanity/image-url':
         specifier: ^1.0.2
         version: 1.1.0
@@ -1737,9 +1740,15 @@ importers:
       tar-stream:
         specifier: ^3.1.7
         version: 3.1.7
+      ts-brand:
+        specifier: ^0.2.0
+        version: 0.2.0
       use-device-pixel-ratio:
         specifier: ^1.1.0
         version: 1.1.2(react@18.3.1)
+      use-effect-event:
+        specifier: ^1.0.2
+        version: 1.0.2(react@18.3.1)
       use-hot-module-reload:
         specifier: ^2.0.0
         version: 2.0.0(react@18.3.1)
@@ -4452,6 +4461,10 @@ packages:
     peerDependencies:
       react: ^18.3 || >=19.0.0-rc
 
+  '@sanity/id-utils@1.0.0':
+    resolution: {integrity: sha512-2sb7tbdMDuUuVyocJPKG0gZBiOML/ovCe+mJiLrv1j69ODOfa2LfUjDVR+dRw/A/+XuxoJSSP8ebG7NiwTOgIA==}
+    engines: {node: '>=18'}
+
   '@sanity/image-url@1.1.0':
     resolution: {integrity: sha512-JHumVRxzzaZAJyOimntdukA9TjjzsJiaiq/uUBdTknMLCNvtM6KQ5OCp6W5fIdY78uyFxtQjz+MPXwK8WBIxWg==}
     engines: {node: '>=10.0.0'}
@@ -10949,6 +10962,9 @@ packages:
     peerDependencies:
       typescript: '>=4.2.0'
 
+  ts-brand@0.2.0:
+    resolution: {integrity: sha512-H5uo7OqMvd91D2EefFmltBP9oeNInNzWLAZUSt6coGDn8b814Eis6SnEtzyXORr9ccEb38PfzyiRVDacdkycSQ==}
+
   ts-node@10.9.2:
     resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
     hasBin: true
@@ -14414,6 +14430,12 @@ snapshots:
     dependencies:
       react: 19.0.0-rc-f994737d14-20240522
 
+  '@sanity/id-utils@1.0.0':
+    dependencies:
+      '@sanity/uuid': 3.0.2
+      lodash: 4.17.21
+      ts-brand: 0.2.0
+
   '@sanity/image-url@1.1.0': {}
 
   '@sanity/import@3.37.5':
@@ -22670,6 +22692,8 @@ snapshots:
     dependencies:
       typescript: 5.6.3
 
+  ts-brand@0.2.0: {}
+
   ts-node@10.9.2(@swc/core@1.7.14(@swc/helpers@0.5.13))(@types/node@18.19.44)(typescript@5.6.3):
     dependencies:
       '@cspotcode/source-map-support': 0.8.1