diff --git a/packages/sanity/playwright-ct/tests/formBuilder/utils/TestWrapper.tsx b/packages/sanity/playwright-ct/tests/formBuilder/utils/TestWrapper.tsx
index 69f98a55a1c..f0508b4893b 100644
--- a/packages/sanity/playwright-ct/tests/formBuilder/utils/TestWrapper.tsx
+++ b/packages/sanity/playwright-ct/tests/formBuilder/utils/TestWrapper.tsx
@@ -1,6 +1,7 @@
 import {type SanityClient} from '@sanity/client'
 import {Card, LayerProvider, ThemeProvider, ToastProvider} from '@sanity/ui'
 import {buildTheme, type RootTheme} from '@sanity/ui/theme'
+import {noop} from 'lodash'
 import {type ReactNode, Suspense, useEffect, useState} from 'react'
 import {
   ChangeConnectorRoot,
@@ -18,6 +19,8 @@ import {
 import {Pane, PaneContent, PaneLayout} from 'sanity/structure'
 import {styled} from 'styled-components'
 
+import {route} from '../../../../src/router'
+import {RouterProvider} from '../../../../src/router/RouterProvider'
 import {createMockSanityClient} from '../../../../test/mocks/mockSanityClient'
 import {getMockWorkspace} from '../../../../test/testUtils/getMockWorkspaceFromConfig'
 
@@ -36,6 +39,8 @@ const StyledChangeConnectorRoot = styled(ChangeConnectorRoot)`
   min-width: 0;
 `
 
+const router = route.create('/')
+
 /**
  * @description This component is used to wrap all tests in the providers it needs to be able to run successfully.
  * It provides a mock Sanity client and a mock workspace.
@@ -72,37 +77,39 @@ export const TestWrapper = (props: TestWrapperProps): JSX.Element | null => {
 
   return (
     <Suspense fallback={null}>
-      <ThemeProvider theme={studioThemeConfig}>
-        <ToastProvider>
-          <LayerProvider>
-            <WorkspaceProvider workspace={mockWorkspace}>
-              <ResourceCacheProvider>
-                <SourceProvider source={mockWorkspace.unstable_sources[0]}>
-                  <CopyPasteProvider>
-                    <ColorSchemeProvider>
-                      <UserColorManagerProvider>
-                        <StyledChangeConnectorRoot
-                          isReviewChangesOpen={false}
-                          onOpenReviewChanges={() => {}}
-                          onSetFocus={() => {}}
-                        >
-                          <PaneLayout height="fill">
-                            <Pane id="test-pane">
-                              <PaneContent>
-                                <Card padding={3}>{children}</Card>
-                              </PaneContent>
-                            </Pane>
-                          </PaneLayout>
-                        </StyledChangeConnectorRoot>
-                      </UserColorManagerProvider>
-                    </ColorSchemeProvider>
-                  </CopyPasteProvider>
-                </SourceProvider>
-              </ResourceCacheProvider>
-            </WorkspaceProvider>
-          </LayerProvider>
-        </ToastProvider>
-      </ThemeProvider>
+      <RouterProvider router={router} state={{}} onNavigate={noop}>
+        <ThemeProvider theme={studioThemeConfig}>
+          <ToastProvider>
+            <LayerProvider>
+              <WorkspaceProvider workspace={mockWorkspace}>
+                <ResourceCacheProvider>
+                  <SourceProvider source={mockWorkspace.unstable_sources[0]}>
+                    <CopyPasteProvider>
+                      <ColorSchemeProvider>
+                        <UserColorManagerProvider>
+                          <StyledChangeConnectorRoot
+                            isReviewChangesOpen={false}
+                            onOpenReviewChanges={() => {}}
+                            onSetFocus={() => {}}
+                          >
+                            <PaneLayout height="fill">
+                              <Pane id="test-pane">
+                                <PaneContent>
+                                  <Card padding={3}>{children}</Card>
+                                </PaneContent>
+                              </Pane>
+                            </PaneLayout>
+                          </StyledChangeConnectorRoot>
+                        </UserColorManagerProvider>
+                      </ColorSchemeProvider>
+                    </CopyPasteProvider>
+                  </SourceProvider>
+                </ResourceCacheProvider>
+              </WorkspaceProvider>
+            </LayerProvider>
+          </ToastProvider>
+        </ThemeProvider>
+      </RouterProvider>
     </Suspense>
   )
 }
diff --git a/packages/sanity/src/core/bundles/components/BundleMenu.tsx b/packages/sanity/src/core/bundles/components/BundleMenu.tsx
index a7e888eefed..c8735d35974 100644
--- a/packages/sanity/src/core/bundles/components/BundleMenu.tsx
+++ b/packages/sanity/src/core/bundles/components/BundleMenu.tsx
@@ -4,7 +4,7 @@ import {type ReactElement, useCallback} from 'react'
 import {styled} from 'styled-components'
 
 import {type BundleDocument} from '../../store/bundles/types'
-import {usePerspective} from '../hooks/usePerspective'
+import {usePerspective} from '../hooks'
 import {LATEST} from '../util/const'
 import {isDraftOrPublished} from '../util/util'
 import {BundleBadge} from './BundleBadge'
@@ -23,14 +23,15 @@ interface BundleListProps {
   bundles: BundleDocument[] | null
   loading: boolean
   actions?: ReactElement
+  perspective?: string
 }
 
 /**
  * @internal
  */
 export function BundleMenu(props: BundleListProps): JSX.Element {
-  const {bundles, loading, actions, button} = props
-  const {currentGlobalBundle, setPerspective} = usePerspective()
+  const {bundles, loading, actions, button, perspective} = props
+  const {currentGlobalBundle, setPerspective} = usePerspective(perspective)
 
   const bundlesToDisplay =
     bundles?.filter((bundle) => !isDraftOrPublished(bundle.slug) && !bundle.archivedAt) || []
diff --git a/packages/sanity/src/core/bundles/components/panes/BundleActions.tsx b/packages/sanity/src/core/bundles/components/panes/BundleActions.tsx
index a8f641eff10..d600557694e 100644
--- a/packages/sanity/src/core/bundles/components/panes/BundleActions.tsx
+++ b/packages/sanity/src/core/bundles/components/panes/BundleActions.tsx
@@ -13,21 +13,21 @@ interface BundleActionsProps {
   documentId: string
   documentType: string
   documentVersions: BundleDocument[] | null
+  bundleSlug?: string
 }
 
 /**
  * @internal
  */
 export function BundleActions(props: BundleActionsProps): ReactNode {
-  const {currentGlobalBundle, documentId, documentType, documentVersions} = props
+  const {currentGlobalBundle, documentId, documentType, documentVersions, bundleSlug} = props
   const {slug, title, archivedAt} = currentGlobalBundle
   const documentStore = useDocumentStore()
-
   const [creatingVersion, setCreatingVersion] = useState<boolean>(false)
   const [isInVersion, setIsInVersion] = useState<boolean>(false)
 
   const toast = useToast()
-  const {newVersion} = useDocumentOperation(documentId, documentType)
+  const {newVersion} = useDocumentOperation(documentId, documentType, bundleSlug)
   const {t} = useTranslation()
 
   useEffect(() => {
diff --git a/packages/sanity/src/core/bundles/hooks/usePerspective.tsx b/packages/sanity/src/core/bundles/hooks/usePerspective.tsx
index 0b812a14689..b8b0733da99 100644
--- a/packages/sanity/src/core/bundles/hooks/usePerspective.tsx
+++ b/packages/sanity/src/core/bundles/hooks/usePerspective.tsx
@@ -15,12 +15,16 @@ export interface PerspectiveValue {
 }
 
 /**
+ * TODO: Improve distinction between global and pane perspectives.
+ *
  * @internal
  */
-export function usePerspective(): PerspectiveValue {
+export function usePerspective(selectedPerspective?: string): PerspectiveValue {
   const router = useRouter()
   const {data: bundles} = useBundles()
+  const perspective = selectedPerspective ?? router.stickyParams.perspective
 
+  // TODO: Should it be possible to set the perspective within a pane, rather than globally?
   const setPerspective = (slug: string | undefined) => {
     if (slug === 'drafts') {
       router.navigateStickyParam('perspective', '')
@@ -28,16 +32,15 @@ export function usePerspective(): PerspectiveValue {
       router.navigateStickyParam('perspective', `bundle.${slug}`)
     }
   }
+
   const selectedBundle =
-    router.stickyParams?.perspective && bundles
+    perspective && bundles
       ? bundles.find((bundle: Partial<BundleDocument>) => {
-          return (
-            `bundle.${bundle.slug}`.toLocaleLowerCase() ===
-            router.stickyParams.perspective?.toLocaleLowerCase()
-          )
+          return `bundle.${bundle.slug}`.toLocaleLowerCase() === perspective?.toLocaleLowerCase()
         })
       : LATEST
 
+  // TODO: Improve naming; this may not be global.
   const currentGlobalBundle = selectedBundle || LATEST
 
   return {
diff --git a/packages/sanity/src/core/comments/context/comments/CommentsProvider.tsx b/packages/sanity/src/core/comments/context/comments/CommentsProvider.tsx
index e15c5a4eb0d..901a04cf50e 100644
--- a/packages/sanity/src/core/comments/context/comments/CommentsProvider.tsx
+++ b/packages/sanity/src/core/comments/context/comments/CommentsProvider.tsx
@@ -43,6 +43,7 @@ export interface CommentsProviderProps {
   children: ReactNode
   documentId: string
   documentType: string
+  perspective?: string
   type: CommentsType
   sortOrder: 'asc' | 'desc'
 
@@ -78,20 +79,27 @@ export const CommentsProvider = memo(function CommentsProvider(props: CommentsPr
     selectedCommentId,
     isConnecting,
     onPathOpen,
+    perspective,
   } = props
   const commentsEnabled = useCommentsEnabled()
   const [status, setStatus] = useState<CommentStatus>('open')
   const {client, createAddonDataset, isCreatingDataset} = useAddonDataset()
   const publishedId = getPublishedId(documentId)
-  const editState = useEditState(publishedId, documentType, 'low')
+
+  const bundlePerspective = perspective?.startsWith('bundle.')
+    ? perspective.split('bundle.').at(1)
+    : undefined
+
+  // TODO: Allow versions to have separate comments.
+  const editState = useEditState(publishedId, documentType, 'default', bundlePerspective)
   const schemaType = useSchema().get(documentType)
   const currentUser = useCurrentUser()
 
   const {name: workspaceName, dataset, projectId} = useWorkspace()
 
   const documentValue = useMemo(() => {
-    return editState.draft || editState.published
-  }, [editState.draft, editState.published])
+    return editState.version || editState.draft || editState.published
+  }, [editState.version, editState.draft, editState.published])
 
   const documentRevisionId = useMemo(() => documentValue?._rev, [documentValue])
 
diff --git a/packages/sanity/src/core/components/documentStatus/DocumentStatus.tsx b/packages/sanity/src/core/components/documentStatus/DocumentStatus.tsx
index 16a5190dbf7..6faa08d66fb 100644
--- a/packages/sanity/src/core/components/documentStatus/DocumentStatus.tsx
+++ b/packages/sanity/src/core/components/documentStatus/DocumentStatus.tsx
@@ -9,6 +9,7 @@ interface DocumentStatusProps {
   absoluteDate?: boolean
   draft?: PreviewValue | Partial<SanityDocument> | null
   published?: PreviewValue | Partial<SanityDocument> | null
+  version?: PreviewValue | Partial<SanityDocument> | null
   singleLine?: boolean
 }
 
@@ -26,9 +27,16 @@ const StyledText = styled(Text)`
  *
  * @internal
  */
-export function DocumentStatus({absoluteDate, draft, published, singleLine}: DocumentStatusProps) {
+export function DocumentStatus({
+  absoluteDate,
+  draft,
+  published,
+  version,
+  singleLine,
+}: DocumentStatusProps) {
   const {t} = useTranslation()
   const draftUpdatedAt = draft && '_updatedAt' in draft ? draft._updatedAt : ''
+  const versionUpdatedAt = version && '_updatedAt' in version ? version._updatedAt : ''
   const publishedUpdatedAt = published && '_updatedAt' in published ? published._updatedAt : ''
 
   const intlDateFormat = useDateTimeFormat({
@@ -39,6 +47,7 @@ export function DocumentStatus({absoluteDate, draft, published, singleLine}: Doc
   const draftDateAbsolute = draftUpdatedAt && intlDateFormat.format(new Date(draftUpdatedAt))
   const publishedDateAbsolute =
     publishedUpdatedAt && intlDateFormat.format(new Date(publishedUpdatedAt))
+  const versionDateAbsolute = versionUpdatedAt && intlDateFormat.format(new Date(versionUpdatedAt))
 
   const draftUpdatedTimeAgo = useRelativeTime(draftUpdatedAt || '', {
     minimal: true,
@@ -48,9 +57,15 @@ export function DocumentStatus({absoluteDate, draft, published, singleLine}: Doc
     minimal: true,
     useTemporalPhrase: true,
   })
+  const versionUpdatedTimeAgo = useRelativeTime(versionUpdatedAt || '', {
+    minimal: true,
+    useTemporalPhrase: true,
+  })
 
   const publishedDate = absoluteDate ? publishedDateAbsolute : publishedUpdatedTimeAgo
-  const updatedDate = absoluteDate ? draftDateAbsolute : draftUpdatedTimeAgo
+  const updatedDate = absoluteDate
+    ? versionDateAbsolute || draftDateAbsolute
+    : versionUpdatedTimeAgo || draftUpdatedTimeAgo
 
   return (
     <Flex
@@ -60,12 +75,12 @@ export function DocumentStatus({absoluteDate, draft, published, singleLine}: Doc
       gap={2}
       wrap="nowrap"
     >
-      {!publishedDate && (
+      {!version && !publishedDate && (
         <StyledText size={1} weight="medium">
           {t('document-status.not-published')}
         </StyledText>
       )}
-      {publishedDate && (
+      {!version && publishedDate && (
         <StyledText size={1} weight="medium">
           {t('document-status.published', {date: publishedDate})}
         </StyledText>
diff --git a/packages/sanity/src/core/components/documentStatusIndicator/DocumentStatusIndicator.tsx b/packages/sanity/src/core/components/documentStatusIndicator/DocumentStatusIndicator.tsx
index 892f2cc0e57..475844e6364 100644
--- a/packages/sanity/src/core/components/documentStatusIndicator/DocumentStatusIndicator.tsx
+++ b/packages/sanity/src/core/components/documentStatusIndicator/DocumentStatusIndicator.tsx
@@ -7,6 +7,7 @@ import {styled} from 'styled-components'
 interface DocumentStatusProps {
   draft?: PreviewValue | Partial<SanityDocument> | null
   published?: PreviewValue | Partial<SanityDocument> | null
+  version?: PreviewValue | Partial<SanityDocument> | null
 }
 
 const Root = styled(Text)`
@@ -25,23 +26,26 @@ const Root = styled(Text)`
  * - Yellow (caution) for published documents with edits
  * - Gray (default) for unpublished documents (with or without edits)
  *
- * No dot will be displayed for published documents without edits.
+ * No dot will be displayed for published documents without edits or for version documents.
  *
  * @internal
  */
-export function DocumentStatusIndicator({draft, published}: DocumentStatusProps) {
+export function DocumentStatusIndicator({draft, published, version}: DocumentStatusProps) {
   const $draft = Boolean(draft)
   const $published = Boolean(published)
+  const $version = Boolean(version)
 
   const status = useMemo(() => {
+    if ($version) return undefined
     if ($draft && !$published) return 'unpublished'
     return 'edited'
-  }, [$draft, $published])
+  }, [$draft, $published, $version])
 
   // Return null if the document is:
   // - Published without edits
   // - Neither published or without edits (this shouldn't be possible)
-  if ((!$draft && !$published) || (!$draft && $published)) {
+  // - A version
+  if ((!$draft && !$published) || (!$draft && $published) || $version) {
     return null
   }
 
diff --git a/packages/sanity/src/core/form/FormBuilderContext.ts b/packages/sanity/src/core/form/FormBuilderContext.ts
index be5b2f561f6..22c4b6358b0 100644
--- a/packages/sanity/src/core/form/FormBuilderContext.ts
+++ b/packages/sanity/src/core/form/FormBuilderContext.ts
@@ -58,4 +58,5 @@ export interface FormBuilderContextValue {
   renderItem: RenderItemCallback
   renderPreview: RenderPreviewCallback
   schemaType: ObjectSchemaType
+  version?: string
 }
diff --git a/packages/sanity/src/core/form/FormBuilderProvider.tsx b/packages/sanity/src/core/form/FormBuilderProvider.tsx
index b14969c90a0..50d7fefa30a 100644
--- a/packages/sanity/src/core/form/FormBuilderProvider.tsx
+++ b/packages/sanity/src/core/form/FormBuilderProvider.tsx
@@ -65,6 +65,7 @@ export interface FormBuilderProviderProps {
   schemaType: ObjectSchemaType
   unstable?: Source['form']['unstable']
   validation: ValidationMarker[]
+  version?: string
 }
 
 const missingPatchChannel: PatchChannel = {
@@ -113,6 +114,7 @@ export function FormBuilderProvider(props: FormBuilderProviderProps) {
     schemaType,
     unstable,
     validation,
+    version,
   } = props
 
   const __internal: FormBuilderContextValue['__internal'] = useMemo(
@@ -171,6 +173,7 @@ export function FormBuilderProvider(props: FormBuilderProviderProps) {
       renderItem,
       renderPreview,
       schemaType,
+      version,
     }),
     [
       __internal,
@@ -191,6 +194,7 @@ export function FormBuilderProvider(props: FormBuilderProviderProps) {
       renderItem,
       renderPreview,
       schemaType,
+      version,
     ],
   )
 
diff --git a/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceField.tsx b/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceField.tsx
index 0fb43121c38..3c11fbc40a0 100644
--- a/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceField.tsx
+++ b/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceField.tsx
@@ -24,6 +24,7 @@ import {useDidUpdate} from '../../hooks/useDidUpdate'
 import {useScrollIntoViewOnFocusWithin} from '../../hooks/useScrollIntoViewOnFocusWithin'
 import {set, unset} from '../../patch'
 import {type ObjectFieldProps, type RenderPreviewCallback} from '../../types'
+import {useFormBuilder} from '../../useFormBuilder'
 import {PreviewReferenceValue} from './PreviewReferenceValue'
 import {ReferenceFinalizeAlertStrip} from './ReferenceFinalizeAlertStrip'
 import {ReferenceLinkCard} from './ReferenceLinkCard'
@@ -60,6 +61,7 @@ export function ReferenceField(props: ReferenceFieldProps) {
   const elementRef = useRef<HTMLDivElement | null>(null)
   const {schemaType, path, open, inputId, children, inputProps} = props
   const {readOnly, focused, renderPreview, onChange} = props.inputProps
+  const {version} = useFormBuilder()
 
   const [fieldActionsNodes, setFieldActionNodes] = useState<DocumentFieldActionNode[]>([])
   const documentId = usePublishedId()
@@ -72,6 +74,7 @@ export function ReferenceField(props: ReferenceFieldProps) {
       path,
       schemaType,
       value,
+      version,
     })
 
   // this is here to make sure the item is visible if it's being edited behind a modal
diff --git a/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceInput.tsx b/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceInput.tsx
index 2703be12ed6..a254fd43fc2 100644
--- a/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceInput.tsx
+++ b/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceInput.tsx
@@ -52,6 +52,7 @@ export function ReferenceInput(props: ReferenceInputProps) {
     id,
     onPathFocus,
     value,
+    version,
     renderPreview,
     path,
     elementProps,
@@ -62,6 +63,7 @@ export function ReferenceInput(props: ReferenceInputProps) {
     path,
     schemaType,
     value,
+    version,
   })
 
   const [searchState, setSearchState] = useState<ReferenceSearchState>(INITIAL_SEARCH_STATE)
@@ -187,6 +189,7 @@ export function ReferenceInput(props: ReferenceInputProps) {
 
   const renderOption = useCallback(
     (option: AutocompleteOption) => {
+      // TODO: Account for checked-out version.
       const documentId = option.hit.draft?._id || option.hit.published?._id || option.value
 
       return (
@@ -205,6 +208,7 @@ export function ReferenceInput(props: ReferenceInputProps) {
 
   const renderValue = useCallback(() => {
     return (
+      loadableReferenceInfo.result?.preview.version?.title ||
       loadableReferenceInfo.result?.preview.draft?.title ||
       loadableReferenceInfo.result?.preview.published?.title ||
       ''
@@ -212,6 +216,7 @@ export function ReferenceInput(props: ReferenceInputProps) {
   }, [
     loadableReferenceInfo.result?.preview.draft?.title,
     loadableReferenceInfo.result?.preview.published?.title,
+    loadableReferenceInfo.result?.preview.version?.title,
   ])
 
   const handleFocus = useCallback(() => onPathFocus(['_ref']), [onPathFocus])
diff --git a/packages/sanity/src/core/form/inputs/ReferenceInput/ReferencePreview.tsx b/packages/sanity/src/core/form/inputs/ReferenceInput/ReferencePreview.tsx
index 9371f6275a4..c12802a7b2a 100644
--- a/packages/sanity/src/core/form/inputs/ReferenceInput/ReferencePreview.tsx
+++ b/packages/sanity/src/core/form/inputs/ReferenceInput/ReferencePreview.tsx
@@ -27,6 +27,7 @@ export function ReferencePreview(props: {
   const documentPresence = useDocumentPresence(id)
 
   const previewId =
+    preview.version?._id ||
     preview.draft?._id ||
     preview.published?._id ||
     // note: during publish of the referenced document we might have both a missing draft and a missing published version
@@ -44,7 +45,7 @@ export function ReferencePreview(props: {
     [previewId, refType.name],
   )
 
-  const {draft, published} = preview
+  const {draft, published, version} = preview
 
   const previewProps = useMemo(
     () => ({
@@ -57,13 +58,17 @@ export function ReferencePreview(props: {
               <DocumentPreviewPresence presence={documentPresence} />
             )}
 
-            <DocumentStatusIndicator draft={preview.draft} published={preview.published} />
+            <DocumentStatusIndicator
+              draft={preview.draft}
+              published={preview.published}
+              version={preview.version}
+            />
           </Inline>
         </Box>
       ),
       layout,
       schemaType: refType,
-      tooltip: <DocumentStatus draft={draft} published={published} />,
+      tooltip: <DocumentStatus draft={draft} published={published} version={version} />,
       value: previewStub,
     }),
     [
@@ -72,10 +77,12 @@ export function ReferencePreview(props: {
       layout,
       preview.draft,
       preview.published,
+      preview.version,
       previewStub,
       published,
       refType,
       showTypeLabel,
+      version,
     ],
   )
 
diff --git a/packages/sanity/src/core/form/inputs/ReferenceInput/types.ts b/packages/sanity/src/core/form/inputs/ReferenceInput/types.ts
index 933afbdb68d..4b8ffe6148f 100644
--- a/packages/sanity/src/core/form/inputs/ReferenceInput/types.ts
+++ b/packages/sanity/src/core/form/inputs/ReferenceInput/types.ts
@@ -18,6 +18,7 @@ export interface ReferenceInfo {
   preview: {
     draft: (PreviewValue & {_id: string; _createdAt?: string; _updatedAt?: string}) | undefined
     published: (PreviewValue & {_id: string; _createdAt?: string; _updatedAt?: string}) | undefined
+    version: (PreviewValue & {_id: string; _createdAt?: string; _updatedAt?: string}) | undefined
   }
 }
 
@@ -76,4 +77,5 @@ export interface ReferenceInputProps<Value = Reference>
 
   onEditReference: (event: EditReferenceEvent) => void
   getReferenceInfo: (id: string, type: ReferenceSchemaType) => Observable<ReferenceInfo>
+  version?: string
 }
diff --git a/packages/sanity/src/core/form/inputs/ReferenceInput/useReferenceInput.tsx b/packages/sanity/src/core/form/inputs/ReferenceInput/useReferenceInput.tsx
index 5e56965952f..4b9a99d9872 100644
--- a/packages/sanity/src/core/form/inputs/ReferenceInput/useReferenceInput.tsx
+++ b/packages/sanity/src/core/form/inputs/ReferenceInput/useReferenceInput.tsx
@@ -28,10 +28,11 @@ interface Options {
   path: Path
   schemaType: ReferenceSchemaType
   value?: Reference
+  version?: string
 }
 
 export function useReferenceInput(options: Options) {
-  const {path, schemaType} = options
+  const {path, schemaType, version} = options
   const schema = useSchema()
   const documentPreviewStore = useDocumentPreviewStore()
   const {EditReferenceLinkComponent, onEditReference, activePath, initialValueTemplateItems} =
@@ -113,8 +114,8 @@ export function useReferenceInput(options: Options) {
   }, [disableNew, initialValueTemplateItems, schemaType.to])
 
   const getReferenceInfo = useCallback(
-    (id: string) => adapter.getReferenceInfo(documentPreviewStore, id, schemaType),
-    [documentPreviewStore, schemaType],
+    (id: string) => adapter.getReferenceInfo(documentPreviewStore, id, schemaType, {version}),
+    [documentPreviewStore, schemaType, version],
   )
 
   return {
diff --git a/packages/sanity/src/core/form/studio/FormBuilder.tsx b/packages/sanity/src/core/form/studio/FormBuilder.tsx
index 98a744aefbc..1d60080dbe2 100644
--- a/packages/sanity/src/core/form/studio/FormBuilder.tsx
+++ b/packages/sanity/src/core/form/studio/FormBuilder.tsx
@@ -69,6 +69,7 @@ export interface FormBuilderProps
   schemaType: ObjectSchemaType
   validation: ValidationMarker[]
   value: FormDocumentValue | undefined
+  version?: string
 }
 
 /**
@@ -100,6 +101,7 @@ export function FormBuilder(props: FormBuilderProps) {
     schemaType,
     validation,
     value,
+    version,
   } = props
 
   const handleCollapseField = useCallback(
@@ -278,6 +280,7 @@ export function FormBuilder(props: FormBuilderProps) {
       validation={validation}
       readOnly={readOnly}
       schemaType={schemaType}
+      version={version}
     >
       <GetFormValueProvider value={value}>
         <FormValueProvider value={value}>
diff --git a/packages/sanity/src/core/form/studio/FormProvider.tsx b/packages/sanity/src/core/form/studio/FormProvider.tsx
index 5e1fbc73a29..e43c957105c 100644
--- a/packages/sanity/src/core/form/studio/FormProvider.tsx
+++ b/packages/sanity/src/core/form/studio/FormProvider.tsx
@@ -55,6 +55,7 @@ export interface FormProviderProps {
   readOnly?: boolean
   schemaType: ObjectSchemaType
   validation: ValidationMarker[]
+  version?: string
 }
 
 /**
@@ -86,6 +87,7 @@ export function FormProvider(props: FormProviderProps) {
     readOnly,
     schemaType,
     validation,
+    version,
   } = props
 
   const {file, image} = useSource().form
@@ -164,6 +166,7 @@ export function FormProvider(props: FormProviderProps) {
       renderPreview={renderPreview}
       schemaType={schemaType}
       validation={validation}
+      version={version}
     >
       {children}
     </FormBuilderProvider>
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 23eb962a061..ca90d3bcfb2 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
@@ -31,22 +31,30 @@ export function getReferenceInfo(
   documentPreviewStore: DocumentPreviewStore,
   id: string,
   referenceType: ReferenceSchemaType,
+  {version}: {version?: string} = {},
 ): Observable<ReferenceInfo> {
-  const {publishedId, draftId} = getIdPair(id)
+  const {publishedId, draftId, versionId} = getIdPair(id, {version})
 
-  const pairAvailability$ = documentPreviewStore.unstable_observeDocumentPairAvailability(id)
+  const pairAvailability$ = documentPreviewStore.unstable_observeDocumentPairAvailability(id, {
+    version,
+  })
 
   return pairAvailability$.pipe(
     switchMap((pairAvailability) => {
-      if (!pairAvailability.draft.available && !pairAvailability.published.available) {
-        // combine availability of draft + published
+      if (
+        !pairAvailability.version?.available &&
+        !pairAvailability.draft.available &&
+        !pairAvailability.published.available
+      ) {
+        // combine availability of draft + published + version
         const availability =
+          pairAvailability.version?.reason === 'PERMISSION_DENIED' ||
           pairAvailability.draft.reason === 'PERMISSION_DENIED' ||
           pairAvailability.published.reason === 'PERMISSION_DENIED'
             ? PERMISSION_DENIED
             : NOT_FOUND
 
-        // short circuit, neither draft nor published is available so no point in trying to get preview
+        // short circuit, neither draft nor published nor version is available so no point in trying to get preview
         return of({
           id,
           type: undefined,
@@ -54,19 +62,25 @@ export function getReferenceInfo(
           preview: {
             draft: undefined,
             published: undefined,
+            version: undefined,
           },
         } as const)
       }
 
       const draftRef = {_type: 'reference', _ref: draftId}
       const publishedRef = {_type: 'reference', _ref: publishedId}
+      const versionRef = versionId ? {_type: 'reference', _ref: versionId} : undefined
 
       const typeName$ = combineLatest([
         documentPreviewStore.observeDocumentTypeFromId(draftId),
         documentPreviewStore.observeDocumentTypeFromId(publishedId),
+        ...(versionId ? [documentPreviewStore.observeDocumentTypeFromId(versionId)] : []),
       ]).pipe(
-        // assume draft + published are always same type
-        map(([draftTypeName, publishedTypeName]) => draftTypeName || publishedTypeName),
+        // assume draft + published + version are always same type
+        map(
+          ([draftTypeName, publishedTypeName, versionTypeName]) =>
+            versionTypeName || draftTypeName || publishedTypeName,
+        ),
       )
 
       return typeName$.pipe(
@@ -83,6 +97,7 @@ export function getReferenceInfo(
               preview: {
                 draft: undefined,
                 published: undefined,
+                version: undefined,
               },
             } as const)
           }
@@ -98,6 +113,7 @@ export function getReferenceInfo(
               preview: {
                 draft: undefined,
                 published: undefined,
+                version: undefined,
               },
             } as const)
           }
@@ -130,21 +146,46 @@ export function getReferenceInfo(
               startWith(undefined),
             )
 
-          const value$ = combineLatest([draftPreview$, publishedPreview$]).pipe(
-            map(([draft, published]) => ({draft, published})),
+          const versionPreview$ =
+            versionId && versionRef
+              ? documentPreviewStore.observePaths(versionRef, previewPaths).pipe(
+                  map((result) =>
+                    result
+                      ? {
+                          _id: versionId,
+                          ...prepareForPreview(result, refSchemaType),
+                        }
+                      : undefined,
+                  ),
+                  startWith(undefined),
+                )
+              : undefined
+
+          const value$ = combineLatest([
+            draftPreview$,
+            publishedPreview$,
+            ...(versionPreview$ ? [versionPreview$] : []),
+          ]).pipe(
+            map(([draft, published, versionValue]) => ({
+              draft,
+              published,
+              ...(versionValue ? {version: versionValue} : {}),
+            })),
           )
 
           return value$.pipe(
             map((value): ReferenceInfo => {
               const availability =
                 // eslint-disable-next-line no-nested-ternary
-                pairAvailability.draft.available || pairAvailability.published.available
+                pairAvailability.version?.available ||
+                pairAvailability.draft.available ||
+                pairAvailability.published.available
                   ? READABLE
-                  : pairAvailability.draft.reason === 'PERMISSION_DENIED' ||
+                  : pairAvailability.version?.reason === 'PERMISSION_DENIED' ||
+                      pairAvailability.draft.reason === 'PERMISSION_DENIED' ||
                       pairAvailability.published.reason === 'PERMISSION_DENIED'
                     ? PERMISSION_DENIED
                     : NOT_FOUND
-
               return {
                 type: typeName,
                 id: publishedId,
@@ -152,6 +193,7 @@ export function getReferenceInfo(
                 preview: {
                   draft: isRecord(value.draft) ? value.draft : undefined,
                   published: isRecord(value.published) ? value.published : undefined,
+                  version: isRecord(value.version) ? value.version : undefined,
                 },
               }
             }),
diff --git a/packages/sanity/src/core/form/studio/inputs/reference/StudioReferenceInput.tsx b/packages/sanity/src/core/form/studio/inputs/reference/StudioReferenceInput.tsx
index ce26ad5c64f..5b65e387510 100644
--- a/packages/sanity/src/core/form/studio/inputs/reference/StudioReferenceInput.tsx
+++ b/packages/sanity/src/core/form/studio/inputs/reference/StudioReferenceInput.tsx
@@ -25,6 +25,7 @@ import {
   type EditReferenceEvent,
 } from '../../../inputs/ReferenceInput/types'
 import {type ObjectInputProps} from '../../../types'
+import {useFormBuilder} from '../../../useFormBuilder'
 import {useReferenceInputOptions} from '../../contexts'
 import * as adapter from '../client-adapters/reference'
 import {resolveUserDefinedFilter} from './resolveUserDefinedFilter'
@@ -61,6 +62,7 @@ export function StudioReferenceInput(props: StudioReferenceInputProps) {
   const schema = useSchema()
   const maxFieldDepth = useSearchMaxFieldDepth()
   const documentPreviewStore = useDocumentPreviewStore()
+  const {version} = useFormBuilder()
   const {path, schemaType} = props
   const {EditReferenceLinkComponent, onEditReference, activePath, initialValueTemplateItems} =
     useReferenceInputOptions()
@@ -192,6 +194,7 @@ export function StudioReferenceInput(props: StudioReferenceInputProps) {
       editReferenceLinkComponent={EditReferenceLink}
       createOptions={createOptions}
       onEditReference={handleEditReference}
+      version={version}
     />
   )
 }
diff --git a/packages/sanity/src/core/form/types/fieldProps.ts b/packages/sanity/src/core/form/types/fieldProps.ts
index e530df7bc60..c9095a2141b 100644
--- a/packages/sanity/src/core/form/types/fieldProps.ts
+++ b/packages/sanity/src/core/form/types/fieldProps.ts
@@ -61,6 +61,7 @@ export interface BaseFieldProps {
   changed: boolean
   children: ReactNode
   renderDefault: (props: FieldProps) => ReactElement
+  version?: string
 }
 
 /**
diff --git a/packages/sanity/src/core/hooks/useConnectionState.ts b/packages/sanity/src/core/hooks/useConnectionState.ts
index 2405544535b..b23971a9104 100644
--- a/packages/sanity/src/core/hooks/useConnectionState.ts
+++ b/packages/sanity/src/core/hooks/useConnectionState.ts
@@ -11,12 +11,16 @@ export type ConnectionState = 'connecting' | 'reconnecting' | 'connected'
 const INITIAL: ConnectionState = 'connecting'
 
 /** @internal */
-export function useConnectionState(publishedDocId: string, docTypeName: string): ConnectionState {
+export function useConnectionState(
+  publishedDocId: string,
+  docTypeName: string,
+  {version}: {version?: string} = {},
+): ConnectionState {
   const documentStore = useDocumentStore()
 
   const observable = useMemo(
     () =>
-      documentStore.pair.documentEvents(publishedDocId, docTypeName).pipe(
+      documentStore.pair.documentEvents(publishedDocId, docTypeName, version).pipe(
         map((ev: {type: string}) => ev.type),
         map((eventType) => eventType !== 'reconnect'),
         switchMap((isConnected) =>
@@ -25,7 +29,7 @@ export function useConnectionState(publishedDocId: string, docTypeName: string):
         startWith(INITIAL as any),
         distinctUntilChanged(),
       ),
-    [docTypeName, documentStore.pair, publishedDocId],
+    [docTypeName, documentStore.pair, publishedDocId, version],
   )
   return useObservable(observable, INITIAL)
 }
diff --git a/packages/sanity/src/core/hooks/useDocumentOperation.ts b/packages/sanity/src/core/hooks/useDocumentOperation.ts
index 4e67302fd06..105de51f1b3 100644
--- a/packages/sanity/src/core/hooks/useDocumentOperation.ts
+++ b/packages/sanity/src/core/hooks/useDocumentOperation.ts
@@ -4,11 +4,15 @@ import {useObservable} from 'react-rx'
 import {type OperationsAPI, useDocumentStore} from '../store'
 
 /** @internal */
-export function useDocumentOperation(publishedDocId: string, docTypeName: string): OperationsAPI {
+export function useDocumentOperation(
+  publishedDocId: string,
+  docTypeName: string,
+  version?: string,
+): OperationsAPI {
   const documentStore = useDocumentStore()
   const observable = useMemo(
-    () => documentStore.pair.editOperations(publishedDocId, docTypeName),
-    [docTypeName, documentStore.pair, publishedDocId],
+    () => documentStore.pair.editOperations(publishedDocId, docTypeName, version),
+    [docTypeName, documentStore.pair, publishedDocId, version],
   )
   /**
    * We know that since the observable has a startWith operator, it will always emit a value
diff --git a/packages/sanity/src/core/hooks/useEditState.ts b/packages/sanity/src/core/hooks/useEditState.ts
index 6d2650b73cf..771f973f07e 100644
--- a/packages/sanity/src/core/hooks/useEditState.ts
+++ b/packages/sanity/src/core/hooks/useEditState.ts
@@ -9,12 +9,13 @@ export function useEditState(
   publishedDocId: string,
   docTypeName: string,
   priority: 'default' | 'low' = 'default',
+  version?: string,
 ): EditStateFor {
   const documentStore = useDocumentStore()
 
   const observable = useMemo(() => {
     if (priority === 'low') {
-      const base = documentStore.pair.editState(publishedDocId, docTypeName).pipe(share())
+      const base = documentStore.pair.editState(publishedDocId, docTypeName, version).pipe(share())
 
       return merge(
         base.pipe(take(1)),
@@ -25,8 +26,8 @@ export function useEditState(
       )
     }
 
-    return documentStore.pair.editState(publishedDocId, docTypeName)
-  }, [docTypeName, documentStore.pair, priority, publishedDocId])
+    return documentStore.pair.editState(publishedDocId, docTypeName, version)
+  }, [docTypeName, documentStore.pair, priority, publishedDocId, version])
   /**
    * We know that since the observable has a startWith operator, it will always emit a value
    * and that's why the non-null assertion is used here
diff --git a/packages/sanity/src/core/hooks/useSyncState.ts b/packages/sanity/src/core/hooks/useSyncState.ts
index 385d3205a25..65888a19558 100644
--- a/packages/sanity/src/core/hooks/useSyncState.ts
+++ b/packages/sanity/src/core/hooks/useSyncState.ts
@@ -14,15 +14,19 @@ const SYNCING = {isSyncing: true}
 const NOT_SYNCING = {isSyncing: false}
 
 /** @internal */
-export function useSyncState(publishedDocId: string, documentType: string): SyncState {
+export function useSyncState(
+  publishedDocId: string,
+  documentType: string,
+  {version}: {version?: string} = {},
+): SyncState {
   const documentStore = useDocumentStore()
 
   const observable = useMemo(
     () =>
       documentStore.pair
-        .consistencyStatus(publishedDocId, documentType)
+        .consistencyStatus(publishedDocId, documentType, version)
         .pipe(map((isConsistent) => (isConsistent ? NOT_SYNCING : SYNCING))),
-    [documentStore.pair, documentType, publishedDocId],
+    [documentStore.pair, documentType, publishedDocId, version],
   )
   return useObservable<Observable<SyncState>>(observable, NOT_SYNCING)
 }
diff --git a/packages/sanity/src/core/hooks/useValidationStatus.ts b/packages/sanity/src/core/hooks/useValidationStatus.ts
index 165e059d932..2411b0eabe8 100644
--- a/packages/sanity/src/core/hooks/useValidationStatus.ts
+++ b/packages/sanity/src/core/hooks/useValidationStatus.ts
@@ -7,12 +7,16 @@ import {type ValidationStatus} from '../validation'
 const INITIAL: ValidationStatus = {validation: [], isValidating: false}
 
 /** @internal */
-export function useValidationStatus(publishedDocId: string, docTypeName: string): ValidationStatus {
+export function useValidationStatus(
+  publishedDocId: string,
+  docTypeName: string,
+  version?: string,
+): ValidationStatus {
   const documentStore = useDocumentStore()
 
   const observable = useMemo(
-    () => documentStore.pair.validation(publishedDocId, docTypeName),
-    [docTypeName, documentStore.pair, publishedDocId],
+    () => documentStore.pair.validation(publishedDocId, docTypeName, version),
+    [docTypeName, documentStore.pair, publishedDocId, version],
   )
   return useObservable(observable, INITIAL)
 }
diff --git a/packages/sanity/src/core/preview/availability.ts b/packages/sanity/src/core/preview/availability.ts
index d339ff3d7aa..160beee56a6 100644
--- a/packages/sanity/src/core/preview/availability.ts
+++ b/packages/sanity/src/core/preview/availability.ts
@@ -71,25 +71,36 @@ export function createPreviewAvailabilityObserver(
   versionedClient: SanityClient,
   observePaths: ObservePathsFn,
 ): {
-  observeDocumentPairAvailability(id: string): Observable<DraftsModelDocumentAvailability>
+  observeDocumentPairAvailability(
+    id: string,
+    options?: {version?: string},
+  ): Observable<DraftsModelDocumentAvailability>
 } {
   /**
    * Returns an observable of metadata for a given drafts model document
    */
   function observeDocumentPairAvailability(
     id: string,
+    {version}: {version?: string} = {},
   ): Observable<DraftsModelDocumentAvailability> {
     const draftId = getDraftId(id)
     const publishedId = getPublishedId(id)
+    const versionId = version ? [version, publishedId].join('.') : undefined
     return combineLatest([
       observeDocumentAvailability(draftId),
       observeDocumentAvailability(publishedId),
+      ...(versionId ? [observeDocumentAvailability(versionId)] : []),
     ]).pipe(
       distinctUntilChanged(shallowEquals),
-      map(([draftReadability, publishedReadability]) => {
+      map(([draftReadability, publishedReadability, versionReadability]) => {
         return {
           draft: draftReadability,
           published: publishedReadability,
+          ...(versionReadability
+            ? {
+                version: versionReadability,
+              }
+            : {}),
         }
       }),
     )
diff --git a/packages/sanity/src/core/preview/components/PreviewLoader.tsx b/packages/sanity/src/core/preview/components/PreviewLoader.tsx
index a9e03420d9c..414ab966964 100644
--- a/packages/sanity/src/core/preview/components/PreviewLoader.tsx
+++ b/packages/sanity/src/core/preview/components/PreviewLoader.tsx
@@ -6,7 +6,6 @@ import {
   useMemo,
   useState,
 } from 'react'
-import {useRouter} from 'sanity/router'
 
 import {type PreviewProps} from '../../components'
 import {type RenderPreviewCallbackProps} from '../../form'
@@ -39,7 +38,6 @@ export function PreviewLoader(
     ...restProps
   } = props
 
-  const perspective = useRouter().stickyParams.perspective
   const {t} = useTranslation()
   const [element, setElement] = useState<HTMLDivElement | null>(null)
 
@@ -53,7 +51,6 @@ export function PreviewLoader(
   const preview = useValuePreview({
     enabled: skipVisibilityCheck || isVisible,
     schemaType,
-    perspective,
     value,
   })
 
diff --git a/packages/sanity/src/core/preview/createPreviewObserver.ts b/packages/sanity/src/core/preview/createPreviewObserver.ts
index 241bbe7b589..3b3338adef6 100644
--- a/packages/sanity/src/core/preview/createPreviewObserver.ts
+++ b/packages/sanity/src/core/preview/createPreviewObserver.ts
@@ -34,7 +34,6 @@ export function createPreviewObserver(context: {
   value: Previewable,
   type: PreviewableType,
   options?: {
-    perspective?: string
     viewOptions?: PrepareViewOptions
     apiConfig?: ApiConfig
   },
@@ -45,7 +44,6 @@ export function createPreviewObserver(context: {
     value: Previewable,
     type: PreviewableType,
     options?: {
-      perspective?: string
       viewOptions?: PrepareViewOptions
       apiConfig?: ApiConfig
     },
diff --git a/packages/sanity/src/core/preview/documentPair.ts b/packages/sanity/src/core/preview/documentPair.ts
index 6c8e682258c..6b5fbabe426 100644
--- a/packages/sanity/src/core/preview/documentPair.ts
+++ b/packages/sanity/src/core/preview/documentPair.ts
@@ -14,6 +14,7 @@ export function create_preview_documentPair(
   observePathsDocumentPair: <T extends SanityDocument = SanityDocument>(
     id: string,
     paths: PreviewPath[],
+    options?: {version?: string},
   ) => Observable<DraftsModelDocument<T>>
 } {
   const {observeDocumentPairAvailability} = createPreviewAvailabilityObserver(
@@ -21,17 +22,23 @@ export function create_preview_documentPair(
     observePaths,
   )
 
-  const ALWAYS_INCLUDED_SNAPSHOT_PATHS: PreviewPath[] = [['_updatedAt'], ['_createdAt'], ['_type']]
+  const ALWAYS_INCLUDED_SNAPSHOT_PATHS: PreviewPath[] = [
+    ['_updatedAt'],
+    ['_createdAt'],
+    ['_type'],
+    ['_version'],
+  ]
 
   return {observePathsDocumentPair}
 
   function observePathsDocumentPair<T extends SanityDocument = SanityDocument>(
     id: string,
     paths: PreviewPath[],
+    {version}: {version?: string} = {},
   ): Observable<DraftsModelDocument<T>> {
-    const {draftId, publishedId} = getIdPair(id)
+    const {draftId, publishedId, versionId} = getIdPair(id, {version})
 
-    return observeDocumentPairAvailability(draftId).pipe(
+    return observeDocumentPairAvailability(draftId, {version}).pipe(
       switchMap((availability) => {
         if (!availability.draft.available && !availability.published.available) {
           // short circuit, neither draft nor published is available so no point in trying to get a snapshot
@@ -46,6 +53,14 @@ export function create_preview_documentPair(
               availability: availability.published,
               snapshot: undefined,
             },
+            ...(availability.version
+              ? {
+                  version: {
+                    availability: availability.version,
+                    snapshot: undefined,
+                  },
+                }
+              : {}),
           })
         }
 
@@ -54,10 +69,12 @@ export function create_preview_documentPair(
         return combineLatest([
           observePaths({_type: 'reference', _ref: draftId}, snapshotPaths),
           observePaths({_type: 'reference', _ref: publishedId}, snapshotPaths),
+          ...(version ? [observePaths({_type: 'reference', _ref: versionId}, snapshotPaths)] : []),
         ]).pipe(
-          map(([draftSnapshot, publishedSnapshot]) => {
+          map(([draftSnapshot, publishedSnapshot, versionSnapshot]) => {
             // note: assume type is always the same
             const type =
+              (isRecord(versionSnapshot) && '_type' in versionSnapshot && versionSnapshot._type) ||
               (isRecord(draftSnapshot) && '_type' in draftSnapshot && draftSnapshot._type) ||
               (isRecord(publishedSnapshot) &&
                 '_type' in publishedSnapshot &&
@@ -75,6 +92,14 @@ export function create_preview_documentPair(
                 availability: availability.published,
                 snapshot: publishedSnapshot as T,
               },
+              ...(availability.version
+                ? {
+                    version: {
+                      availability: availability.version,
+                      snapshot: versionSnapshot as T,
+                    },
+                  }
+                : {}),
             }
           }),
         )
diff --git a/packages/sanity/src/core/preview/documentPreviewStore.ts b/packages/sanity/src/core/preview/documentPreviewStore.ts
index e1fbd9f4d6c..7f4343819ef 100644
--- a/packages/sanity/src/core/preview/documentPreviewStore.ts
+++ b/packages/sanity/src/core/preview/documentPreviewStore.ts
@@ -34,7 +34,7 @@ import {
 export type ObserveForPreviewFn = (
   value: Previewable,
   type: PreviewableType,
-  options?: {viewOptions?: PrepareViewOptions; apiConfig?: ApiConfig; perspective?: string},
+  options?: {viewOptions?: PrepareViewOptions; apiConfig?: ApiConfig},
 ) => Observable<PreparedSnapshot>
 
 /**
@@ -57,11 +57,13 @@ export interface DocumentPreviewStore {
    */
   unstable_observeDocumentPairAvailability: (
     id: string,
+    options?: {version?: string},
   ) => Observable<DraftsModelDocumentAvailability>
 
   unstable_observePathsDocumentPair: <T extends SanityDocument = SanityDocument>(
     id: string,
     paths: PreviewPath[],
+    options?: {version?: string},
   ) => Observable<DraftsModelDocument<T>>
 
   /**
diff --git a/packages/sanity/src/core/preview/types.ts b/packages/sanity/src/core/preview/types.ts
index dbd82de69fa..91ccb78a2a8 100644
--- a/packages/sanity/src/core/preview/types.ts
+++ b/packages/sanity/src/core/preview/types.ts
@@ -94,6 +94,11 @@ export interface DraftsModelDocumentAvailability {
    * document readability for the draft document
    */
   draft: DocumentAvailability
+
+  /**
+   * document readability for the version document
+   */
+  version?: DocumentAvailability
 }
 
 /**
@@ -110,6 +115,10 @@ export interface DraftsModelDocument<T extends SanityDocumentLike = SanityDocume
     availability: DocumentAvailability
     snapshot: T | undefined
   }
+  version?: {
+    availability: DocumentAvailability
+    snapshot: T | undefined
+  }
 }
 
 /**
diff --git a/packages/sanity/src/core/preview/useValuePreview.ts b/packages/sanity/src/core/preview/useValuePreview.ts
index 5613d7c2582..863dc2a54e7 100644
--- a/packages/sanity/src/core/preview/useValuePreview.ts
+++ b/packages/sanity/src/core/preview/useValuePreview.ts
@@ -28,22 +28,20 @@ function useDocumentPreview(props: {
   enabled?: boolean
   ordering?: SortOrdering
   schemaType?: SchemaType
-  perspective?: string
   value: unknown | undefined
 }): State {
-  const {enabled = true, perspective, ordering, schemaType, value: previewValue} = props || {}
+  const {enabled = true, ordering, schemaType, value: previewValue} = props || {}
   const {observeForPreview} = useDocumentPreviewStore()
   const observable = useMemo<Observable<State>>(() => {
     if (!enabled || !previewValue || !schemaType) return of(PENDING_STATE)
 
     return observeForPreview(previewValue as Previewable, schemaType, {
-      perspective,
       viewOptions: {ordering: ordering},
     }).pipe(
       map((event) => ({isLoading: false, value: event.snapshot || undefined})),
       catchError((error) => of({isLoading: false, error})),
     )
-  }, [enabled, previewValue, schemaType, observeForPreview, perspective, ordering])
+  }, [enabled, previewValue, schemaType, observeForPreview, ordering])
 
   return useObservable(observable, INITIAL_STATE)
 }
diff --git a/packages/sanity/src/core/scheduledPublishing/plugin/documentActions/schedule/ScheduleAction.tsx b/packages/sanity/src/core/scheduledPublishing/plugin/documentActions/schedule/ScheduleAction.tsx
index fc704e80e2f..67b773ece53 100644
--- a/packages/sanity/src/core/scheduledPublishing/plugin/documentActions/schedule/ScheduleAction.tsx
+++ b/packages/sanity/src/core/scheduledPublishing/plugin/documentActions/schedule/ScheduleAction.tsx
@@ -44,12 +44,13 @@ const debug = debugWithName('ScheduleAction')
  * @beta
  */
 export const ScheduleAction = (props: DocumentActionProps): DocumentActionDescription | null => {
-  const {draft, id, liveEdit, onComplete, published, type} = props
-
+  const {draft, id, liveEdit, onComplete, published, type, bundleSlug} = props
   const currentUser = useCurrentUser()
+
   const [permissions, isPermissionsLoading] = useDocumentPairPermissions({
     id,
     type,
+    version: bundleSlug,
     permission: 'publish',
   })
   const {createSchedule} = useScheduleOperation()
diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/checkoutPair.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/checkoutPair.ts
index 72f788086a5..491c94a42d8 100644
--- a/packages/sanity/src/core/store/_legacy/document/document-pair/checkoutPair.ts
+++ b/packages/sanity/src/core/store/_legacy/document/document-pair/checkoutPair.ts
@@ -27,7 +27,7 @@ const isMutationEventForDocId =
 /**
  * @hidden
  * @beta */
-export type WithVersion<T> = T & {version: 'published' | 'draft'}
+export type WithVersion<T> = T & {version: 'published' | 'draft' | 'version'}
 
 /**
  * @hidden
@@ -60,16 +60,17 @@ export interface DocumentVersion {
 /**
  * @hidden
  * @beta */
-export interface Pair {
+export type Pair = {
   /** @internal */
   transactionsPendingEvents$: Observable<PendingMutationsEvent>
   published: DocumentVersion
   draft: DocumentVersion
+  version?: DocumentVersion
   complete: () => void
 }
 
-function setVersion<T>(version: 'draft' | 'published') {
-  return (ev: T): T & {version: 'draft' | 'published'} => ({...ev, version})
+function setVersion<T>(version: 'draft' | 'published' | 'version') {
+  return (ev: T): T & {version: 'draft' | 'published' | 'version'} => ({...ev, version})
 }
 
 function requireId<T extends {_id?: string; _type: string}>(
@@ -183,7 +184,7 @@ export function checkoutPair(
   idPair: IdPair,
   serverActionsEnabled: Observable<boolean>,
 ): Pair {
-  const {publishedId, draftId} = idPair
+  const {publishedId, draftId, versionId} = idPair
 
   const listenerEventsConnector = new Subject<ListenerEvent>()
   const listenerEvents$ = getPairListener(client, idPair).pipe(
@@ -199,6 +200,14 @@ export function checkoutPair(
     listenerEvents$.pipe(filter(isMutationEventForDocId(draftId))),
   )
 
+  const version =
+    typeof versionId === 'undefined'
+      ? undefined
+      : createBufferedDocument(
+          versionId,
+          listenerEvents$.pipe(filter(isMutationEventForDocId(versionId))),
+        )
+
   const published = createBufferedDocument(
     publishedId,
     listenerEvents$.pipe(filter(isMutationEventForDocId(publishedId))),
@@ -209,7 +218,11 @@ export function checkoutPair(
     filter((ev): ev is PendingMutationsEvent => ev.type === 'pending'),
   )
 
-  const commits$ = merge(draft.commitRequest$, published.commitRequest$).pipe(
+  const commits$ = merge(
+    draft.commitRequest$,
+    published.commitRequest$,
+    version ? version.commitRequest$ : EMPTY,
+  ).pipe(
     mergeMap((commitRequest) =>
       serverActionsEnabled.pipe(
         take(1),
@@ -227,13 +240,20 @@ export function checkoutPair(
     draft: {
       ...draft,
       events: merge(commits$, reconnect$, draft.events).pipe(map(setVersion('draft'))),
-      consistency$: draft.consistency$,
       remoteSnapshot$: draft.remoteSnapshot$.pipe(map(setVersion('draft'))),
     },
+    ...(typeof version === 'undefined'
+      ? {}
+      : {
+          version: {
+            ...version,
+            events: merge(commits$, reconnect$, version.events).pipe(map(setVersion('version'))),
+            remoteSnapshot$: version.remoteSnapshot$.pipe(map(setVersion('version'))),
+          },
+        }),
     published: {
       ...published,
       events: merge(commits$, reconnect$, published.events).pipe(map(setVersion('published'))),
-      consistency$: published.consistency$,
       remoteSnapshot$: published.remoteSnapshot$.pipe(map(setVersion('published'))),
     },
     complete: () => listenerEventsConnector.complete(),
diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/editState.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/editState.ts
index adba9e7588f..6abf9c8094b 100644
--- a/packages/sanity/src/core/store/_legacy/document/document-pair/editState.ts
+++ b/packages/sanity/src/core/store/_legacy/document/document-pair/editState.ts
@@ -22,8 +22,23 @@ export interface EditStateFor {
   transactionSyncLock: TransactionSyncLockState | null
   draft: SanityDocument | null
   published: SanityDocument | null
+  version: SanityDocument | null
+  /**
+   * Whether live edit is enabled. This may be true for various reasons:
+   *
+   * - The schema type has live edit enabled.
+   * - A version of the document is checked out.
+   */
   liveEdit: boolean
+  /**
+   * Whether the schema type has live edit enabled.
+   */
+  liveEditSchemaType: boolean
   ready: boolean
+  /**
+   * When editing a version, the slug of the bundle the document belongs to.
+   */
+  bundleSlug?: string
 }
 const LOCKED: TransactionSyncLockState = {enabled: true}
 const NOT_LOCKED: TransactionSyncLockState = {enabled: false}
@@ -39,7 +54,9 @@ export const editState = memoize(
     idPair: IdPair,
     typeName: string,
   ): Observable<EditStateFor> => {
-    const liveEdit = isLiveEditEnabled(ctx.schema, typeName)
+    const liveEditSchemaType = isLiveEditEnabled(ctx.schema, typeName)
+    const liveEdit = typeof idPair.versionId !== 'undefined' || liveEditSchemaType
+
     return snapshotPair(ctx.client, idPair, typeName, ctx.serverActionsEnabled).pipe(
       switchMap((versions) =>
         combineLatest([
@@ -49,25 +66,32 @@ export const editState = memoize(
             map((ev: PendingMutationsEvent) => (ev.phase === 'begin' ? LOCKED : NOT_LOCKED)),
             startWith(NOT_LOCKED),
           ),
+          ...(typeof versions.version === 'undefined' ? [] : [versions.version.snapshots$]),
         ]),
       ),
-      map(([draftSnapshot, publishedSnapshot, transactionSyncLock]) => ({
+      map(([draftSnapshot, publishedSnapshot, transactionSyncLock, versionSnapshot]) => ({
         id: idPair.publishedId,
         type: typeName,
         draft: draftSnapshot,
         published: publishedSnapshot,
+        version: typeof idPair.versionId === 'undefined' ? null : versionSnapshot,
         liveEdit,
+        liveEditSchemaType,
         ready: true,
         transactionSyncLock,
+        bundleSlug: idPair.versionId?.split('.').at(0),
       })),
       startWith({
         id: idPair.publishedId,
         type: typeName,
         draft: null,
         published: null,
+        version: null,
         liveEdit,
+        liveEditSchemaType,
         ready: false,
         transactionSyncLock: null,
+        bundleSlug: idPair.versionId?.split('.').at(0),
       }),
       publishReplay(1),
       refCount(),
diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/memoizeKeyGen.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/memoizeKeyGen.ts
index 25ff846510d..258a58408bc 100644
--- a/packages/sanity/src/core/store/_legacy/document/document-pair/memoizeKeyGen.ts
+++ b/packages/sanity/src/core/store/_legacy/document/document-pair/memoizeKeyGen.ts
@@ -4,5 +4,5 @@ import {type IdPair} from '../types'
 
 export function memoizeKeyGen(client: SanityClient, idPair: IdPair, typeName: string) {
   const config = client.config()
-  return `${config.dataset ?? ''}-${config.projectId ?? ''}-${idPair.publishedId}-${typeName}`
+  return `${config.dataset ?? ''}-${config.projectId ?? ''}-${idPair.publishedId}-${idPair.versionId ?? ''}-${typeName}`
 }
diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/operationArgs.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/operationArgs.ts
index de0b0b5ab29..397d308383a 100644
--- a/packages/sanity/src/core/store/_legacy/document/document-pair/operationArgs.ts
+++ b/packages/sanity/src/core/store/_legacy/document/document-pair/operationArgs.ts
@@ -30,14 +30,28 @@ export const operationArgs = memoize(
           versions.draft.snapshots$,
           versions.published.snapshots$,
           ctx.serverActionsEnabled,
+          ...(typeof versions.version === 'undefined' ? [] : [versions.version.snapshots$]),
         ]).pipe(
           map(
-            ([draft, published, canUseServerActions]): OperationArgs => ({
+            ([draft, published, canUseServerActions, version]): OperationArgs => ({
               ...ctx,
               serverActionsEnabled: canUseServerActions,
               idPair,
               typeName,
-              snapshots: {draft, published},
+              snapshots: {
+                published,
+                draft,
+                ...(version
+                  ? {
+                      version,
+                    }
+                  : {}),
+              },
+              ...(versions.version
+                ? {
+                    version: versions.version,
+                  }
+                : {}),
               draft: versions.draft,
               published: versions.published,
             }),
diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/operations/commit.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/operations/commit.ts
index 570af1eea09..45d90538c20 100644
--- a/packages/sanity/src/core/store/_legacy/document/document-pair/operations/commit.ts
+++ b/packages/sanity/src/core/store/_legacy/document/document-pair/operations/commit.ts
@@ -4,7 +4,8 @@ import {type OperationImpl} from './types'
 
 export const commit: OperationImpl = {
   disabled: (): false => false,
-  execute: ({draft, published}) => {
+  execute: ({draft, published, version}) => {
+    version?.commit()
     draft.commit()
     published.commit()
     // note: we might be able to connect with the outgoing commit request stream here
diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/operations/patch.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/operations/patch.ts
index f67b96767ec..bb4740ebef4 100644
--- a/packages/sanity/src/core/store/_legacy/document/document-pair/operations/patch.ts
+++ b/packages/sanity/src/core/store/_legacy/document/document-pair/operations/patch.ts
@@ -5,10 +5,23 @@ import {type OperationImpl} from './types'
 export const patch: OperationImpl<[patches: any[], initialDocument?: Record<string, any>]> = {
   disabled: (): false => false,
   execute: (
-    {schema, snapshots, idPair, draft, published, typeName},
+    {schema, snapshots, idPair, draft, published, version, typeName},
     patches = [],
     initialDocument,
   ): void => {
+    // TODO: This is exactly the same strategy as live-editing. Can we avoid duplication?
+    if (version) {
+      // No drafting, so patch and commit the published document
+      version.mutate([
+        version.createIfNotExists({
+          _type: typeName,
+          ...initialDocument,
+        }),
+        ...version.patch(patches),
+      ])
+      return
+    }
+
     if (isLiveEditEnabled(schema, typeName)) {
       // No drafting, so patch and commit the published document
       published.mutate([
diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/operations/types.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/operations/types.ts
index 62615742dd5..df823865e5c 100644
--- a/packages/sanity/src/core/store/_legacy/document/document-pair/operations/types.ts
+++ b/packages/sanity/src/core/store/_legacy/document/document-pair/operations/types.ts
@@ -47,8 +47,13 @@ export interface OperationArgs {
   schema: Schema
   typeName: string
   idPair: IdPair
-  snapshots: {draft: null | SanityDocument; published: null | SanityDocument}
+  snapshots: {
+    draft: null | SanityDocument
+    published: null | SanityDocument
+    version?: null | SanityDocument
+  }
   draft: DocumentVersionSnapshots
   published: DocumentVersionSnapshots
+  version?: DocumentVersionSnapshots
   serverActionsEnabled: boolean
 }
diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/remoteSnapshots.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/remoteSnapshots.ts
index 91db8f91fde..e280c196263 100644
--- a/packages/sanity/src/core/store/_legacy/document/document-pair/remoteSnapshots.ts
+++ b/packages/sanity/src/core/store/_legacy/document/document-pair/remoteSnapshots.ts
@@ -1,5 +1,5 @@
 import {type SanityClient} from '@sanity/client'
-import {merge, type Observable} from 'rxjs'
+import {EMPTY, merge, type Observable} from 'rxjs'
 import {switchMap} from 'rxjs/operators'
 
 import {type IdPair} from '../types'
@@ -17,7 +17,9 @@ export const remoteSnapshots = memoize(
     serverActionsEnabled: Observable<boolean>,
   ): Observable<RemoteSnapshotVersionEvent> => {
     return memoizedPair(client, idPair, typeName, serverActionsEnabled).pipe(
-      switchMap(({published, draft}) => merge(published.remoteSnapshot$, draft.remoteSnapshot$)),
+      switchMap(({published, draft, version}) =>
+        merge(published.remoteSnapshot$, draft.remoteSnapshot$, version?.remoteSnapshot$ ?? EMPTY),
+      ),
     )
   },
   memoizeKeyGen,
diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/serverOperations/patch.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/serverOperations/patch.ts
index 4fce51f99a3..99f056a02f6 100644
--- a/packages/sanity/src/core/store/_legacy/document/document-pair/serverOperations/patch.ts
+++ b/packages/sanity/src/core/store/_legacy/document/document-pair/serverOperations/patch.ts
@@ -4,10 +4,31 @@ import {isLiveEditEnabled} from '../utils/isLiveEditEnabled'
 export const patch: OperationImpl<[patches: any[], initialDocument?: Record<string, any>]> = {
   disabled: (): false => false,
   execute: (
-    {schema, snapshots, idPair, draft, published, typeName},
+    {schema, snapshots, idPair, draft, published, version, typeName},
     patches = [],
     initialDocument,
   ): void => {
+    // TODO: Actions API can't edit versions (yet).
+    // TODO: This is exactly the same strategy as live-editing. Can we avoid duplication?
+    if (version) {
+      // No drafting, so patch and commit the published document
+      const patchMutation = version.patch(patches)
+      // Note: if the document doesn't exist on the server yet, we need to create it first. We only want to do this if we can't see it locally
+      // if it's been deleted on the server we want that to become a mutation error when submitting.
+      const mutations = snapshots.version
+        ? patchMutation
+        : [
+            version.createIfNotExists({
+              _type: typeName,
+              ...initialDocument,
+            }),
+          ]
+      // No drafting, so patch and commit the published document
+      version.mutate(mutations)
+
+      return
+    }
+
     if (isLiveEditEnabled(schema, typeName)) {
       // No drafting, so patch and commit the published document
       const patchMutation = published.patch(patches)
diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/snapshotPair.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/snapshotPair.ts
index 0efc43a5461..4fb0d8cdf08 100644
--- a/packages/sanity/src/core/store/_legacy/document/document-pair/snapshotPair.ts
+++ b/packages/sanity/src/core/store/_legacy/document/document-pair/snapshotPair.ts
@@ -57,6 +57,7 @@ interface SnapshotPair {
   transactionsPendingEvents$: Observable<PendingMutationsEvent>
   draft: DocumentVersionSnapshots
   published: DocumentVersionSnapshots
+  version?: DocumentVersionSnapshots
 }
 
 /** @internal */
@@ -68,11 +69,12 @@ export const snapshotPair = memoize(
     serverActionsEnabled: Observable<boolean>,
   ): Observable<SnapshotPair> => {
     return memoizedPair(client, idPair, typeName, serverActionsEnabled).pipe(
-      map(({published, draft, transactionsPendingEvents$}): SnapshotPair => {
+      map(({published, draft, version, transactionsPendingEvents$}): SnapshotPair => {
         return {
           transactionsPendingEvents$,
           published: withSnapshots(published),
           draft: withSnapshots(draft),
+          ...(version ? {version: withSnapshots(version)} : {}),
         }
       }),
       publishReplay(1),
diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/validation.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/validation.ts
index 8cf895fe80a..94c0e865b7b 100644
--- a/packages/sanity/src/core/store/_legacy/document/document-pair/validation.ts
+++ b/packages/sanity/src/core/store/_legacy/document/document-pair/validation.ts
@@ -32,11 +32,11 @@ export const validation = memoize(
       i18n: LocaleSource
       serverActionsEnabled: Observable<boolean>
     },
-    {draftId, publishedId}: IdPair,
+    {draftId, publishedId, versionId}: IdPair,
     typeName: string,
   ): Observable<ValidationStatus> => {
-    const document$ = editState(ctx, {draftId, publishedId}, typeName).pipe(
-      map(({draft, published}) => draft || published),
+    const document$ = editState(ctx, {draftId, publishedId, versionId}, typeName).pipe(
+      map(({version, draft, published}) => version || draft || published),
       throttleTime(DOC_UPDATE_DELAY, asyncScheduler, {trailing: true}),
       distinctUntilChanged((prev, next) => {
         if (prev?._rev === next?._rev) {
diff --git a/packages/sanity/src/core/store/_legacy/document/document-store.ts b/packages/sanity/src/core/store/_legacy/document/document-store.ts
index d40fba36ce4..ea52b3857c9 100644
--- a/packages/sanity/src/core/store/_legacy/document/document-store.ts
+++ b/packages/sanity/src/core/store/_legacy/document/document-store.ts
@@ -8,7 +8,7 @@ import {type LocaleSource} from '../../../i18n'
 import {type DocumentPreviewStore} from '../../../preview'
 import {DEFAULT_STUDIO_CLIENT_OPTIONS} from '../../../studioClient'
 import {type Template} from '../../../templates'
-import {getDraftId, isDraftId} from '../../../util'
+import {getIdPair, isDraftId} from '../../../util'
 import {type ValidationStatus} from '../../../validation'
 import {type HistoryStore} from '../history'
 import {checkoutPair, type DocumentVersionEvent, type Pair} from './document-pair/checkoutPair'
@@ -33,12 +33,12 @@ import {type IdPair} from './types'
  * @beta */
 export type QueryParams = Record<string, string | number | boolean | string[]>
 
-function getIdPairFromPublished(publishedId: string): IdPair {
+function getIdPairFromPublished(publishedId: string, version?: string): IdPair {
   if (isDraftId(publishedId)) {
     throw new Error('editOpsOf does not expect a draft id.')
   }
 
-  return {publishedId, draftId: getDraftId(publishedId)}
+  return getIdPair(publishedId, {version})
 }
 
 /**
@@ -59,17 +59,29 @@ export interface DocumentStore {
   resolveTypeForDocument: (id: string, specifiedType?: string) => Observable<string>
 
   pair: {
-    consistencyStatus: (publishedId: string, type: string) => Observable<boolean>
+    consistencyStatus: (publishedId: string, type: string, version?: string) => Observable<boolean>
     /** @internal */
-    documentEvents: (publishedId: string, type: string) => Observable<DocumentVersionEvent>
+    documentEvents: (
+      publishedId: string,
+      type: string,
+      version?: string,
+    ) => Observable<DocumentVersionEvent>
     /** @internal */
-    editOperations: (publishedId: string, type: string) => Observable<OperationsAPI>
-    editState: (publishedId: string, type: string) => Observable<EditStateFor>
+    editOperations: (
+      publishedId: string,
+      type: string,
+      version?: string,
+    ) => Observable<OperationsAPI>
+    editState: (publishedId: string, type: string, version?: string) => Observable<EditStateFor>
     operationEvents: (
       publishedId: string,
       type: string,
     ) => Observable<OperationSuccess | OperationError>
-    validation: (publishedId: string, type: string) => Observable<ValidationStatus>
+    validation: (
+      publishedId: string,
+      type: string,
+      version?: string,
+    ) => Observable<ValidationStatus>
   }
 }
 
@@ -133,27 +145,27 @@ export function createDocumentStore({
       return resolveTypeForDocument(client, id, specifiedType)
     },
     pair: {
-      consistencyStatus(publishedId, type) {
+      consistencyStatus(publishedId, type, version) {
         return consistencyStatus(
           ctx.client,
-          getIdPairFromPublished(publishedId),
+          getIdPairFromPublished(publishedId, version),
           type,
           serverActionsEnabled,
         )
       },
-      documentEvents(publishedId, type) {
+      documentEvents(publishedId, type, version) {
         return documentEvents(
           ctx.client,
-          getIdPairFromPublished(publishedId),
+          getIdPairFromPublished(publishedId, version),
           type,
           serverActionsEnabled,
         )
       },
-      editOperations(publishedId, type) {
-        return editOperations(ctx, getIdPairFromPublished(publishedId), type)
+      editOperations(publishedId, type, version) {
+        return editOperations(ctx, getIdPairFromPublished(publishedId, version), type)
       },
-      editState(publishedId, type) {
-        return editState(ctx, getIdPairFromPublished(publishedId), type)
+      editState(publishedId, type, version) {
+        return editState(ctx, getIdPairFromPublished(publishedId, version), type)
       },
       operationEvents(publishedId, type) {
         return operationEvents({
@@ -174,8 +186,8 @@ export function createDocumentStore({
           }),
         )
       },
-      validation(publishedId, type) {
-        return validation(ctx, getIdPairFromPublished(publishedId), type)
+      validation(publishedId, type, version) {
+        return validation(ctx, getIdPairFromPublished(publishedId, version), type)
       },
     },
   }
diff --git a/packages/sanity/src/core/store/_legacy/document/getPairListener.ts b/packages/sanity/src/core/store/_legacy/document/getPairListener.ts
index aadbc5384c0..2e77b22203a 100644
--- a/packages/sanity/src/core/store/_legacy/document/getPairListener.ts
+++ b/packages/sanity/src/core/store/_legacy/document/getPairListener.ts
@@ -16,6 +16,7 @@ import {
 interface Snapshots {
   draft: SanityDocument | null
   published: SanityDocument | null
+  version: SanityDocument | null
 }
 
 /** @internal */
@@ -64,14 +65,14 @@ export function getPairListener(
   idPair: IdPair,
   options: PairListenerOptions = {},
 ): Observable<ListenerEvent> {
-  const {publishedId, draftId} = idPair
+  const {publishedId, draftId, versionId} = idPair
+
   return defer(
     () =>
       client.observable.listen(
-        `*[_id == $publishedId || _id == $draftId]`,
+        `*[_id in $ids]`,
         {
-          publishedId,
-          draftId,
+          ids: [publishedId, draftId, versionId].filter((id) => typeof id !== 'undefined'),
         },
         {
           includeResult: false,
@@ -87,6 +88,7 @@ export function getPairListener(
             concatMap((snapshots) => [
               createSnapshotEvent(draftId, snapshots.draft),
               createSnapshotEvent(publishedId, snapshots.published),
+              ...(versionId ? [createSnapshotEvent(versionId, snapshots.version)] : []),
             ]),
           )
         : observableOf(event),
@@ -131,11 +133,15 @@ export function getPairListener(
 
   function fetchInitialDocumentSnapshots(): Observable<Snapshots> {
     return client.observable
-      .getDocuments<SanityDocument>([draftId, publishedId], {tag: 'document.snapshots'})
+      .getDocuments<SanityDocument>(
+        [publishedId, draftId, versionId].filter((id): id is string => typeof id === 'string'),
+        {tag: 'document.snapshots'},
+      )
       .pipe(
-        map(([draft, published]) => ({
+        map(([published, draft, version]) => ({
           draft,
           published,
+          version,
         })),
       )
   }
diff --git a/packages/sanity/src/core/store/_legacy/document/types.ts b/packages/sanity/src/core/store/_legacy/document/types.ts
index 0ad2cf6e0a1..0891e85787f 100644
--- a/packages/sanity/src/core/store/_legacy/document/types.ts
+++ b/packages/sanity/src/core/store/_legacy/document/types.ts
@@ -34,7 +34,8 @@ export interface PendingMutationsEvent {
 }
 
 /** @internal */
-export interface IdPair {
+export type IdPair = {
   draftId: string
   publishedId: string
+  versionId?: string
 }
diff --git a/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts b/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts
index 5c84b123398..a4f4185f489 100644
--- a/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts
+++ b/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts
@@ -10,6 +10,7 @@ import {DEFAULT_STUDIO_CLIENT_OPTIONS} from '../../../studioClient'
 import {
   createHookFromObservableFactory,
   getDraftId,
+  getIdPair,
   getPublishedId,
   type PartialExcept,
 } from '../../../util'
@@ -168,6 +169,7 @@ export interface DocumentPairPermissionsOptions {
   grantsStore: GrantsStore
   id: string
   type: string
+  version?: string
   permission: DocumentPermission
   serverActionsEnabled: Observable<boolean>
 }
@@ -187,6 +189,7 @@ export function getDocumentPairPermissions({
   permission,
   type,
   serverActionsEnabled,
+  version,
 }: DocumentPairPermissionsOptions): Observable<PermissionCheckResult> {
   // this case was added to fix a crash that would occur if the `schemaType` was
   // omitted from `S.documentList()`
@@ -199,12 +202,7 @@ export function getDocumentPairPermissions({
 
   const liveEdit = Boolean(getSchemaType(schema, type).liveEdit)
 
-  return snapshotPair(
-    client,
-    {draftId: getDraftId(id), publishedId: getPublishedId(id)},
-    type,
-    serverActionsEnabled,
-  ).pipe(
+  return snapshotPair(client, getIdPair(id, {version}), type, serverActionsEnabled).pipe(
     switchMap((pair) =>
       combineLatest([pair.draft.snapshots$, pair.published.snapshots$]).pipe(
         map(([draft, published]) => ({draft, published})),
@@ -279,6 +277,7 @@ export const useDocumentPairPermissionsFromHookFactory = createHookFromObservabl
 export function useDocumentPairPermissions({
   id,
   type,
+  version,
   permission,
   client: overrideClient,
   schema: overrideSchema,
@@ -306,8 +305,8 @@ export function useDocumentPairPermissions({
 
   return useDocumentPairPermissionsFromHookFactory(
     useMemo(
-      () => ({client, schema, grantsStore, id, permission, type, serverActionsEnabled}),
-      [client, grantsStore, id, permission, schema, type, serverActionsEnabled],
+      () => ({client, schema, grantsStore, id, permission, type, serverActionsEnabled, version}),
+      [client, grantsStore, id, permission, schema, type, serverActionsEnabled, version],
     ),
   )
 }
diff --git a/packages/sanity/src/core/store/_legacy/history/history/Aligner.ts b/packages/sanity/src/core/store/_legacy/history/history/Aligner.ts
index b3f0df10b17..d02e6e589f0 100644
--- a/packages/sanity/src/core/store/_legacy/history/history/Aligner.ts
+++ b/packages/sanity/src/core/store/_legacy/history/history/Aligner.ts
@@ -101,6 +101,10 @@ export class Aligner {
   }
 
   appendRemoteSnapshotEvent(evt: RemoteSnapshotVersionEvent): void {
+    if (evt.version === 'version') {
+      return
+    }
+
     const state = this._states[evt.version]
 
     if (evt.type === 'snapshot') {
diff --git a/packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts b/packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts
index 403a755fbb7..1cf5d2b1594 100644
--- a/packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts
+++ b/packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts
@@ -6,14 +6,19 @@ import {
   catchError,
   distinctUntilChanged,
   map,
+  type Observable,
   of,
   type Subscription,
+  switchMap,
   tap,
 } from 'rxjs'
 
 import {
   type Annotation,
   type Chunk,
+  DRAFTS_FOLDER,
+  remoteSnapshots,
+  type RemoteSnapshotVersionEvent,
   type SelectionState,
   type TimelineController,
   useHistoryStore,
@@ -21,7 +26,6 @@ import {
 } from '../../..'
 import {useClient} from '../../../hooks'
 import {DEFAULT_STUDIO_CLIENT_OPTIONS} from '../../../studioClient'
-import {remoteSnapshots, type RemoteSnapshotVersionEvent} from '../document'
 import {fetchFeatureToggle} from '../document/document-pair/utils/fetchFeatureToggle'
 
 interface UseTimelineControllerOpts {
@@ -30,6 +34,7 @@ interface UseTimelineControllerOpts {
   onError?: (err: Error) => void
   rev?: string
   since?: string
+  version?: string
 }
 
 /** @internal */
@@ -97,6 +102,7 @@ export function useTimelineStore({
   onError,
   rev,
   since,
+  version,
 }: UseTimelineControllerOpts): TimelineStore {
   const historyStore = useHistoryStore()
   const snapshotsSubscriptionRef = useRef<Subscription | null>(null)
@@ -111,10 +117,10 @@ export function useTimelineStore({
     () =>
       historyStore.getTimelineController({
         client,
-        documentId,
+        documentId: version ? [version, documentId].join('.') : documentId,
         documentType,
       }),
-    [client, documentId, documentType, historyStore],
+    [client, documentId, documentType, historyStore, version],
   )
 
   /**
@@ -173,12 +179,22 @@ export function useTimelineStore({
     if (!snapshotsSubscriptionRef.current) {
       snapshotsSubscriptionRef.current = remoteSnapshots(
         client,
-        {draftId: `drafts.${documentId}`, publishedId: documentId},
+        {
+          draftId: [DRAFTS_FOLDER, documentId].join('.'),
+          publishedId: documentId,
+          ...(version
+            ? {
+                versionId: [version, documentId].join('.'),
+              }
+            : {}),
+        },
         documentType,
         serverActionsEnabled,
-      ).subscribe((ev: RemoteSnapshotVersionEvent) => {
-        controller.handleRemoteMutation(ev)
-      })
+      )
+        .pipe(mapVersion(version))
+        .subscribe((ev: RemoteSnapshotVersionEvent) => {
+          controller.handleRemoteMutation(ev)
+        })
     }
     return () => {
       if (snapshotsSubscriptionRef.current) {
@@ -186,7 +202,7 @@ export function useTimelineStore({
         snapshotsSubscriptionRef.current = null
       }
     }
-  }, [client, controller, documentId, documentType, serverActionsEnabled])
+  }, [client, controller, documentId, documentType, serverActionsEnabled, version])
 
   const timelineStore = useMemo(() => {
     return {
@@ -255,3 +271,31 @@ export function useTimelineStore({
 
   return timelineStore
 }
+
+/**
+ * When computing the timeline for a version document, the version id cannot simply be treated as
+ * the primary document id. This would result in multiple document pairs being checked out with
+ * different parameters, which causes multiple listeners to be created.
+ *
+ * Instead, the timeline store checks out a document pair including the version, and maps the
+ * emitted version snapshots to published and draft snapshots. This allows the underyling timeline
+ * controller to be used without modification.
+ */
+function mapVersion(version?: string) {
+  return switchMap<RemoteSnapshotVersionEvent, Observable<RemoteSnapshotVersionEvent>>((ev) => {
+    if (version) {
+      return of<RemoteSnapshotVersionEvent[]>(
+        {
+          ...ev,
+          version: 'published',
+        },
+        {
+          ...ev,
+          version: 'draft',
+        },
+      )
+    }
+
+    return of(ev)
+  })
+}
diff --git a/packages/sanity/src/core/tasks/components/form/tasksFormBuilder/FormEdit.tsx b/packages/sanity/src/core/tasks/components/form/tasksFormBuilder/FormEdit.tsx
index 505d5ea9cac..1a5efd92c51 100644
--- a/packages/sanity/src/core/tasks/components/form/tasksFormBuilder/FormEdit.tsx
+++ b/packages/sanity/src/core/tasks/components/form/tasksFormBuilder/FormEdit.tsx
@@ -5,6 +5,7 @@ import {Box, Card, Flex, Menu, MenuDivider, Stack} from '@sanity/ui'
 // eslint-disable-next-line camelcase
 import {getTheme_v2} from '@sanity/ui/theme'
 import {useCallback} from 'react'
+import {useRouter} from 'sanity/router'
 import {css, styled} from 'styled-components'
 
 import {MenuButton, MenuItem, TooltipDelayGroupProvider} from '../../../../../ui-components'
@@ -102,6 +103,7 @@ function FormEditInner(props: ObjectInputProps) {
   const statusField = props.schemaType.fields.find((f) => f.name === 'status')
   const value = props.value as TaskDocument
   const currentUser = useCurrentUser()
+  const router = useRouter()
   const {t} = useTranslation(tasksLocaleNamespace)
   const activityData = useActivityLog(value).changes
   const handleChangeAndSubscribe = useCallback(
diff --git a/packages/sanity/src/core/util/draftUtils.ts b/packages/sanity/src/core/util/draftUtils.ts
index 2eb756d9aee..c0d8a7955bb 100644
--- a/packages/sanity/src/core/util/draftUtils.ts
+++ b/packages/sanity/src/core/util/draftUtils.ts
@@ -56,11 +56,27 @@ export function isDraftId(id: string): id is DraftId {
   return id.startsWith(DRAFTS_PREFIX)
 }
 
-/** @internal */
-export function getIdPair(id: string): {draftId: DraftId; publishedId: PublishedId} {
+/**
+ * TODO: Improve return type based on presence of `version` option.
+ *
+ * @internal
+ */
+export function getIdPair(
+  id: string,
+  {version}: {version?: string} = {},
+): {
+  draftId: DraftId
+  publishedId: PublishedId
+  versionId?: string
+} {
   return {
-    draftId: getDraftId(id),
     publishedId: getPublishedId(id),
+    draftId: getDraftId(id),
+    ...(version
+      ? {
+          versionId: id.startsWith(`${version}.`) ? id : [version, getPublishedId(id)].join('.'),
+        }
+      : {}),
   }
 }
 
diff --git a/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx b/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx
index 326a9dbd675..e47a13ae481 100644
--- a/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx
+++ b/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx
@@ -77,12 +77,12 @@ export function PaneItemPreview(props: PaneItemPreviewProps) {
     <TooltipDelayGroupProvider>
       <Flex align="center" gap={3}>
         {presence && presence.length > 0 && <DocumentPreviewPresence presence={presence} />}
-        <DocumentStatusIndicator draft={draft} published={published} />
+        <DocumentStatusIndicator draft={draft} published={published} version={version} />
       </Flex>
     </TooltipDelayGroupProvider>
   )
 
-  const tooltip = <DocumentStatus draft={draft} published={published} />
+  const tooltip = <DocumentStatus draft={draft} published={published} version={version} />
 
   return (
     <Root $isInPerspective={isInPerspective}>
diff --git a/packages/sanity/src/structure/components/paneRouter/PaneRouterProvider.tsx b/packages/sanity/src/structure/components/paneRouter/PaneRouterProvider.tsx
index 9dc1eaab36d..94356a57cad 100644
--- a/packages/sanity/src/structure/components/paneRouter/PaneRouterProvider.tsx
+++ b/packages/sanity/src/structure/components/paneRouter/PaneRouterProvider.tsx
@@ -24,6 +24,7 @@ export function PaneRouterProvider(props: {
   params: Record<string, string | undefined>
   payload: unknown
   siblingIndex: number
+  perspective?: string
 }) {
   const {children, flatIndex, index, params, payload, siblingIndex} = props
   const {navigate, navigateIntent, resolvePathFromState} = useRouter()
@@ -219,6 +220,9 @@ export function PaneRouterProvider(props: {
 
       // Proxied navigation to a given intent. Consider just exposing `router` instead?
       navigateIntent,
+
+      // Perspective of the current pane
+      perspective: props.perspective,
     }),
     [
       flatIndex,
@@ -232,6 +236,7 @@ export function PaneRouterProvider(props: {
       setPayload,
       createPathWithParams,
       navigateIntent,
+      props.perspective,
       modifyCurrentGroup,
       lastPane,
       navigate,
diff --git a/packages/sanity/src/structure/components/paneRouter/types.ts b/packages/sanity/src/structure/components/paneRouter/types.ts
index a9a364674ce..b153f876a34 100644
--- a/packages/sanity/src/structure/components/paneRouter/types.ts
+++ b/packages/sanity/src/structure/components/paneRouter/types.ts
@@ -168,4 +168,9 @@ export interface PaneRouterContextValue {
     params: Record<string, string>,
     options?: {replace?: boolean},
   ) => void
+
+  /**
+   * Perspective of the current pane
+   */
+  perspective?: string
 }
diff --git a/packages/sanity/src/structure/components/structureTool/StructureTitle.tsx b/packages/sanity/src/structure/components/structureTool/StructureTitle.tsx
index 426f684e448..ac93b3dc194 100644
--- a/packages/sanity/src/structure/components/structureTool/StructureTitle.tsx
+++ b/packages/sanity/src/structure/components/structureTool/StructureTitle.tsx
@@ -6,24 +6,35 @@ import {
   useSchema,
   useTranslation,
 } from 'sanity'
+import {useRouter} from 'sanity/router'
 
 import {LOADING_PANE} from '../../constants'
 import {structureLocaleNamespace} from '../../i18n'
 import {type Panes} from '../../structureResolvers'
 import {type DocumentPaneNode} from '../../types'
 import {useStructureTool} from '../../useStructureTool'
+import {usePaneRouter} from '../paneRouter'
 
 interface StructureTitleProps {
   resolvedPanes: Panes['resolvedPanes']
 }
 
+// TODO: Fix state jank when editing different versions inside panes.
 const DocumentTitle = (props: {documentId: string; documentType: string}) => {
   const {documentId, documentType} = props
-  const editState = useEditState(documentId, documentType)
+  const router = useRouter()
+  const paneRouter = usePaneRouter()
+  const perspective = paneRouter.perspective ?? router.stickyParams.perspective
+
+  const bundlePerspective = perspective?.startsWith('bundle.')
+    ? perspective.split('bundle.').at(1)
+    : undefined
+
+  const editState = useEditState(documentId, documentType, 'default', bundlePerspective)
   const schema = useSchema()
   const {t} = useTranslation(structureLocaleNamespace)
   const isNewDocument = !editState?.published && !editState?.draft
-  const documentValue = editState?.draft || editState?.published
+  const documentValue = editState?.version || editState?.draft || editState?.published
   const schemaType = schema.get(documentType) as ObjectSchemaType | undefined
 
   const {value, isLoading: previewValueIsLoading} = useValuePreview({
diff --git a/packages/sanity/src/structure/components/structureTool/StructureTool.tsx b/packages/sanity/src/structure/components/structureTool/StructureTool.tsx
index c695d592000..1cf12900c8b 100644
--- a/packages/sanity/src/structure/components/structureTool/StructureTool.tsx
+++ b/packages/sanity/src/structure/components/structureTool/StructureTool.tsx
@@ -135,8 +135,8 @@ export const StructureTool = memo(function StructureTool({onPaneChange}: Structu
           <LoadingPane paneKey="intent-resolver" />
         )}
       </StyledPaneLayout>
-      <StructureTitle resolvedPanes={resolvedPanes} />
       <div data-portal="" ref={setPortalElement} />
+      <StructureTitle resolvedPanes={resolvedPanes} />
     </PortalProvider>
   )
 })
diff --git a/packages/sanity/src/structure/documentActions/DeleteAction.tsx b/packages/sanity/src/structure/documentActions/DeleteAction.tsx
index 2b25570dbc6..fb65cdd9a93 100644
--- a/packages/sanity/src/structure/documentActions/DeleteAction.tsx
+++ b/packages/sanity/src/structure/documentActions/DeleteAction.tsx
@@ -21,9 +21,16 @@ const DISABLED_REASON_TITLE_KEY = {
 }
 
 /** @internal */
-export const DeleteAction: DocumentActionComponent = ({id, type, draft, onComplete}) => {
+export const DeleteAction: DocumentActionComponent = ({
+  id,
+  type,
+  draft,
+  onComplete,
+  bundleSlug,
+}) => {
   const {setIsDeleting: paneSetIsDeleting} = useDocumentPane()
-  const {delete: deleteOp} = useDocumentOperation(id, type)
+
+  const {delete: deleteOp} = useDocumentOperation(id, type, bundleSlug)
   const [isDeleting, setIsDeleting] = useState(false)
   const [isConfirmDialogOpen, setConfirmDialogOpen] = useState(false)
 
@@ -49,6 +56,7 @@ export const DeleteAction: DocumentActionComponent = ({id, type, draft, onComple
   const [permissions, isPermissionsLoading] = useDocumentPairPermissions({
     id,
     type,
+    version: bundleSlug,
     permission: 'delete',
   })
 
diff --git a/packages/sanity/src/structure/documentActions/DiscardChangesAction.tsx b/packages/sanity/src/structure/documentActions/DiscardChangesAction.tsx
index 21a7524c1b4..3c17ce9322d 100644
--- a/packages/sanity/src/structure/documentActions/DiscardChangesAction.tsx
+++ b/packages/sanity/src/structure/documentActions/DiscardChangesAction.tsx
@@ -27,12 +27,14 @@ export const DiscardChangesAction: DocumentActionComponent = ({
   published,
   liveEdit,
   onComplete,
+  bundleSlug,
 }) => {
-  const {discardChanges} = useDocumentOperation(id, type)
+  const {discardChanges} = useDocumentOperation(id, type, bundleSlug)
   const [isConfirmDialogOpen, setConfirmDialogOpen] = useState(false)
   const [permissions, isPermissionsLoading] = useDocumentPairPermissions({
     id,
     type,
+    version: bundleSlug,
     permission: 'discardDraft',
   })
   const currentUser = useCurrentUser()
diff --git a/packages/sanity/src/structure/documentActions/DuplicateAction.tsx b/packages/sanity/src/structure/documentActions/DuplicateAction.tsx
index b3f30937d8a..040a8c1ebbc 100644
--- a/packages/sanity/src/structure/documentActions/DuplicateAction.tsx
+++ b/packages/sanity/src/structure/documentActions/DuplicateAction.tsx
@@ -21,14 +21,16 @@ const DISABLED_REASON_KEY = {
 }
 
 /** @internal */
-export const DuplicateAction: DocumentActionComponent = ({id, type, onComplete}) => {
+export const DuplicateAction: DocumentActionComponent = ({id, type, onComplete, bundleSlug}) => {
   const documentStore = useDocumentStore()
-  const {duplicate} = useDocumentOperation(id, type)
+  const {duplicate} = useDocumentOperation(id, type, bundleSlug)
   const {navigateIntent} = useRouter()
   const [isDuplicating, setDuplicating] = useState(false)
+
   const [permissions, isPermissionsLoading] = useDocumentPairPermissions({
     id,
     type,
+    version: bundleSlug,
     permission: 'duplicate',
   })
 
diff --git a/packages/sanity/src/structure/documentActions/HistoryRestoreAction.tsx b/packages/sanity/src/structure/documentActions/HistoryRestoreAction.tsx
index 7daf4f6848f..332666609c8 100644
--- a/packages/sanity/src/structure/documentActions/HistoryRestoreAction.tsx
+++ b/packages/sanity/src/structure/documentActions/HistoryRestoreAction.tsx
@@ -12,8 +12,14 @@ import {useRouter} from 'sanity/router'
 import {structureLocaleNamespace} from '../i18n'
 
 /** @internal */
-export const HistoryRestoreAction: DocumentActionComponent = ({id, type, revision, onComplete}) => {
-  const {restore} = useDocumentOperation(id, type)
+export const HistoryRestoreAction: DocumentActionComponent = ({
+  id,
+  type,
+  revision,
+  onComplete,
+  bundleSlug,
+}) => {
+  const {restore} = useDocumentOperation(id, type, bundleSlug)
   const event = useDocumentOperationEvent(id, type)
   const {navigateIntent} = useRouter()
   const prevEvent = useRef(event)
diff --git a/packages/sanity/src/structure/documentActions/PublishAction.tsx b/packages/sanity/src/structure/documentActions/PublishAction.tsx
index 83df6415c5f..e4fea76ea59 100644
--- a/packages/sanity/src/structure/documentActions/PublishAction.tsx
+++ b/packages/sanity/src/structure/documentActions/PublishAction.tsx
@@ -44,16 +44,21 @@ function AlreadyPublished({publishedAt}: {publishedAt: string}) {
   return <span>{t('action.publish.already-published.tooltip', {timeSincePublished})}</span>
 }
 
-/** @internal */
+/**
+ * TODO: Verify how this should work with versions, if it's needed at all.
+ *
+ *@internal
+ */
 // eslint-disable-next-line complexity
 export const PublishAction: DocumentActionComponent = (props) => {
-  const {id, type, liveEdit, draft, published} = props
+  const {id, type, liveEdit, draft, published, bundleSlug} = props
   const [publishState, setPublishState] = useState<'publishing' | 'published' | null>(null)
-  const {publish} = useDocumentOperation(id, type)
-  const validationStatus = useValidationStatus(id, type)
-  const syncState = useSyncState(id, type)
+  const {publish} = useDocumentOperation(id, type, bundleSlug)
+  const validationStatus = useValidationStatus(id, type, bundleSlug)
+  const syncState = useSyncState(id, type, {version: bundleSlug})
   const {changesOpen, onHistoryOpen, documentId, documentType} = useDocumentPane()
-  const editState = useEditState(documentId, documentType)
+
+  const editState = useEditState(documentId, documentType, 'default', bundleSlug)
   const {t} = useTranslation(structureLocaleNamespace)
 
   const revision = (editState?.draft || editState?.published || {})._rev
@@ -66,6 +71,7 @@ export const PublishAction: DocumentActionComponent = (props) => {
   const [permissions, isPermissionsLoading] = useDocumentPairPermissions({
     id,
     type,
+    version: bundleSlug,
     permission: 'publish',
   })
 
@@ -153,6 +159,7 @@ export const PublishAction: DocumentActionComponent = (props) => {
   ])
 
   return useMemo(() => {
+    // TODO: Check whether live edit is enabled because we're editing a version.
     if (liveEdit) {
       return {
         tone: 'default',
diff --git a/packages/sanity/src/structure/documentActions/UnpublishAction.tsx b/packages/sanity/src/structure/documentActions/UnpublishAction.tsx
index d9230db5b8a..6a7201fdd38 100644
--- a/packages/sanity/src/structure/documentActions/UnpublishAction.tsx
+++ b/packages/sanity/src/structure/documentActions/UnpublishAction.tsx
@@ -26,12 +26,14 @@ export const UnpublishAction: DocumentActionComponent = ({
   draft,
   onComplete,
   liveEdit,
+  bundleSlug,
 }) => {
-  const {unpublish} = useDocumentOperation(id, type)
+  const {unpublish} = useDocumentOperation(id, type, bundleSlug)
   const [isConfirmDialogOpen, setConfirmDialogOpen] = useState(false)
   const [permissions, isPermissionsLoading] = useDocumentPairPermissions({
     id,
     type,
+    version: bundleSlug,
     permission: 'unpublish',
   })
   const currentUser = useCurrentUser()
diff --git a/packages/sanity/src/structure/documentBadges/LiveEditBadge.ts b/packages/sanity/src/structure/documentBadges/LiveEditBadge.ts
index 78ccc9e2aa0..4051dc1b174 100644
--- a/packages/sanity/src/structure/documentBadges/LiveEditBadge.ts
+++ b/packages/sanity/src/structure/documentBadges/LiveEditBadge.ts
@@ -2,9 +2,10 @@ import {type DocumentBadgeComponent} from 'sanity'
 
 /** @internal */
 export const LiveEditBadge: DocumentBadgeComponent = (props) => {
-  const {liveEdit} = props
+  const {liveEditSchemaType} = props
 
-  if (liveEdit) {
+  if (liveEditSchemaType) {
+    // TODO: i18n.
     return {
       label: 'Live',
       color: 'danger',
diff --git a/packages/sanity/src/structure/panes/StructureToolPane.tsx b/packages/sanity/src/structure/panes/StructureToolPane.tsx
index fd47373c13b..053a76e8755 100644
--- a/packages/sanity/src/structure/panes/StructureToolPane.tsx
+++ b/packages/sanity/src/structure/panes/StructureToolPane.tsx
@@ -1,7 +1,9 @@
 import {isEqual} from 'lodash'
 import {lazy, memo, Suspense} from 'react'
+import {useRouter} from 'sanity/router'
 
 import {PaneRouterProvider} from '../components/paneRouter'
+import {useResolvedPanes} from '../structureResolvers'
 import {type PaneNode} from '../types'
 import {LoadingPane} from './loading'
 import {UnknownPane} from './unknown'
@@ -14,7 +16,7 @@ interface StructureToolPaneProps {
   itemId: string
   pane: PaneNode
   paneKey: string
-  params: Record<string, string | undefined>
+  params: Record<string, string | undefined> & {perspective?: string}
   payload: unknown
   path: string
   selected: boolean
@@ -52,6 +54,8 @@ export const StructureToolPane = memo(
     } = props
 
     const PaneComponent = paneMap[pane.type] || UnknownPane
+    const {stickyParams} = useRouter()
+    const {resolvedPanes} = useResolvedPanes()
 
     return (
       <PaneRouterProvider
@@ -60,6 +64,7 @@ export const StructureToolPane = memo(
         params={params}
         payload={payload}
         siblingIndex={siblingIndex}
+        perspective={params.perspective ?? stickyParams.perspective}
       >
         <Suspense fallback={<LoadingPane paneKey={paneKey} path={path} selected={selected} />}>
           <PaneComponent
diff --git a/packages/sanity/src/structure/panes/document/DocumentPaneContext.ts b/packages/sanity/src/structure/panes/document/DocumentPaneContext.ts
index 60af9c05af8..0c986608db5 100644
--- a/packages/sanity/src/structure/panes/document/DocumentPaneContext.ts
+++ b/packages/sanity/src/structure/panes/document/DocumentPaneContext.ts
@@ -41,6 +41,10 @@ export interface DocumentPaneContextValue {
   documentType: string
   documentVersions: BundleDocument[] | null
   editState: EditStateFor | null
+  /**
+   * Whether the document being edited exists in the checked-out bundle.
+   */
+  existsInBundle: boolean
   fieldActions: DocumentFieldAction[]
   focusPath: Path
   index: number
@@ -76,6 +80,7 @@ export interface DocumentPaneContextValue {
   title: string | null
   validation: ValidationMarker[]
   value: SanityDocumentLike
+  version?: string
   views: View[]
   formState: DocumentFormNode | null
   permissions?: PermissionCheckResult | null
diff --git a/packages/sanity/src/structure/panes/document/DocumentPaneProvider.tsx b/packages/sanity/src/structure/panes/document/DocumentPaneProvider.tsx
index 55e3baa1526..d894eb48f32 100644
--- a/packages/sanity/src/structure/panes/document/DocumentPaneProvider.tsx
+++ b/packages/sanity/src/structure/panes/document/DocumentPaneProvider.tsx
@@ -43,7 +43,6 @@ import {
   useValidationStatus,
 } from 'sanity'
 import {DocumentPaneContext} from 'sanity/_singletons'
-import {useRouter} from 'sanity/router'
 
 import {usePaneRouter} from '../../components'
 import {structureLocaleNamespace} from '../../i18n'
@@ -79,7 +78,6 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
       inspectors: inspectorsResolver,
     },
   } = useSource()
-  const {stickyParams} = useRouter()
   const presenceStore = usePresenceStore()
   const paneRouter = usePaneRouter()
   const setPaneParams = paneRouter.setParams
@@ -115,20 +113,37 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
     templateParams,
   })
 
+  const {perspective} = paneRouter
+
+  const bundlePerspective = perspective?.startsWith('bundle.')
+    ? perspective.split('bundle.').at(1)
+    : undefined
+
   const initialValue = useUnique(initialValueRaw)
-  const {patch} = useDocumentOperation(documentId, documentType)
+  const {patch} = useDocumentOperation(documentId, documentType, bundlePerspective)
   const schemaType = schema.get(documentType) as ObjectSchemaType | undefined
-  const editState = useEditState(documentId, documentType)
-  const {validation: validationRaw} = useValidationStatus(documentId, documentType)
-  const connectionState = useConnectionState(documentId, documentType)
+  const editState = useEditState(documentId, documentType, 'default', bundlePerspective)
+  const {validation: validationRaw} = useValidationStatus(
+    documentId,
+    documentType,
+    bundlePerspective,
+  )
+  const connectionState = useConnectionState(documentId, documentType, {version: bundlePerspective})
   const {data: documentVersions} = useDocumentVersions({documentId})
 
-  const perspective = stickyParams.perspective
+  let value: SanityDocumentLike = initialValue.value
+
+  switch (true) {
+    case typeof bundlePerspective !== 'undefined':
+      value = editState.version || editState.draft || value
+      break
+    case perspective === 'published':
+      value = editState.published || editState.draft || value
+      break
+    default:
+      value = editState?.draft || editState?.published || value
+  }
 
-  const value: SanityDocumentLike =
-    (perspective === 'published'
-      ? editState.published || editState.draft
-      : editState?.draft || editState?.published) || initialValue.value
   const [isDeleting, setIsDeleting] = useState(false)
 
   // Resolve document actions
@@ -172,6 +187,7 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
     onError: setTimelineError,
     rev: params.rev,
     since: params.since,
+    version: bundlePerspective,
   })
 
   // Subscribe to external timeline state changes
@@ -207,13 +223,13 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
 
   const [presence, setPresence] = useState<DocumentPresence[]>([])
   useEffect(() => {
-    const subscription = presenceStore.documentPresence(documentId).subscribe((nextPresence) => {
+    const subscription = presenceStore.documentPresence(value._id).subscribe((nextPresence) => {
       setPresence(nextPresence)
     })
     return () => {
       subscription.unsubscribe()
     }
-  }, [documentId, presenceStore])
+  }, [presenceStore, value._id])
 
   const inspectors: DocumentInspector[] = useMemo(
     () => inspectorsResolver({documentId, documentType}),
@@ -495,6 +511,10 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
   })
 
   const isNonExistent = !value?._id
+  const isNonExistentInBundle =
+    typeof bundlePerspective !== 'undefined' && !value._id.startsWith(`${bundlePerspective}.`)
+  const existsInBundle =
+    typeof bundlePerspective !== 'undefined' && value._id.startsWith(`${bundlePerspective}.`)
 
   const readOnly = useMemo(() => {
     const hasNoPermission = !isPermissionsLoading && !permissions?.granted
@@ -502,9 +522,11 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
     const createActionDisabled = isNonExistent && !isActionEnabled(schemaType!, 'create')
     const reconnecting = connectionState === 'reconnecting'
     const isLocked = editState.transactionSyncLock?.enabled
+    const isSystemPerspectiveApplied = perspective && typeof bundlePerspective === 'undefined'
 
     return (
-      !!perspective ||
+      isNonExistentInBundle ||
+      isSystemPerspectiveApplied ||
       !ready ||
       revTime !== null ||
       hasNoPermission ||
@@ -520,9 +542,11 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
     permissions?.granted,
     schemaType,
     isNonExistent,
+    isNonExistentInBundle,
     connectionState,
     editState.transactionSyncLock?.enabled,
     perspective,
+    bundlePerspective,
     ready,
     revTime,
     isDeleting,
@@ -579,14 +603,14 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
       presenceStore.setLocation([
         {
           type: 'document',
-          documentId,
+          documentId: value._id,
           path: nextFocusPath,
           lastActiveAt: new Date().toISOString(),
           selection: payload?.selection,
         },
       ])
     },
-    [documentId, presenceStore],
+    [presenceStore, value._id],
   )
 
   const updatePresenceThrottled = useMemo(
@@ -624,6 +648,7 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
       documentType,
       documentVersions,
       editState,
+      existsInBundle,
       fieldActions,
       focusPath,
       inspector: currentInspector || null,
@@ -664,6 +689,7 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
       timelineStore,
       title,
       value,
+      version: bundlePerspective,
       views,
       formState,
       unstable_languageFilter: languageFilter,
@@ -673,6 +699,7 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
       actions,
       activeViewId,
       badges,
+      bundlePerspective,
       changesOpen,
       closeInspector,
       collapsedFieldSets,
@@ -686,6 +713,7 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
       documentType,
       documentVersions,
       editState,
+      existsInBundle,
       fieldActions,
       focusPath,
       formState,
diff --git a/packages/sanity/src/structure/panes/document/comments/CommentsWrapper.tsx b/packages/sanity/src/structure/panes/document/comments/CommentsWrapper.tsx
index fcd234329a1..88fb450afd7 100644
--- a/packages/sanity/src/structure/panes/document/comments/CommentsWrapper.tsx
+++ b/packages/sanity/src/structure/panes/document/comments/CommentsWrapper.tsx
@@ -5,6 +5,7 @@ import {
   CommentsProvider,
   useCommentsEnabled,
 } from 'sanity'
+import {useRouter} from 'sanity/router'
 
 import {usePaneRouter} from '../../../components'
 import {useDocumentPane} from '../useDocumentPane'
@@ -37,7 +38,9 @@ function CommentsProviderWrapper(props: CommentsWrapperProps) {
 
   const {enabled} = useCommentsEnabled()
   const {connectionState, onPathOpen, inspector, openInspector} = useDocumentPane()
-  const {params, setParams, createPathWithParams} = usePaneRouter()
+  const router = useRouter()
+  const {params, setParams, createPathWithParams, ...paneRouter} = usePaneRouter()
+  const perspective = paneRouter.perspective ?? router.stickyParams.perspective
 
   const selectedCommentId = params?.comment
   const paramsRef = useRef(params)
@@ -90,6 +93,7 @@ function CommentsProviderWrapper(props: CommentsWrapperProps) {
       selectedCommentId={selectedCommentId}
       sortOrder="desc"
       type="field"
+      perspective={perspective}
     >
       {children}
     </CommentsProvider>
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 ad0ed930a5c..07dbd1b2afb 100644
--- a/packages/sanity/src/structure/panes/document/documentPanel/banners/ReferenceChangedBanner.tsx
+++ b/packages/sanity/src/structure/panes/document/documentPanel/banners/ReferenceChangedBanner.tsx
@@ -21,14 +21,19 @@ import {Banner} from './Banner'
 interface ParentReferenceInfo {
   loading: boolean
   result?: {
-    availability: {draft: DocumentAvailability; published: DocumentAvailability}
+    availability: {
+      draft: DocumentAvailability
+      published: DocumentAvailability
+      version?: DocumentAvailability
+    }
     refValue: string | undefined
   }
 }
 
 export const ReferenceChangedBanner = memo(() => {
   const documentPreviewStore = useDocumentPreviewStore()
-  const {params, groupIndex, routerPanesState, replaceCurrent, BackLink} = usePaneRouter()
+  const {params, groupIndex, routerPanesState, replaceCurrent, BackLink, perspective} =
+    usePaneRouter()
   const routerReferenceId = routerPanesState[groupIndex]?.[0].id
   const parentGroup = routerPanesState[groupIndex - 1] as RouterPaneGroup | undefined
   const parentSibling = parentGroup?.[0]
@@ -39,6 +44,10 @@ export const ReferenceChangedBanner = memo(() => {
   }, [params?.parentRefPath])
   const {t} = useTranslation(structureLocaleNamespace)
 
+  const bundlePerspective = perspective?.startsWith('bundle.')
+    ? perspective.split('bundle.').at(1)
+    : undefined
+
   /**
    * Loads information regarding the reference field of the parent pane. This
    * is only applicable to child references (aka references-in-place).
@@ -74,6 +83,9 @@ export const ReferenceChangedBanner = memo(() => {
         .unstable_observePathsDocumentPair(
           publishedId,
           (keyedSegmentIndex === -1 ? path : path.slice(0, keyedSegmentIndex)) as string[][],
+          {
+            version: bundlePerspective,
+          },
         )
         .pipe(
           // this debounce time is needed to prevent flashing banners due to
@@ -82,21 +94,28 @@ export const ReferenceChangedBanner = memo(() => {
           // initially could be stale.
           debounceTime(750),
           map(
-            ({draft, published}): ParentReferenceInfo => ({
+            ({draft, published, version}): ParentReferenceInfo => ({
               loading: false,
               result: {
                 availability: {
                   draft: draft.availability,
                   published: published.availability,
+                  ...(version?.availability
+                    ? {
+                        version: version.availability,
+                      }
+                    : {}),
                 },
-                refValue: pathGet<Reference>(draft.snapshot || published.snapshot, parentRefPath)
-                  ?._ref,
+                refValue: pathGet<Reference>(
+                  version?.snapshot || draft.snapshot || published.snapshot,
+                  parentRefPath,
+                )?._ref,
               },
             }),
           ),
         ),
     )
-  }, [documentPreviewStore, parentId, parentRefPath])
+  }, [bundlePerspective, documentPreviewStore, parentId, parentRefPath])
   const referenceInfo = useObservable(referenceInfoObservable, {loading: true})
 
   const handleReloadReference = useCallback(() => {
diff --git a/packages/sanity/src/structure/panes/document/documentPanel/documentViews/FormView.tsx b/packages/sanity/src/structure/panes/document/documentPanel/documentViews/FormView.tsx
index 6868e2c4166..fbdd62114e1 100644
--- a/packages/sanity/src/structure/panes/document/documentPanel/documentViews/FormView.tsx
+++ b/packages/sanity/src/structure/panes/document/documentPanel/documentViews/FormView.tsx
@@ -40,6 +40,7 @@ export const FormView = forwardRef<HTMLDivElement, FormViewProps>(function FormV
     editState,
     documentId,
     documentType,
+    version,
     fieldActions,
     onChange,
     validation,
@@ -77,7 +78,7 @@ export const FormView = forwardRef<HTMLDivElement, FormViewProps>(function FormV
 
   useEffect(() => {
     const sub = documentStore.pair
-      .documentEvents(documentId, documentType)
+      .documentEvents(documentId, documentType, version)
       .pipe(
         tap((event) => {
           if (event.type === 'mutation') {
@@ -94,7 +95,7 @@ export const FormView = forwardRef<HTMLDivElement, FormViewProps>(function FormV
     return () => {
       sub.unsubscribe()
     }
-  }, [documentId, documentStore, documentType, patchChannel])
+  }, [documentId, documentStore, documentType, patchChannel, version])
 
   const hasRev = Boolean(value?._rev)
   useEffect(() => {
@@ -207,6 +208,7 @@ export const FormView = forwardRef<HTMLDivElement, FormViewProps>(function FormV
                   // but these should be compatible
                   formState.value as FormDocumentValue
                 }
+                version={version}
               />
             </>
           )}
diff --git a/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentHeaderTitle.tsx b/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentHeaderTitle.tsx
index 701845754ae..9a48a11e5ff 100644
--- a/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentHeaderTitle.tsx
+++ b/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentHeaderTitle.tsx
@@ -59,7 +59,7 @@ export function DocumentHeaderTitle(): ReactElement {
       </Flex>
 
       <Flex flex="none" gap={1}>
-        <DocumentPerspectiveMenu documentId={documentId} />
+        <DocumentPerspectiveMenu />
       </Flex>
     </Flex>
   )
diff --git a/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveMenu.tsx b/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveMenu.tsx
index 59fbb4cf51e..e853a10326c 100644
--- a/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveMenu.tsx
+++ b/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveMenu.tsx
@@ -1,23 +1,23 @@
 import {ChevronDownIcon} from '@sanity/icons'
 import {Box, Button} from '@sanity/ui'
 import {useCallback} from 'react'
-import {BundleBadge, BundleMenu, getBundleSlug, usePerspective} from 'sanity'
+import {BundleBadge, BundleMenu, usePerspective} from 'sanity'
 import {useRouter} from 'sanity/router'
 import {styled} from 'styled-components'
 
+import {usePaneRouter} from '../../../../../components'
 import {useDocumentPane} from '../../../useDocumentPane'
 
 const BadgeButton = styled(Button)({
   cursor: 'pointer',
 })
 
-export function DocumentPerspectiveMenu(props: {documentId: string}): JSX.Element {
-  const {documentId} = props
-  const {currentGlobalBundle} = usePerspective()
+export function DocumentPerspectiveMenu(): JSX.Element {
+  const paneRouter = usePaneRouter()
+  const {currentGlobalBundle} = usePerspective(paneRouter.perspective)
 
-  const existsInBundle = getBundleSlug(documentId) === currentGlobalBundle?.slug
+  const {documentVersions, existsInBundle} = useDocumentPane()
   const {title, hue, icon, slug} = currentGlobalBundle
-  const {documentVersions} = useDocumentPane()
 
   const router = useRouter()
 
@@ -46,6 +46,7 @@ export function DocumentPerspectiveMenu(props: {documentId: string}): JSX.Elemen
           button={<Button icon={ChevronDownIcon} mode="bleed" padding={2} space={2} />}
           bundles={documentVersions}
           loading={!documentVersions}
+          perspective={paneRouter.perspective}
         />
       </Box>
     </>
diff --git a/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/__tests__/DocumentPerspectiveMenu.test.tsx b/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/__tests__/DocumentPerspectiveMenu.test.tsx
index 2f59b9d756e..167438243f4 100644
--- a/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/__tests__/DocumentPerspectiveMenu.test.tsx
+++ b/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/__tests__/DocumentPerspectiveMenu.test.tsx
@@ -24,6 +24,7 @@ jest.mock('sanity', () => {
 jest.mock('sanity/router', () => ({
   useRouter: jest.fn().mockReturnValue({
     navigateIntent: jest.fn(),
+    stickyParams: {},
   }),
   route: {
     create: jest.fn(),
@@ -63,13 +64,13 @@ describe('DocumentPerspectiveMenu', () => {
       setPerspective: jest.fn(),
     })
 
-    mockUseDocumentPane.mockImplementationOnce(() => ({
+    mockUseDocumentPane.mockReturnValue({
       documentVersions: [],
-    }))
+    })
   })
 
   it('should render the bundle badge if the document exists in the global bundle', async () => {
-    mockUseDocumentPane.mockImplementationOnce(() => ({
+    mockUseDocumentPane.mockReturnValue({
       documentVersions: [
         {
           slug: 'spring-drop',
@@ -84,23 +85,28 @@ describe('DocumentPerspectiveMenu', () => {
           _rev: '',
         },
       ],
-    }))
+      existsInBundle: true,
+    })
 
     const wrapper = await createTestProvider()
-    render(<DocumentPerspectiveMenu documentId="spring-drop.document-id" />, {wrapper})
+    render(<DocumentPerspectiveMenu />, {wrapper})
 
     expect(screen.getByTestId('button-document-release')).toBeInTheDocument()
   })
 
   it('should not render the bundle badge if the document does not exist in the bundle', async () => {
+    mockUseDocumentPane.mockReturnValue({
+      existsInBundle: false,
+    })
+
     const wrapper = await createTestProvider()
-    render(<DocumentPerspectiveMenu documentId="document-id" />, {wrapper})
+    render(<DocumentPerspectiveMenu />, {wrapper})
 
     expect(screen.queryByTestId('button-document-release')).toBeNull()
   })
 
   it('should navigate to the release intent when the bundle badge is clicked', async () => {
-    mockUseDocumentPane.mockImplementationOnce(() => ({
+    mockUseDocumentPane.mockReturnValue({
       documentVersions: [
         {
           slug: 'spring-drop',
@@ -115,10 +121,11 @@ describe('DocumentPerspectiveMenu', () => {
           _rev: '',
         },
       ],
-    }))
+      existsInBundle: true,
+    })
 
     const wrapper = await createTestProvider()
-    render(<DocumentPerspectiveMenu documentId="spring-drop.document-1" />, {wrapper})
+    render(<DocumentPerspectiveMenu />, {wrapper})
 
     expect(screen.queryByTestId('button-document-release')).toBeInTheDocument()
     fireEvent.click(screen.getByTestId('button-document-release'))
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 591ed41cad6..12f25645662 100644
--- a/packages/sanity/src/structure/panes/document/inspectors/validation/index.ts
+++ b/packages/sanity/src/structure/panes/document/inspectors/validation/index.ts
@@ -10,6 +10,7 @@ import {
   useTranslation,
   useValidationStatus,
 } from 'sanity'
+import {usePaneRouter} from 'sanity/structure'
 
 import {VALIDATION_INSPECTOR_NAME} from '../../constants'
 import {ValidationInspector} from './ValidationInspector'
@@ -17,7 +18,17 @@ import {ValidationInspector} from './ValidationInspector'
 function useMenuItem(props: DocumentInspectorUseMenuItemProps): DocumentInspectorMenuItem {
   const {documentId, documentType} = props
   const {t} = useTranslation('validation')
-  const {validation: validationMarkers} = useValidationStatus(documentId, documentType)
+  const paneRouter = usePaneRouter()
+
+  const bundlePerspective = paneRouter.perspective?.startsWith('bundle.')
+    ? paneRouter.perspective.split('bundle.').at(1)
+    : undefined
+
+  const {validation: validationMarkers} = useValidationStatus(
+    documentId,
+    documentType,
+    bundlePerspective,
+  )
 
   const validation: FormNodeValidation[] = useMemo(
     () =>
diff --git a/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusBarActions.tsx b/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusBarActions.tsx
index 651b99c3618..7d4bc68a504 100644
--- a/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusBarActions.tsx
+++ b/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusBarActions.tsx
@@ -6,6 +6,7 @@ import {
   BundleActions,
   type DocumentActionComponent,
   type DocumentActionDescription,
+  type DocumentActionProps,
   isBundleDocument,
   LATEST,
   shouldArrayDialogOpen,
@@ -15,7 +16,7 @@ import {
 } from 'sanity'
 
 import {Button, Tooltip} from '../../../../ui-components'
-import {RenderActionCollectionState} from '../../../components'
+import {RenderActionCollectionState, usePaneRouter} from '../../../components'
 import {HistoryRestoreAction} from '../../../documentActions'
 import {useDocumentPane} from '../useDocumentPane'
 import {ActionMenuButton} from './ActionMenuButton'
@@ -25,15 +26,17 @@ interface DocumentStatusBarActionsInnerProps {
   disabled: boolean
   showMenu: boolean
   states: DocumentActionDescription[]
+  actionProps?: Omit<DocumentActionProps, 'onComplete'> | null
 }
 
 const DocumentStatusBarActionsInner = memo(function DocumentStatusBarActionsInner(
   props: DocumentStatusBarActionsInnerProps,
 ) {
-  const {disabled, showMenu, states} = props
+  const {disabled, showMenu, states, actionProps} = props
   const {__internal_tasks, schemaType, openPath, documentId, documentType, documentVersions} =
     useDocumentPane()
 
+  const paneRouter = usePaneRouter()
   const [firstActionState, ...menuActionStates] = states
   const [buttonElement, setButtonElement] = useState<HTMLButtonElement | null>(null)
   const isTreeArrayEditingEnabled = useSource().beta?.treeArrayEditing?.enabled
@@ -69,7 +72,7 @@ const DocumentStatusBarActionsInner = memo(function DocumentStatusBarActionsInne
   /* Version / Bundling handling */
 
   // TODO MAKE SURE THIS IS HOW WE WANT TO DO THIS
-  const {currentGlobalBundle} = usePerspective()
+  const {currentGlobalBundle} = usePerspective(paneRouter.perspective)
 
   return (
     <Flex align="center" gap={1}>
@@ -101,6 +104,7 @@ const DocumentStatusBarActionsInner = memo(function DocumentStatusBarActionsInne
                         documentId={documentId}
                         documentType={documentType}
                         documentVersions={documentVersions}
+                        {...actionProps}
                       />
                     ) : (
                       <div>
@@ -156,9 +160,10 @@ export const DocumentStatusBarActions = memo(function DocumentStatusBarActions()
         states={states}
         // Use document ID as key to make sure that the actions state is reset when the document changes
         key={documentId}
+        actionProps={editState}
       />
     ),
-    [actions.length, connectionState, documentId],
+    [actions.length, connectionState, documentId, editState],
   )
 
   if (actions.length === 0 || !editState) {
diff --git a/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusLine.tsx b/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusLine.tsx
index 586b5bb6ab1..1a475f58030 100644
--- a/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusLine.tsx
+++ b/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusLine.tsx
@@ -18,7 +18,7 @@ export function DocumentStatusLine({singleLine}: DocumentStatusLineProps) {
 
   const [status, setStatus] = useState<'saved' | 'syncing' | null>(null)
 
-  const syncState = useSyncState(documentId, documentType)
+  const syncState = useSyncState(documentId, documentType, {version: editState?.bundleSlug})
 
   const lastUpdated = value?._updatedAt
 
diff --git a/packages/sanity/src/structure/structureResolvers/useResolvedPanes.ts b/packages/sanity/src/structure/structureResolvers/useResolvedPanes.ts
index eb9acadf64d..b13ca6f82a5 100644
--- a/packages/sanity/src/structure/structureResolvers/useResolvedPanes.ts
+++ b/packages/sanity/src/structure/structureResolvers/useResolvedPanes.ts
@@ -16,7 +16,7 @@ interface PaneData {
   itemId: string
   key: string
   pane: PaneNode | typeof LOADING_PANE
-  params: Record<string, string | undefined>
+  params: Record<string, string | undefined> & {perspective?: string}
   path: string
   payload: unknown
   selected: boolean
diff --git a/packages/sanity/test/testUtils/TestProvider.tsx b/packages/sanity/test/testUtils/TestProvider.tsx
index 9fba0d5268f..0397bf94825 100644
--- a/packages/sanity/test/testUtils/TestProvider.tsx
+++ b/packages/sanity/test/testUtils/TestProvider.tsx
@@ -1,5 +1,6 @@
 import {type SanityClient} from '@sanity/client'
 import {LayerProvider, studioTheme, ThemeProvider, ToastProvider} from '@sanity/ui'
+import {noop} from 'lodash'
 import {type ReactNode} from 'react'
 
 import {
@@ -14,6 +15,7 @@ import {
 } from '../../src/core'
 import {studioDefaultLocaleResources} from '../../src/core/i18n/bundles/studio'
 import {prepareI18n} from '../../src/core/i18n/i18nConfig'
+import {route, RouterProvider} from '../../src/router'
 import {getMockWorkspace} from './getMockWorkspaceFromConfig'
 
 export interface TestProviderOptions {
@@ -37,25 +39,29 @@ export async function createTestProvider({
     i18n: {bundles: resources},
   })
 
+  const router = route.create('/')
+
   await i18next.init()
 
   function TestProvider({children}: {children: ReactNode}) {
     return (
-      <ThemeProvider theme={studioTheme}>
-        <LocaleProviderBase locales={locales} i18next={i18next} projectId="test" sourceId="test">
-          <ToastProvider>
-            <LayerProvider>
-              <WorkspaceProvider workspace={workspace}>
-                <SourceProvider source={workspace.unstable_sources[0]}>
-                  <ResourceCacheProvider>
-                    <AddonDatasetProvider>{children}</AddonDatasetProvider>
-                  </ResourceCacheProvider>
-                </SourceProvider>
-              </WorkspaceProvider>
-            </LayerProvider>
-          </ToastProvider>
-        </LocaleProviderBase>
-      </ThemeProvider>
+      <RouterProvider router={router} state={{}} onNavigate={noop}>
+        <ThemeProvider theme={studioTheme}>
+          <LocaleProviderBase locales={locales} i18next={i18next} projectId="test" sourceId="test">
+            <ToastProvider>
+              <LayerProvider>
+                <WorkspaceProvider workspace={workspace}>
+                  <SourceProvider source={workspace.unstable_sources[0]}>
+                    <ResourceCacheProvider>
+                      <AddonDatasetProvider>{children}</AddonDatasetProvider>
+                    </ResourceCacheProvider>
+                  </SourceProvider>
+                </WorkspaceProvider>
+              </LayerProvider>
+            </ToastProvider>
+          </LocaleProviderBase>
+        </ThemeProvider>
+      </RouterProvider>
     )
   }