import React, { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { RootState } from '../../../store/store' import api from '../../../config/api'; import TagProperties, { MASKTYPE, TEXT } from './tagProperties/TagProperties'; import { DroppedItem, TagsTypes } from '../../../utlity/types'; import { useNavigate, useParams } from 'react-router-dom'; import { fetchRecipients, toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE } from '../../../store/slices/selectedRecipientsSlice'; import { fetchDocuments } from '../../../store/slices/documentSlice'; import { FaQuestionCircle } from 'react-icons/fa'; import typeToIconMap from '../../../helper/iconMap'; import Loader from '../../dashboard/Loader'; import { hideLoader, showLoader } from '../../../store/slices/loaderSlice'; import DroppedItemsTagsCon from './DroppedItemsTagsCon'; import DocumentPages from './documentPages/DocumentPages'; import { initializeDroppedItems, setDroppedItems, clearTransactionTags, incrementCounter, } from '../../../store/slices/documentTagsSlice'; import { serializeGroupedFields } from '../../../helper/handleGroupedFields'; export interface DroppedItemsObj { [key: string]: DroppedItem[] } interface DroppedTagsDefaultFieldType { [key: string]: string } export const droppedTagsDefaultField: DroppedTagsDefaultFieldType = { text: "paragraph", temporal: "date", number: "integer", // image: "STAMP", select: "single select", checkbox: "TOGGLE" } interface DocumentContainerProps { isPages: boolean; setIsPages: React.Dispatch>; zoomScale: number; transactionSaving?: boolean; setTransactionSaving?: React.Dispatch>; isSend?: boolean | null; setIsSend?: React.Dispatch>; isPreview?: boolean; aId?: string; tId?: string; selectedRecipientIndx?: number; onHistoryChange?: (canUndo: boolean, canRedo: boolean) => void; } let copiedTags: DroppedItem[] = [] let selectedDoc: { [key: string]: string | null } = { key: null } const fetchedTags: string[] = [] // 👇 Create an interface for the exposed methods export interface DocumentContainerRef { handleTransactionProcess: (isSave?: boolean, isNavigate?: boolean, navigateTo?: string) => void; handleUndo: () => void; handleRedo: () => void; handleCopy: () => void; handlePaste: () => void; } const DocumentContainer = forwardRef( ({ isPages, zoomScale, transactionSaving, setTransactionSaving, isSend, isPreview = false, aId = null, tId = null, selectedRecipientIndx = 0, setIsPages, onHistoryChange }, ref) => { const EMPTY_DROPPED_ITEMS: DroppedItemsObj = {}; const scaleFactor: number = (zoomScale ?? 100) / 100; let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS; const dispatch = useDispatch(); const params = useParams<{ accountId?: string; transactionId?: string; templateId?: string }>(); let accountId = params.accountId; let transactionId = params.transactionId if (!transactionId) { transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES; transactionId = params.templateId; dispatch(toggleToTemplates()); } if (!accountId || !transactionId) { window.location.href = "/"; return null; } // If is preview is enable if (isPreview && aId != null && tId != null) { accountId = aId; transactionId = tId; } // const transactionId = useSelector((state: RootState) => state.transaction.transactionId) const navigate = useNavigate(); const childRef = useRef(null); // const tagUndoRef = useRef([]); const imageRefs = useRef<{ [key: string]: HTMLImageElement | null }>({}); const pageRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); const [activePage, setActivePage] = useState(null); // NEW const isProgrammaticScrollRef = useRef(false); // NEW — guards against feedback loop const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients); // const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments) const selectedDocuments = useSelector((state: RootState) => state.documents.documents) // Sync activePage with scroll position via IntersectionObserver useEffect(() => { const scrollRoot = document.getElementById('doc-con'); if (!scrollRoot) return; // Track intersection ratio per page so we can pick the most-visible one // when multiple pages are partially visible at once. const ratios: Record = {}; const observer = new IntersectionObserver( (entries) => { // Ignore intersection changes caused by our own programmatic // scroll (e.g. clicking a thumbnail) to avoid feedback loops. if (isProgrammaticScrollRef.current) return; entries.forEach((entry) => { const key = entry.target.getAttribute('data-page-key'); if (!key) return; ratios[key] = entry.isIntersecting ? entry.intersectionRatio : 0; }); // Pick the key with the highest intersection ratio let bestKey: string | null = null; let bestRatio = 0; for (const [key, ratio] of Object.entries(ratios)) { if (ratio > bestRatio) { bestRatio = ratio; bestKey = key; } } if (bestKey && bestRatio > 0) { setActivePage((prev) => (prev === bestKey ? prev : bestKey)); // dedupe, avoid re-render } }, { root: scrollRoot, // Multiple thresholds so ratio updates fire smoothly while // scrolling, without needing scroll-event listeners at all. threshold: [0, 0.25, 0.5, 0.75, 1], rootMargin: '0px', } ); Object.values(pageRefs.current).forEach((el) => { if (el) observer.observe(el); }); return () => observer.disconnect(); }, [selectedDocuments]); // handle Tag - Document Pos and size for Responsiveness - START const [imageSizes, setImageSizes] = useState>({}); useEffect(() => { const observers: ResizeObserver[] = []; Object.keys(imageRefs.current).forEach((key) => { const el = imageRefs.current[key]; if (!el) return; const observer = new ResizeObserver((entries) => { for (const entry of entries) { const { width, height } = entry.contentRect; setImageSizes((prev) => ({ ...prev, [key]: { width, height }, })); } }); observer.observe(el); observers.push(observer); }); return () => observers.forEach((o) => o.disconnect()); }, [selectedDocuments]); // handle Tag - Document Pos and size for Responsiveness - START useEffect(() => { if (selectedDocuments.length > 0 && selectedDocuments[0].pages?.length > 0) { const firstDoc = selectedDocuments[0]; const firstPage = firstDoc.pages[0]; if (!selectedDoc.key) { selectedDoc.key = `${firstDoc.id}_${firstPage.index}`; } } }, [selectedDocuments]); // Redux state for dropped items const transactionKey = useMemo( () => `${accountId}_${transactionId}`, [accountId, transactionId] ); const droppedItems = useSelector((state: RootState) => state.documentTags.items[transactionKey] ?? EMPTY_DROPPED_ITEMS); const droppedItemsCounter = useSelector((state: RootState) => state.documentTags.counter[transactionKey] || 0); interface HotKeysObj { isActive: boolean; key: string | null; tagId?: string | null; hotKey?: string | null; } const hotKeysObjInitial: HotKeysObj = { isActive: false, key: null, tagId: null, hotKey: null } // const [hotKeysObj, setHotKeysObj] = useState(hotKeysObjInitial); useEffect(() => { selectedRecipients.length == 0 && dispatch(fetchRecipients({ accountId, transactionId })); selectedDocuments.length == 0 && dispatch(fetchDocuments({ accountId, transactionId })); }, []) const fetchTags = async () => { try { if (Object.keys(droppedItems).length) return const requests = selectedRecipients.filter((recipient) => { // Adding new rec doesn't have id till we click on IoMdClose or save so removing them and adding rec after click then fetching tags if (!recipient.id) return false; // also checking if have id but tags already fetched so to avoid same req if (fetchedTags.includes(recipient.id)) return false; fetchedTags.push(recipient.id) return true }).map((recipient) => api.get(`/api/v1/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/recipients/${recipient.id}/tags`)); if (!requests.length) return const response = await Promise.all(requests); let recTags = {} for (const res of response) { if (!res && !res.data) return for (const tag of res.data) { const key = `${tag.documentId}_${tag.pageIndex}`; if (recTags[key]) { recTags[key].push(tag); } else { recTags[key] = [tag]; // Initialize with an array } } } // Dispatch to Redux instead of local state dispatch(initializeDroppedItems({ transactionKey, items: recTags })); // Handle your response here } catch (err) { console.error(err); } }; // FETCHING TAG - START useEffect(() => { if (selectedRecipients.length == 0) return; fetchTags(); }, [selectedRecipients, isPreview]); // FETCHING TAG - END const [selectedTag, setSelectedTag] = useState<{ key: string, index: number } | null>(null); // NAVIGATING to Dashboard if Transaction is not created - START useLayoutEffect(() => { if (transactionId) return; navigate("/"); }, [transactionId]) // NAVIGATING to Dashboard if Transaction is not created - END // const hotKeysObj = useRef(hotKeysObjInitial); // use ref, not state // const docKeyListenerRef = useRef<((e: KeyboardEvent) => void) | null>(null); const selectedTagRef = useRef<{ key: string; index: number } | null>(null); const droppedItemsRef = useRef(droppedItems); const historyRef = useRef([]); const cursorRef = useRef(-1); const isUndoRedoRef = useRef(false); // prevents history push during undo/redo replay const notifyHistoryChange = () => { onHistoryChange?.( cursorRef.current > 0, cursorRef.current < historyRef.current.length - 1 ); }; // Global keydown listener — works regardless of which element has focus useEffect(() => { const handler = (event: KeyboardEvent) => { // Don't intercept when user is typing in an input/textarea/contenteditable const target = event.target as HTMLElement; if ( target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable ) return; const ctrl = event.ctrlKey || event.metaKey; const shift = event.shiftKey; const pressedKey = event.key.toLowerCase(); // Only intercept known hotkeys to avoid blocking browser shortcuts const isKnownHotkey = (ctrl && ['c', 'v', 'a', 'z', 'y'].includes(pressedKey)) || ['delete', 'backspace'].includes(pressedKey); if (!isKnownHotkey) return; event.preventDefault(); let hotKey = pressedKey; if (ctrl && shift) hotKey = 'Ctrl+Shift+' + pressedKey; else if (ctrl) hotKey = 'Ctrl+' + pressedKey; handleHotKey(hotKey, selectedDoc.key ?? ''); }; document.addEventListener('keydown', handler); return () => document.removeEventListener('keydown', handler); }, []); // empty deps — handleHotKey uses refs so it's always fresh useEffect(() => { droppedItemsRef.current = droppedItems; // For Undo Redo // Skip recording when the change was caused by undo/redo itself if (isUndoRedoRef.current) { isUndoRedoRef.current = false; return; } if (Object.keys(droppedItems).length === 0) return; // Trim any future states beyond cursor (new action wipes redo stack) historyRef.current = historyRef.current.slice(0, cursorRef.current + 1); // Deep clone so mutations don't corrupt history historyRef.current.push(JSON.parse(JSON.stringify(droppedItems))); cursorRef.current = historyRef.current.length - 1; notifyHistoryChange(); }, [droppedItems]); useEffect(() => { selectedTagRef.current = selectedTag; }, [selectedTag]); const handleUndo = () => { if (cursorRef.current <= 0) return; // nothing to undo cursorRef.current -= 1; const snapshot = historyRef.current[cursorRef.current]; isUndoRedoRef.current = true; // flag so the effect above skips pushing dispatch(setDroppedItems({ transactionKey, items: JSON.parse(JSON.stringify(snapshot)) })); notifyHistoryChange(); }; const handleRedo = () => { if (cursorRef.current >= historyRef.current.length - 1) return; // nothing to redo cursorRef.current += 1; const snapshot = historyRef.current[cursorRef.current]; isUndoRedoRef.current = true; dispatch(setDroppedItems({ transactionKey, items: JSON.parse(JSON.stringify(snapshot)) })); notifyHistoryChange(); }; const groupByRecipientId = (tagsArray: any[]) => { const tagsData: Record = {}; for (const tag of tagsArray) { const recipientId = tag.recipientId; if (!tagsData[recipientId]) { tagsData[recipientId] = []; } tagsData[recipientId].push(tag); } return tagsData; }; const sendTagsToRecipients = async ( tagsData: Record, accountId: string, transactionId: string ) => { // tagsData = tagsData.filter((tag, indx) => (("update" in tag && tag.update) || !("id" in tag)) ) // Filter Tags that needs to update const updatedData = {} for (const [recipientId, tags] of Object.entries(tagsData)) { // console.log(recipientId, tags, "line 133") const updateTags = [] for (const tag of tags) { if ("update" in tag && tag.update) { updateTags.push(tag) } if (updateTags.length) { const lastTag = updateTags[updateTags.length - 1]; if ("update" in lastTag || "icon" in lastTag) { // Create new object without update and icon properties const { update, icon, ...cleanTag } = lastTag as any; updateTags[updateTags.length - 1] = cleanTag; } } } updatedData[recipientId] = updateTags.filter((tag) => !tag.isDeleted); } const requests = Object.entries(updatedData) .filter(([_, tags]) => tags.length > 0) // filter out empty ones .map(([recipientId, tags]) => { const updatedTagsWithNavOrder = tags.map((tag, indx) => ({ ...tag, navigationOrder: indx + 1, })); // const tagsData: any[] = serializeGroupedFields(updatedTagsWithNavOrder) return api.put( `/api/v1/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}/recipients/${recipientId}/tags`, updatedTagsWithNavOrder ); }); try { const responses = await Promise.all(requests); // Dispatch Redux action instead of local state const droppedItemsUpdateFalse = Object.fromEntries( Object.entries(droppedItems).map(([recipientId, tags]) => { return [recipientId, tags.map(tag => ({ ...tag, update: false }))]; }) ) dispatch(setDroppedItems({ transactionKey, items: droppedItemsUpdateFalse })); return responses; } catch (error) { console.error("Error submitting recipient tags:", error); } }; const handleTransactionProcess = async (save: boolean = false, isNavigate: boolean = false, navigateTo: string = "/") => { // if (TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES === transactionTemplateToggle) save = true; dispatch(showLoader("Saving Tags ...")) if (Object.keys(droppedItems).length == 0) { if (isNavigate) navigate(navigateTo); dispatch(hideLoader()); }; // if (!save) return; let updatedTags: any = []; for (const dropPagesId of Object.keys(droppedItems)) { updatedTags.push(...droppedItems[dropPagesId]); } let tagsData = groupByRecipientId(updatedTags); const responses = await sendTagsToRecipients(tagsData, accountId, transactionId); if (responses && "status" in responses && responses?.status !== 200) return // return // SENDING Transaction - START if (!save) { dispatch(showLoader("Sending ...")) const res = await api.put(`/api/v1/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`, { status: "SENT" }) if (res.status >= 200 && res.status < 300) { navigate(`/transaction-sent/${accountId}/${transactionId}`, { replace: true }) // window.location.href = `${import.meta.env.VITE_BASE_URI}/transaction-sent/${accountId}/${transactionId}` } dispatch(hideLoader()) } if (isNavigate) { navigate(navigateTo); } dispatch(hideLoader()); // SENDING Transaction - END } // Save Tags - START // useEffect(() => { // handleTransactionProcess() // }, [isSend]) useImperativeHandle(ref, () => ({ handleTransactionProcess, handleUndo, handleRedo, handleCopy, handlePaste })); // Save Tags - END useEffect(() => { if (transactionSaving) { setTimeout(() => { setTransactionSaving(false) }, 1000) } }, [transactionSaving]) const handleDragOver = (event: React.DragEvent) => { event.preventDefault(); // Allows the drop event to work }; // HANDLE DROP - START const handleDrop = (event: React.DragEvent, docId: string, documentId: number, pageIndex: number, originalWidth: number, originalHeight: number) => { event.preventDefault(); const pageRefKey = `${docId}_${pageIndex}`; const imageElement = imageRefs.current[pageRefKey] || document.getElementById(`${docId}-${pageIndex}`) as HTMLImageElement; const currentWidth = imageElement?.offsetWidth || originalWidth; const currentHeight = imageElement?.offsetHeight || originalHeight; const scaleX = currentWidth / originalWidth; // 350 / 700 = 0.5 const scaleY = currentHeight / originalHeight; // 225 / 450 = 0.5 const data = window.dragElemData; if (!data) return; const dragElemData = JSON.parse(data); // Get the dropped label const field = { ...dragElemData } if (!field.label) return; // const field = fields.find((item) => item.label == dragElemData.label); // Find corresponding field // if (!field) return; // If not found, exit const dropZone = event.currentTarget.getBoundingClientRect(); let clientX, clientY; if (event.type.startsWith("touch") || event.nativeEvent?.changedTouches) { const touch = event.nativeEvent?.changedTouches?.[0] || event.changedTouches?.[0]; clientX = touch.pageX; clientY = touch.pageY; } else { clientX = event.clientX; clientY = event.clientY; } // let x = (clientX - dropZone.left) - (dragElemData.width / 2); // let y = (clientY - dropZone.top) - (dragElemData.height / 2); let x = (clientX - dropZone.left) / scaleFactor - (dragElemData.width / 2); let y = (clientY - dropZone.top) / scaleFactor - (dragElemData.height / 2); // Handling Drop Over Right // if (dropZone.width < x + dragElemData.width) { // x -= (x + dragElemData.width) - dropZone.width; // } // Handling Drop Over Bottom // if (dropZone.height < y + dragElemData.height) { // y -= (y + dragElemData.height) - dropZone.height; // } // Handling Drop Over Left // if (x < 0) { // x = 0; // } // Handling Drop Over Top // if (y < 0) { // y = 0; // } // x = Math.max(0, Math.min(x, dropZone.width - dragElemData.width)); // y = Math.max(0, Math.min(y, dropZone.height - dragElemData.height)); x = Math.max(0, Math.min(x, originalWidth - dragElemData.width)); y = Math.max(0, Math.min(y, originalHeight - dragElemData.height)); const key = `${docId}_${pageIndex}`; // x = x * scaleX // y = y * scaleY // const IconComponent = typeToIconMap[field.type] || ; let prepData = { ...data.properties, icon: typeToIconMap[field.label.toUpperCase()] || , type: dragElemData.label, label: dragElemData.label, // subType: (dragElemData.label in droppedTagsDefaultField) ? droppedTagsDefaultField[dragElemData.label] : '', dimensions: { left: x / scaleX, top: y / scaleY, width: dragElemData.width / scaleX, height: dragElemData.height / scaleY, }, styles: null, documentId: docId, pageIndex: pageIndex, recipientId: dragElemData.recipientId, recipientIndx: dragElemData.recipientIndx, required: (dragElemData.label != TagsTypes.SIGNATURE && dragElemData.label != TagsTypes.INITIALS) ? false : true, update: true, isDeleted: false } if ("group" in dragElemData) { prepData.group = dragElemData.group; prepData.isGroupExist = false; } if (prepData.type == "temporal") { prepData.currentTimeStamp = true; } if (prepData.type.toLowerCase() == "text") { prepData.multiLine = true } if (prepData.type.toLowerCase() == "number") { prepData.decimalPlace = 0; prepData.isCurrency = false; } const dataToSet: DroppedItemsObj = { ...droppedItems, [key]: [...(droppedItems[key] || []), prepData], } dispatch(setDroppedItems({ transactionKey, items: dataToSet })); dispatch(incrementCounter(transactionKey)); }; // HANDLE DROP - END // useEffect(() => { // return () => { // const targetCon = document.getElementById('doc-con'); // if (docKeyListenerRef.current && targetCon) { // targetCon.removeEventListener('keydown', docKeyListenerRef.current); // } // }; // }, []); const handleCopy = () => { const currentSelectedTag = selectedTagRef.current; const currentDroppedItems = droppedItemsRef.current; if (!currentSelectedTag) return; const tag = currentDroppedItems[currentSelectedTag.key]?.[currentSelectedTag.index]; if (!tag) return; const { id, ...tagWithoutId } = tag as any; copiedTags = [{ ...tagWithoutId, dimensions: { ...tagWithoutId.dimensions, top: tagWithoutId.dimensions.top + 10, left: tagWithoutId.dimensions.left + 10, } }]; }; const handlePaste = () => { if (!copiedTags.length || !selectedDoc.key) return; const currentDroppedItems = droppedItemsRef.current; const existingTags = currentDroppedItems[selectedDoc.key] || []; dispatch(setDroppedItems({ transactionKey, items: { ...currentDroppedItems, [selectedDoc.key]: [...existingTags, ...copiedTags.map(t => ({ ...t, update: true }))] } })); dispatch(incrementCounter(transactionKey)); }; // Replace the hotkeys useEffect with a direct handler function const handleHotKey = (hotKey: string, docKey: string) => { const currentDroppedItems = droppedItemsRef.current; const currentSelectedTag = selectedTagRef.current; const docCon = document.getElementById('doc-con'); switch (hotKey) { case 'Ctrl+c': { handleCopy(); break; } case 'Ctrl+v': { handlePaste(); break; } case 'Ctrl+a': { docCon?.querySelectorAll('[data-tag-id]').forEach(tag => { tag.classList.add('border-2', 'border-sky-700'); tag.setAttribute('all-select-group', 'true'); }); // One-time click to deselect all const deselect = () => { docCon?.querySelectorAll('[all-select-group]').forEach(tag => { tag.classList.remove('border-2', 'border-sky-700'); tag.removeAttribute('all-select-group'); }); docCon?.removeEventListener('click', deselect); }; docCon?.addEventListener('click', deselect); break; } case 'delete': case 'backspace': { const allSelected = docCon?.querySelectorAll('[all-select-group]') ?? []; if (allSelected.length > 0) { allSelected.forEach(tag => { const tagIndex = tag.getAttribute('data-tag-id') ?? ''; const key = tag.getAttribute('data-drop-key') ?? ''; childRef.current?.handleRemoveTag(key, tagIndex); }); } else if (currentSelectedTag) { childRef.current?.handleRemoveTag(currentSelectedTag.key, currentSelectedTag.index); } break; } case 'Ctrl+z': { handleUndo(); break; } case 'Ctrl+y': { handleRedo(); break; } case 'Ctrl+Shift+z': { handleRedo(); // Mac convention break; } default: break; } }; const handleDocumentFocus = (key: string) => { selectedDoc.key = key; }; return ( <> {/* Center Doc Container - START */}
{ selectedDocuments.length > 0 ? selectedDocuments.map((doc, indxDoc) => { return (

{doc.name}

{ doc.pages && doc.pages.length > 0 && doc.pages.map((page, indx) => { const key = `${doc.id}_${page.index}`; return (
handleDocumentFocus(key)} key={indx} id={`${doc.id}${indx}`} data-page-key={key} ref={(el) => { pageRefs.current[key] = el; }} onDrop={(e) => handleDrop(e, doc.id, indxDoc, page.index, page.width, page.height)} onDragOver={handleDragOver} className='drop-page-in shadow-md relative' style={{ aspectRatio: `${page.width}/${page.height}` }} > { const pageRefKey = `${doc.id}_${page.index}`; imageRefs.current[pageRefKey] = el; }} onLoad={(event) => { const pageRefKey = `${doc.id}_${page.index}`; const el = event.currentTarget; setImageSizes((prev) => ({ ...prev, [pageRefKey]: { width: el.offsetWidth, height: el.offsetHeight, }, })); }} draggable="false" width={page.width} height={page.height} // src={`http://10.10.1.99:8083/api/v1/accounts/${accountId}/transactions/${transactionId}/documents/${doc.id}/pages/${page.id}`} src={page.src} loading='lazy' data-doc-img="true" /> { const newItems = typeof updateFn === 'function' ? updateFn(droppedItems) : updateFn; dispatch(setDroppedItems({ transactionKey, items: newItems })); }} accountId={accountId} transactionId={transactionId} transactionTemplateToggle={transactionTemplateToggle} // tagUndoRef={tagUndoRef} transactionKey={transactionKey} /> {/* Side details of documents Pages and document Name - START */}

page {indx + 1} of {doc.pages && doc.pages.length}

{/*

Document {indxDoc + 1} {doc.name}

*/} {/* Side details of documents Pages and document Name - END */}
) }) }
) }) : }
{/* Note Check Here - Work on Tag Propertieres Apppearrrrrrrrrrrrrrrr ------------------------- */} <> {selectedTag && selectedTag != null && "key" in selectedTag && "index" in selectedTag && droppedItems[selectedTag.key] && droppedItems[selectedTag.key][selectedTag.index] && { const newItems = typeof updateFn === 'function' ? updateFn(droppedItems) : updateFn; dispatch(setDroppedItems({ transactionKey, items: newItems })); }} selTagKey={selectedTag.key} selTagIndx={selectedTag.index} signClient={false} emptySelectedTag={() => setSelectedTag(null)} /> } {/* {selectedDocuments && selectedDocuments.length > 0 && } */} {!isPreview && selectedDocuments && selectedDocuments.length > 0 && { // NEW — replaces hash-based nav isProgrammaticScrollRef.current = true; setActivePage(key); pageRefs.current[key]?.scrollIntoView({ behavior: 'smooth', block: 'start' }); // Release the guard after the smooth scroll has had time to settle, // so the IntersectionObserver resumes driving activePage afterward. window.setTimeout(() => { isProgrammaticScrollRef.current = false; }, 600); }} />} {/* Center Doc Container - END */} ) }) export default DocumentContainer;