/ src / dreamspace / DreamNode3D.tsx.backup
DreamNode3D.tsx.backup
   1  import React, { useState, useRef, useMemo, useEffect, useImperativeHandle, forwardRef, useCallback } from 'react';
   2  import { Html, Billboard } from '@react-three/drei';
   3  import { useFrame, useThree } from '@react-three/fiber';
   4  import { Vector3, Group, Mesh, Quaternion } from 'three';
   5  import { DreamNode } from '../types/dreamnode';
   6  import { calculateDynamicScaling, DEFAULT_SCALING_CONFIG } from '../dreamspace/DynamicViewScaling';
   7  import { useInterBrainStore } from '../store/interbrain-store';
   8  import { dreamNodeStyles } from './dreamNodeStyles';
   9  import { DreamSongParserService } from '../services/dreamsong-parser-service';
  10  import { CanvasParserService } from '../services/canvas-parser-service';
  11  import { VaultService } from '../services/vault-service';
  12  import { DreamSongData } from '../types/dreamsong';
  13  import { DreamTalkSide } from './DreamTalkSide';
  14  import { DreamSongSide } from './DreamSongSide';
  15  import './dreamNodeAnimations.css';
  16  
  17  // Universal Movement API interface
  18  export interface DreamNode3DRef {
  19    moveToPosition: (targetPosition: [number, number, number], duration?: number, easing?: string) => void;
  20    returnToConstellation: (duration?: number, easing?: string) => void;
  21    returnToScaledPosition: (duration?: number, worldRotation?: Quaternion, easing?: string) => void; // New method for full constellation return with rotation and easing support
  22    interruptAndMoveToPosition: (targetPosition: [number, number, number], duration?: number, easing?: string) => void;
  23    interruptAndReturnToConstellation: (duration?: number, easing?: string) => void;
  24    interruptAndReturnToScaledPosition: (duration?: number, worldRotation?: Quaternion, easing?: string) => void;
  25    setActiveState: (active: boolean) => void;
  26    getCurrentPosition: () => [number, number, number];
  27    isMoving: () => boolean;
  28  }
  29  
  30  interface DreamNode3DProps {
  31    dreamNode: DreamNode;
  32    onHover?: (node: DreamNode, isHovered: boolean) => void;
  33    onClick?: (node: DreamNode) => void;
  34    onDoubleClick?: (node: DreamNode) => void;
  35    enableDynamicScaling?: boolean;
  36    onHitSphereRef?: (nodeId: string, meshRef: React.RefObject<Mesh | null>) => void;
  37    // Services for DreamSong parsing
  38    vaultService?: VaultService;
  39    canvasParserService?: CanvasParserService;
  40  }
  41  
  42  /**
  43   * 3D DreamNode component with dual-mode positioning system
  44   * 
  45   * Features:
  46   * - Dual-mode: constellation (continuous radial offset) vs active (discrete interpolation)
  47   * - Universal movement API for liminal web transitions
  48   * - Position sovereignty - component owns its position state
  49   * - Counter-rotation support for world-space positioning
  50   * - Color coding: blue for Dreams, red for Dreamers
  51   */
  52  const DreamNode3D = forwardRef<DreamNode3DRef, DreamNode3DProps>(({ 
  53    dreamNode, 
  54    onHover, 
  55    onClick, 
  56    onDoubleClick,
  57    enableDynamicScaling = false,
  58    onHitSphereRef,
  59    vaultService,
  60    canvasParserService
  61  }, ref) => {
  62    const [isHovered, setIsHovered] = useState(false);
  63    const [radialOffset, setRadialOffset] = useState(0);
  64    const groupRef = useRef<Group>(null);
  65    const hitSphereRef = useRef<Mesh>(null);
  66    
  67    // Flip animation state - default to Math.PI for front side (corrected orientation)
  68    const [flipRotation, setFlipRotation] = useState(Math.PI);
  69    const [dreamSongData, setDreamSongData] = useState<DreamSongData | null>(null);
  70    const [hasDreamSong, setHasDreamSong] = useState(false);
  71    const [dreamSongHasContent, setDreamSongHasContent] = useState(false);
  72    const [isLoadingDreamSong, setIsLoadingDreamSong] = useState(false);
  73    
  74    // Dual-mode position state
  75    const [positionMode, setPositionMode] = useState<'constellation' | 'active'>('constellation');
  76    const [targetPosition, setTargetPosition] = useState<[number, number, number]>(dreamNode.position);
  77    const [currentPosition, setCurrentPosition] = useState<[number, number, number]>(dreamNode.position);
  78    const [startPosition, setStartPosition] = useState<[number, number, number]>(dreamNode.position);
  79    const [isTransitioning, setIsTransitioning] = useState(false);
  80    const [transitionStartTime, setTransitionStartTime] = useState(0);
  81    const [transitionDuration, setTransitionDuration] = useState(1000);
  82    const [transitionType, setTransitionType] = useState<'liminal' | 'constellation' | 'scaled'>('liminal');
  83    const [transitionEasing, setTransitionEasing] = useState<'easeOutCubic' | 'easeInQuart' | 'easeOutQuart'>('easeOutCubic');
  84    
  85    // Check global drag state to prevent hover interference during sphere rotation
  86    const isDragging = useInterBrainStore(state => state.isDragging);
  87    
  88    // Flip state management
  89    const flipState = useInterBrainStore(state => state.flipState);
  90    const setFlippedNode = useInterBrainStore(state => state.setFlippedNode);
  91    const startFlipAnimation = useInterBrainStore(state => state.startFlipAnimation);
  92    const completeFlipAnimation = useInterBrainStore(state => state.completeFlipAnimation);
  93    const spatialLayout = useInterBrainStore(state => state.spatialLayout);
  94    const selectedNode = useInterBrainStore(state => state.selectedNode);
  95    
  96    // Subscribe to edit mode state for relationship glow
  97    const isEditModeActive = useInterBrainStore(state => state.editMode.isActive);
  98    const isPendingRelationship = useInterBrainStore(state => 
  99      state.editMode.pendingRelationships.includes(dreamNode.id)
 100    );
 101    
 102    // Get current flip state for this node
 103    const nodeFlipState = flipState.flipStates.get(dreamNode.id);
 104    const isFlipped = nodeFlipState?.isFlipped || false;
 105    const isFlipping = nodeFlipState?.isFlipping || false;
 106    
 107    // Ensure initial state shows front side (flipRotation = Math.PI = front)
 108    useEffect(() => {
 109      if (!nodeFlipState) {
 110        setFlipRotation(Math.PI); // Front side by default
 111      }
 112    }, [nodeFlipState]);
 113    
 114    // Determine if flip button should be visible (only in liminal web mode for selected node)
 115    const shouldShowFlipButton = useMemo(() => {
 116      const result = spatialLayout === 'liminal-web' && 
 117                     selectedNode?.id === dreamNode.id && 
 118                     isHovered &&
 119                     hasDreamSong &&  // Show button if DreamSong file exists (even if empty)
 120                     !isDragging;
 121      
 122      // Debug logging for flip button visibility (only when conditions are close)
 123      if (spatialLayout === 'liminal-web' && selectedNode?.id === dreamNode.id) {
 124        console.log(`🔄 [DreamNode3D] Flip button logic for "${dreamNode.name}":`);  
 125        console.log(`  - spatialLayout === 'liminal-web': ${spatialLayout === 'liminal-web'}`);
 126        console.log(`  - selectedNode?.id === dreamNode.id: ${selectedNode?.id === dreamNode.id}`);
 127        console.log(`  - isHovered: ${isHovered}`);
 128        console.log(`  - hasDreamSong: ${hasDreamSong}`);
 129        console.log(`  - dreamSongHasContent: ${dreamSongHasContent}`);
 130        console.log(`  - isDragging: ${isDragging}`);
 131        console.log(`  - shouldShowFlipButton: ${result}`);
 132      }
 133      
 134      return result;
 135    }, [spatialLayout, selectedNode, dreamNode.id, isHovered, hasDreamSong, isDragging]);
 136  
 137    // Register hit sphere reference with parent component
 138    useEffect(() => {
 139      if (onHitSphereRef && hitSphereRef) {
 140        onHitSphereRef(dreamNode.id, hitSphereRef);
 141      }
 142    }, [dreamNode.id, onHitSphereRef]);
 143    
 144    // Check for DreamSong canvas file on component mount, when selected, or when services become available
 145    useEffect(() => {
 146      const checkDreamSong = async () => {
 147        console.log(`🎭 [DreamNode3D] Checking DreamSong for node: "${dreamNode.name}" (${dreamNode.id})`);
 148        console.log(`🎭 [DreamNode3D] Services available - vault: ${!!vaultService}, canvas: ${!!canvasParserService}`);
 149        console.log(`🎭 [DreamNode3D] Node selected: ${selectedNode?.id === dreamNode.id}, spatialLayout: ${spatialLayout}`);
 150        
 151        if (!vaultService || !canvasParserService) {
 152          console.log(`⚠️ [DreamNode3D] Cannot check DreamSong: missing services - will retry when services become available`);
 153          // Reset states when services unavailable
 154          setHasDreamSong(false);
 155          setDreamSongHasContent(false);
 156          return;
 157        }
 158        
 159        const canvasPath = `${dreamNode.repoPath}/DreamSong.canvas`;
 160        console.log(`🎭 [DreamNode3D] Checking canvas at: "${canvasPath}"`);
 161        
 162        try {
 163          const exists = await vaultService.fileExists(canvasPath);
 164          console.log(`${exists ? '✅' : '❌'} [DreamNode3D] DreamSong canvas ${exists ? 'EXISTS' : 'NOT FOUND'} for "${dreamNode.name}"`);
 165          setHasDreamSong(exists);
 166          
 167          // If DreamSong exists, try to parse it to check if it has content
 168          if (exists) {
 169            console.log(`🎭 [DreamNode3D] DreamSong exists, creating parser to check content...`);
 170            const dreamSongParser = new DreamSongParserService(vaultService, canvasParserService);
 171            const parseResult = await dreamSongParser.parseDreamSong(canvasPath, dreamNode.repoPath);
 172            
 173            if (parseResult.success && parseResult.data) {
 174              console.log(`✅ [DreamNode3D] DreamSong parsed successfully:`);
 175              console.log(`  - Blocks: ${parseResult.data.blocks.length}`);
 176              console.log(`  - Has content: ${parseResult.data.hasContent}`);
 177              console.log(`  - Total blocks: ${parseResult.data.totalBlocks}`);
 178              
 179              // Set content availability based on actual parsed content
 180              setDreamSongHasContent(parseResult.data.hasContent);
 181            } else {
 182              console.log(`❌ [DreamNode3D] DreamSong parse failed:`, parseResult.error?.message);
 183              setDreamSongHasContent(false);
 184            }
 185          } else {
 186            setDreamSongHasContent(false);
 187          }
 188        } catch (error) {
 189          console.error(`❌ [DreamNode3D] Error checking DreamSong for ${dreamNode.id}:`, error);
 190          setHasDreamSong(false);
 191          setDreamSongHasContent(false);
 192        }
 193      };
 194      
 195      checkDreamSong();
 196    }, [dreamNode.id, dreamNode.repoPath, vaultService, canvasParserService, selectedNode?.id, spatialLayout]);
 197    
 198    // Load DreamSong data when flipped to back side
 199    useEffect(() => {
 200      const loadDreamSongData = async () => {
 201        if (!isFlipped || !hasDreamSong || !vaultService || !canvasParserService || dreamSongData) {
 202          return;
 203        }
 204        
 205        setIsLoadingDreamSong(true);
 206        
 207        try {
 208          const dreamSongParser = new DreamSongParserService(vaultService, canvasParserService);
 209          const canvasPath = `${dreamNode.repoPath}/DreamSong.canvas`;
 210          const result = await dreamSongParser.parseDreamSong(canvasPath, dreamNode.repoPath);
 211          
 212          if (result.success && result.data) {
 213            setDreamSongData(result.data);
 214          } else {
 215            console.error(`Failed to parse DreamSong: ${result.error?.message}`);
 216          }
 217        } catch (error) {
 218          console.error(`Error loading DreamSong for ${dreamNode.id}:`, error);
 219        } finally {
 220          setIsLoadingDreamSong(false);
 221        }
 222      };
 223      
 224      loadDreamSongData();
 225    }, [isFlipped, hasDreamSong, dreamNode.id, dreamNode.repoPath, vaultService, canvasParserService, dreamSongData]);
 226    
 227    // Reset flip state when node is no longer selected in liminal web mode
 228    useEffect(() => {
 229      if (spatialLayout !== 'liminal-web' || selectedNode?.id !== dreamNode.id) {
 230        if (flipState.flippedNodeId === dreamNode.id) {
 231          setFlippedNode(null);
 232          setFlipRotation(0);
 233          setDreamSongData(null);
 234        }
 235      }
 236    }, [spatialLayout, selectedNode, dreamNode.id, flipState.flippedNodeId, setFlippedNode]);
 237  
 238    // Handle mouse events (suppress during sphere rotation to prevent interference)
 239    const handleMouseEnter = () => {
 240      if (isDragging) {
 241        return; // Suppress hover during drag operations
 242      }
 243      setIsHovered(true);
 244      onHover?.(dreamNode, true);
 245    };
 246  
 247    const handleMouseLeave = () => {
 248      if (isDragging) {
 249        return; // Suppress hover during drag operations
 250      }
 251      setIsHovered(false);
 252      onHover?.(dreamNode, false);
 253    };
 254  
 255    const handleClick = (e: React.MouseEvent) => {
 256      if (isDragging) return; // Suppress click during drag operations
 257      e.stopPropagation();
 258      onClick?.(dreamNode);
 259    };
 260    
 261    // Handle flip button click
 262    const handleFlipClick = useCallback((e: React.MouseEvent) => {
 263      e.stopPropagation();
 264      if (isDragging || isFlipping) return;
 265      
 266      const direction = isFlipped ? 'back-to-front' : 'front-to-back';
 267      startFlipAnimation(dreamNode.id, direction);
 268    }, [isDragging, isFlipping, isFlipped, startFlipAnimation, dreamNode.id]);
 269  
 270    const handleDoubleClick = (e: React.MouseEvent) => {
 271      if (isDragging) return; // Suppress double-click during drag operations
 272      e.stopPropagation();
 273      onDoubleClick?.(dreamNode);
 274    };
 275  
 276    // Universal Movement API
 277    useImperativeHandle(ref, () => ({
 278      moveToPosition: (newTargetPosition, duration = 1000, easing = 'easeOutCubic') => {
 279        // Switch to active mode and start transition
 280        // CRITICAL FIX: Calculate actual current visual position
 281        let actualCurrentPosition: [number, number, number];
 282        
 283        if (positionMode === 'constellation') {
 284          // Calculate constellation position with radial offset
 285          const anchorPos = dreamNode.position;
 286          const direction = [-anchorPos[0], -anchorPos[1], -anchorPos[2]];
 287          const dirLength = Math.sqrt(direction[0]**2 + direction[1]**2 + direction[2]**2);
 288          const normalizedDir = [direction[0]/dirLength, direction[1]/dirLength, direction[2]/dirLength];
 289          
 290          actualCurrentPosition = [
 291            anchorPos[0] - normalizedDir[0] * radialOffset,
 292            anchorPos[1] - normalizedDir[1] * radialOffset,
 293            anchorPos[2] - normalizedDir[2] * radialOffset
 294          ];
 295        } else {
 296          actualCurrentPosition = [...currentPosition];
 297        }
 298        
 299        setStartPosition(actualCurrentPosition);
 300        setCurrentPosition(actualCurrentPosition); // Initialize currentPosition for active mode
 301        setTargetPosition(newTargetPosition);
 302        setTransitionDuration(duration);
 303        setTransitionStartTime(globalThis.performance.now());
 304        setPositionMode('active');
 305        setIsTransitioning(true);
 306        setTransitionType('liminal'); // This is a liminal web transition
 307        setTransitionEasing(easing as 'easeOutCubic' | 'easeInQuart' | 'easeOutQuart');
 308        
 309      },
 310      returnToConstellation: (duration = 1000, easing = 'easeInQuart') => {
 311        // Enhanced method: returns to proper constellation position (with scaling if enabled)
 312        let actualCurrentPosition: [number, number, number];
 313        
 314        if (positionMode === 'constellation') {
 315          // Calculate current visual position with radial offset
 316          const anchorPos = dreamNode.position;
 317          const direction = [-anchorPos[0], -anchorPos[1], -anchorPos[2]];
 318          const dirLength = Math.sqrt(direction[0]**2 + direction[1]**2 + direction[2]**2);
 319          const normalizedDir = [direction[0]/dirLength, direction[1]/dirLength, direction[2]/dirLength];
 320          
 321          actualCurrentPosition = [
 322            anchorPos[0] - normalizedDir[0] * radialOffset,
 323            anchorPos[1] - normalizedDir[1] * radialOffset,
 324            anchorPos[2] - normalizedDir[2] * radialOffset
 325          ];
 326        } else {
 327          actualCurrentPosition = [...currentPosition];
 328        }
 329        
 330        // Target position should be sphere surface - constellation mode will handle scaling
 331        const constellationPosition = dreamNode.position;
 332        setStartPosition(actualCurrentPosition);
 333        setCurrentPosition(actualCurrentPosition);
 334        setTargetPosition(constellationPosition);
 335        setTransitionDuration(duration);
 336        setTransitionStartTime(globalThis.performance.now());
 337        setPositionMode('active'); // Use active mode for the transition
 338        setIsTransitioning(true);
 339        setTransitionType('constellation'); // This is a constellation return transition
 340        setTransitionEasing(easing as 'easeOutCubic' | 'easeInQuart' | 'easeOutQuart');
 341      },
 342      returnToScaledPosition: (duration = 1000, worldRotation, easing = 'easeOutCubic') => {
 343        // ROBUST METHOD: Returns ANY node to its proper scaled constellation position
 344        // Handles both active nodes (from liminal positions) and inactive nodes (from sphere surface)
 345        
 346        // Calculate target dynamically scaled position for this node
 347        const anchorPosition = dreamNode.position;
 348        
 349        // Transform anchor position to world space using provided rotation (similar to useFrame logic)
 350        const worldAnchorPosition = new Vector3(anchorPosition[0], anchorPosition[1], anchorPosition[2]);
 351        if (worldRotation) {
 352          // Apply the sphere's world rotation to get the actual world position
 353          worldAnchorPosition.applyQuaternion(worldRotation);
 354        }
 355        
 356        // Calculate what the radial offset should be using dynamic scaling
 357        const { radialOffset: targetRadialOffset } = calculateDynamicScaling(
 358          worldAnchorPosition,
 359          DEFAULT_SCALING_CONFIG
 360        );
 361        
 362        // Calculate target scaled position
 363        const direction = [-anchorPosition[0], -anchorPosition[1], -anchorPosition[2]];
 364        const dirLength = Math.sqrt(direction[0]**2 + direction[1]**2 + direction[2]**2);
 365        const normalizedDir = [direction[0]/dirLength, direction[1]/dirLength, direction[2]/dirLength];
 366        
 367        const targetScaledPosition: [number, number, number] = [
 368          anchorPosition[0] - normalizedDir[0] * targetRadialOffset,
 369          anchorPosition[1] - normalizedDir[1] * targetRadialOffset,
 370          anchorPosition[2] - normalizedDir[2] * targetRadialOffset
 371        ];
 372        
 373        // Get actual current position
 374        let actualCurrentPosition: [number, number, number];
 375        if (positionMode === 'constellation') {
 376          // Calculate current visual position with radial offset
 377          actualCurrentPosition = [
 378            anchorPosition[0] - normalizedDir[0] * radialOffset,
 379            anchorPosition[1] - normalizedDir[1] * radialOffset,
 380            anchorPosition[2] - normalizedDir[2] * radialOffset
 381          ];
 382        } else {
 383          actualCurrentPosition = [...currentPosition];
 384        }
 385        
 386        // Animate to target scaled position
 387        setStartPosition(actualCurrentPosition);
 388        setCurrentPosition(actualCurrentPosition);
 389        setTargetPosition(targetScaledPosition);
 390        setTransitionDuration(duration);
 391        setTransitionStartTime(globalThis.performance.now());
 392        setPositionMode('active'); // Use active mode for the transition
 393        setIsTransitioning(true);
 394        setTransitionType('scaled'); // This is a scaled position return transition
 395        setTransitionEasing(easing as 'easeOutCubic' | 'easeInQuart' | 'easeOutQuart');
 396        
 397        // Determine node's current state for logging (removed unused variables for cleaner build)
 398        
 399        // Set the target radial offset for when we switch back to constellation mode
 400        globalThis.setTimeout(() => {
 401          setRadialOffset(targetRadialOffset);
 402        }, duration - 100); // Set slightly before transition completes
 403      },
 404      interruptAndMoveToPosition: (newTargetPosition, duration = 1000, easing = 'easeOutCubic') => {
 405        // Enhanced method: Can interrupt existing animation using current position as new start point
 406        
 407        // CRITICAL: Calculate actual current visual position (including mid-flight positions)
 408        let actualCurrentPosition: [number, number, number];
 409        
 410        if (positionMode === 'constellation') {
 411          // Calculate constellation position with radial offset
 412          const anchorPos = dreamNode.position;
 413          const direction = [-anchorPos[0], -anchorPos[1], -anchorPos[2]];
 414          const dirLength = Math.sqrt(direction[0]**2 + direction[1]**2 + direction[2]**2);
 415          const normalizedDir = [direction[0]/dirLength, direction[1]/dirLength, direction[2]/dirLength];
 416          
 417          actualCurrentPosition = [
 418            anchorPos[0] - normalizedDir[0] * radialOffset,
 419            anchorPos[1] - normalizedDir[1] * radialOffset,
 420            anchorPos[2] - normalizedDir[2] * radialOffset
 421          ];
 422        } else {
 423          // Node is in active mode - use the current interpolated position
 424          actualCurrentPosition = [...currentPosition];
 425        }
 426        
 427        // Start new animation from current position (interrupts existing animation smoothly)
 428        setStartPosition(actualCurrentPosition);
 429        setCurrentPosition(actualCurrentPosition);
 430        setTargetPosition(newTargetPosition);
 431        setTransitionDuration(duration);
 432        setTransitionStartTime(globalThis.performance.now());
 433        setPositionMode('active');
 434        setIsTransitioning(true);
 435        setTransitionType('liminal');
 436        setTransitionEasing(easing as 'easeOutCubic' | 'easeInQuart' | 'easeOutQuart');
 437      },
 438      interruptAndReturnToConstellation: (duration = 1000, easing = 'easeInQuart') => {
 439        // Enhanced method: Can interrupt existing animation to return to constellation
 440        
 441        let actualCurrentPosition: [number, number, number];
 442        
 443        if (positionMode === 'constellation') {
 444          // Calculate current visual position with radial offset
 445          const anchorPos = dreamNode.position;
 446          const direction = [-anchorPos[0], -anchorPos[1], -anchorPos[2]];
 447          const dirLength = Math.sqrt(direction[0]**2 + direction[1]**2 + direction[2]**2);
 448          const normalizedDir = [direction[0]/dirLength, direction[1]/dirLength, direction[2]/dirLength];
 449          
 450          actualCurrentPosition = [
 451            anchorPos[0] - normalizedDir[0] * radialOffset,
 452            anchorPos[1] - normalizedDir[1] * radialOffset,
 453            anchorPos[2] - normalizedDir[2] * radialOffset
 454          ];
 455        } else {
 456          // Node is in active mode - use the current interpolated position
 457          actualCurrentPosition = [...currentPosition];
 458        }
 459        
 460        // Target position should be sphere surface - constellation mode will handle scaling
 461        const constellationPosition = dreamNode.position;
 462        setStartPosition(actualCurrentPosition);
 463        setCurrentPosition(actualCurrentPosition);
 464        setTargetPosition(constellationPosition);
 465        setTransitionDuration(duration);
 466        setTransitionStartTime(globalThis.performance.now());
 467        setPositionMode('active'); // Use active mode for the transition
 468        setIsTransitioning(true);
 469        setTransitionType('constellation');
 470        setTransitionEasing(easing as 'easeOutCubic' | 'easeInQuart' | 'easeOutQuart');
 471      },
 472      interruptAndReturnToScaledPosition: (duration = 1000, worldRotation, easing = 'easeOutCubic') => {
 473        // Enhanced method: Can interrupt existing animation to return to scaled constellation position
 474        
 475        // Calculate target dynamically scaled position for this node
 476        const anchorPosition = dreamNode.position;
 477        
 478        // Transform anchor position to world space using provided rotation (similar to useFrame logic)
 479        const worldAnchorPosition = new Vector3(anchorPosition[0], anchorPosition[1], anchorPosition[2]);
 480        if (worldRotation) {
 481          // Apply the sphere's world rotation to get the actual world position
 482          worldAnchorPosition.applyQuaternion(worldRotation);
 483        }
 484        
 485        // Calculate what the radial offset should be using dynamic scaling
 486        const { radialOffset: targetRadialOffset } = calculateDynamicScaling(
 487          worldAnchorPosition,
 488          DEFAULT_SCALING_CONFIG
 489        );
 490        
 491        // Calculate target scaled position
 492        const direction = [-anchorPosition[0], -anchorPosition[1], -anchorPosition[2]];
 493        const dirLength = Math.sqrt(direction[0]**2 + direction[1]**2 + direction[2]**2);
 494        const normalizedDir = [direction[0]/dirLength, direction[1]/dirLength, direction[2]/dirLength];
 495        
 496        const targetScaledPosition: [number, number, number] = [
 497          anchorPosition[0] - normalizedDir[0] * targetRadialOffset,
 498          anchorPosition[1] - normalizedDir[1] * targetRadialOffset,
 499          anchorPosition[2] - normalizedDir[2] * targetRadialOffset
 500        ];
 501        
 502        // Get actual current position (including mid-flight positions)
 503        let actualCurrentPosition: [number, number, number];
 504        if (positionMode === 'constellation') {
 505          // Calculate current visual position with radial offset
 506          actualCurrentPosition = [
 507            anchorPosition[0] - normalizedDir[0] * radialOffset,
 508            anchorPosition[1] - normalizedDir[1] * radialOffset,
 509            anchorPosition[2] - normalizedDir[2] * radialOffset
 510          ];
 511        } else {
 512          // Node is in active mode - use the current interpolated position
 513          actualCurrentPosition = [...currentPosition];
 514        }
 515        
 516        // Animate to target scaled position (interrupts existing animation smoothly)
 517        setStartPosition(actualCurrentPosition);
 518        setCurrentPosition(actualCurrentPosition);
 519        setTargetPosition(targetScaledPosition);
 520        setTransitionDuration(duration);
 521        setTransitionStartTime(globalThis.performance.now());
 522        setPositionMode('active'); // Use active mode for the transition
 523        setIsTransitioning(true);
 524        setTransitionType('scaled');
 525        setTransitionEasing(easing as 'easeOutCubic' | 'easeInQuart' | 'easeOutQuart');
 526        
 527        // Set the target radial offset for when we switch back to constellation mode
 528        globalThis.setTimeout(() => {
 529          setRadialOffset(targetRadialOffset);
 530        }, duration - 100); // Set slightly before transition completes
 531      },
 532      setActiveState: (active: boolean) => {
 533        if (active) {
 534          setPositionMode('active');
 535          setCurrentPosition([...currentPosition]); // Preserve current position
 536        } else {
 537          setPositionMode('constellation');
 538          // Reset to original position state
 539          setCurrentPosition(dreamNode.position);
 540        }
 541      },
 542      getCurrentPosition: () => currentPosition,
 543      isMoving: () => isTransitioning
 544    }), [currentPosition, isTransitioning, dreamNode.position, positionMode, radialOffset, transitionEasing]);
 545    
 546    // Dual-mode position calculation with counter-rotation
 547    useFrame((_state, _delta) => {
 548      if (positionMode === 'constellation' && enableDynamicScaling && groupRef.current) {
 549        // CONSTELLATION MODE: Continuous radial offset calculation (existing behavior)
 550        // Get world position of the anchor (includes rotation from parent group)
 551        const worldPosition = new Vector3();
 552        groupRef.current.getWorldPosition(worldPosition);
 553        
 554        // But we need to get the world position of the ANCHOR, not the current final position
 555        // So we need to calculate where the anchor would be in world space
 556        const anchorGroup = groupRef.current.parent; // Get the rotatable group
 557        const anchorVector = new Vector3(anchorPosition[0], anchorPosition[1], anchorPosition[2]);
 558        if (anchorGroup) {
 559          anchorGroup.localToWorld(anchorVector);
 560        }
 561        
 562        // Calculate dynamic scaling based on anchor's world position
 563        const { radialOffset: newRadialOffset } = calculateDynamicScaling(
 564          anchorVector,
 565          DEFAULT_SCALING_CONFIG
 566        );
 567        
 568        // Update state if changed
 569        if (radialOffset !== newRadialOffset) {
 570          setRadialOffset(newRadialOffset);
 571        }
 572      } else if (positionMode === 'active' && isTransitioning) {
 573        // ACTIVE MODE: Discrete position interpolation
 574        const elapsed = globalThis.performance.now() - transitionStartTime;
 575        const progress = Math.min(elapsed / transitionDuration, 1);
 576        
 577        // Apply selected easing function
 578        let easedProgress: number;
 579        switch (transitionEasing) {
 580          case 'easeInQuart':
 581            // Strong ease-in for nodes flying OUT to sphere
 582            easedProgress = Math.pow(progress, 4);
 583            break;
 584          case 'easeOutQuart':
 585            // Strong ease-out for nodes flying IN from sphere
 586            easedProgress = 1 - Math.pow(1 - progress, 4);
 587            break;
 588          case 'easeOutCubic':
 589          default:
 590            // Default easing for other transitions
 591            easedProgress = 1 - Math.pow(1 - progress, 3);
 592            break;
 593        }
 594        
 595        // Linear interpolation from start to target position
 596        const newPosition: [number, number, number] = [
 597          startPosition[0] + (targetPosition[0] - startPosition[0]) * easedProgress,
 598          startPosition[1] + (targetPosition[1] - startPosition[1]) * easedProgress,
 599          startPosition[2] + (targetPosition[2] - startPosition[2]) * easedProgress
 600        ];
 601        
 602        setCurrentPosition(newPosition);
 603        
 604        // Check if transition is complete
 605        if (progress >= 1) {
 606          setIsTransitioning(false);
 607          setCurrentPosition(targetPosition); // Ensure exact target position
 608          
 609          // Handle transition completion based on type
 610          if (transitionType === 'liminal') {
 611            // Liminal web transitions: STAY in active mode at target position
 612            // Don't change positionMode - stay active!
 613          } else if (transitionType === 'constellation') {
 614            // Constellation return: Switch back to constellation mode
 615            setPositionMode('constellation');
 616            setRadialOffset(0); // Reset radial offset for clean sphere positioning
 617          } else if (transitionType === 'scaled') {
 618            // Scaled position return: Switch back to constellation mode
 619            setPositionMode('constellation');
 620            // radialOffset was already set during the animation
 621          }
 622        }
 623      }
 624    });
 625  
 626    // Get consistent colors from shared styles
 627    const nodeColors = getNodeColors(dreamNode.type);
 628    
 629    // Get git visual state and styling
 630    const gitState = getGitVisualState(dreamNode.gitStatus);
 631    const gitStyle = getGitStateStyle(gitState);
 632    
 633    // Base size for 3D scaling - will scale with distance due to distanceFactor
 634    const nodeSize = dreamNodeStyles.dimensions.nodeSizeThreeD;
 635    const borderWidth = dreamNodeStyles.dimensions.borderWidth; // Use shared border width
 636    
 637    // Calculate visual component position with radial offset
 638    // Anchor point stays at dreamNode.position, visual component moves radially toward camera
 639    const anchorPosition = dreamNode.position;
 640    
 641    // Calculate normalized direction toward origin (radially inward)
 642    const normalizedDirection = useMemo(() => {
 643      const direction = [
 644        -anchorPosition[0], // Direction toward origin (radially inward)
 645        -anchorPosition[1],
 646        -anchorPosition[2]
 647      ];
 648      const directionLength = Math.sqrt(direction[0]**2 + direction[1]**2 + direction[2]**2);
 649      return [
 650        direction[0] / directionLength,
 651        direction[1] / directionLength,
 652        direction[2] / directionLength
 653      ];
 654    }, [anchorPosition]);
 655    
 656    // Calculate final position based on mode
 657    // CRITICAL: Don't memoize this - we need it to update every render when currentPosition changes
 658    const finalPosition: [number, number, number] = positionMode === 'constellation'
 659      ? [
 660          anchorPosition[0] - normalizedDirection[0] * radialOffset,
 661          anchorPosition[1] - normalizedDirection[1] * radialOffset,
 662          anchorPosition[2] - normalizedDirection[2] * radialOffset
 663        ]
 664      : currentPosition;
 665      
 666    // Removed excessive dynamic scaling logging for performance
 667    
 668    // Access camera for billboard rotation
 669    const { camera } = useThree();
 670    
 671    // Handle flip animation updates
 672    useFrame(() => {
 673      if (!isFlipping) return;
 674      
 675      const targetRotation = nodeFlipState?.flipDirection === 'front-to-back' ? 0 : Math.PI;
 676      const animationDuration = 600; // ms
 677      const elapsed = globalThis.performance.now() - (nodeFlipState?.animationStartTime || 0);
 678      const progress = Math.min(elapsed / animationDuration, 1);
 679      
 680      // Ease-in-out timing function
 681      const easedProgress = progress < 0.5 
 682        ? 2 * progress * progress 
 683        : 1 - Math.pow(-2 * progress + 2, 2) / 2;
 684      
 685      const newRotation = nodeFlipState?.flipDirection === 'front-to-back' 
 686        ? Math.PI - (easedProgress * Math.PI)  // From Math.PI (front) to 0 (back)
 687        : easedProgress * Math.PI;             // From 0 (back) to Math.PI (front)
 688      
 689      setFlipRotation(newRotation);
 690      
 691      // Debug logging for flip animation
 692      console.log(`🔄 [Flip Animation] Node: ${dreamNode.name}, Progress: ${progress.toFixed(2)}, Rotation: ${(newRotation * 180 / Math.PI).toFixed(1)}°`);
 693      
 694      if (progress >= 1) {
 695        console.log(`✅ [Flip Animation] Completed for ${dreamNode.name} at ${(targetRotation * 180 / Math.PI).toFixed(1)}°`);
 696        completeFlipAnimation(dreamNode.id);
 697        setFlipRotation(targetRotation);
 698      }
 699    });
 700    
 701    // Using sprite mode for automatic billboarding - no manual rotation needed
 702    
 703    // Debug logging removed for cleaner console
 704    
 705    // Wrap in group at final position for world position calculations
 706    // Apply hover scaling to the entire group so both visual and hit detection scale together
 707    return (
 708      <group 
 709        ref={groupRef} 
 710        position={finalPosition}
 711      >
 712        {/* Html wrapper for 3D flip animation - manual billboard via CSS */}
 713        <Html
 714          center
 715          transform
 716          distanceFactor={10}
 717          style={{
 718            pointerEvents: isDragging ? 'none' : 'auto',
 719            userSelect: 'none'
 720          }}
 721        >
 722          {/* Rotatable container for flip animation with 3D transforms */}
 723          <div
 724            style={{
 725              transform: `rotateY(${flipRotation}rad) scaleX(-1)`,
 726              transformStyle: 'preserve-3d',
 727              transition: 'transform 0.6s ease-in-out',
 728              width: `${nodeSize}px`,
 729              height: `${nodeSize}px`,
 730              position: 'relative'
 731            }}
 732          >
 733            {/* Front side (DreamTalk) */}
 734            <div
 735              style={{
 736                position: 'absolute',
 737                width: '100%',
 738                height: '100%',
 739                borderRadius: dreamNodeStyles.dimensions.borderRadius,
 740                border: `${borderWidth}px ${gitStyle.borderStyle} ${nodeColors.border}`,
 741                background: nodeColors.fill,
 742                overflow: 'hidden',
 743                cursor: 'pointer !important',
 744                transition: `${dreamNodeStyles.transitions.default}, ${dreamNodeStyles.transitions.gitState}`,
 745                transform: isHovered ? `scale(${dreamNodeStyles.states.hover.scale}) translateZ(1px)` : 'scale(1) translateZ(1px)',
 746                animation: gitStyle.animation,
 747                backfaceVisibility: 'hidden',
 748                boxShadow: (() => {
 749                  // Priority 1: Git status glow (always highest priority)
 750                  if (gitStyle.glowIntensity > 0) {
 751                    return getGitGlow(gitState, gitStyle.glowIntensity);
 752                  }
 753                  
 754                  // Priority 2: Edit mode relationship glow
 755                  if (isEditModeActive && isPendingRelationship) {
 756                    return getEditModeGlow(25); // Strong gold glow for relationships
 757                  }
 758                  
 759                  // Priority 3: Hover glow (fallback)
 760                  return isHovered ? getNodeGlow(dreamNode.type, dreamNodeStyles.states.hover.glowIntensity) : 'none';
 761                })()
 762              }}
 763              onMouseEnter={handleMouseEnter}
 764              onMouseLeave={handleMouseLeave}
 765              onClick={handleClick}
 766              onDoubleClick={handleDoubleClick}
 767            >
 768          {/* DreamTalk Media Container */}
 769          {dreamNode.dreamTalkMedia[0] && (
 770            <div style={getMediaContainerStyle()}>
 771              <MediaRenderer media={dreamNode.dreamTalkMedia[0]} />
 772              {/* Fade-to-black overlay */}
 773              <div style={getMediaOverlayStyle()} />
 774              
 775              {/* Hover overlay with name */}
 776              {isHovered && (
 777                <div
 778                  style={{
 779                    position: 'absolute',
 780                    top: 0,
 781                    left: 0,
 782                    width: '100%',
 783                    height: '100%',
 784                    borderRadius: '50%',
 785                    background: 'rgba(0, 0, 0, 0.7)',
 786                    display: 'flex',
 787                    alignItems: 'center',
 788                    justifyContent: 'center',
 789                    opacity: isHovered ? 1 : 0,
 790                    transition: 'opacity 0.2s ease-in-out',
 791                    pointerEvents: 'none',
 792                    zIndex: 10
 793                  }}
 794                >
 795                  <div
 796                    style={{
 797                      color: dreamNodeStyles.colors.text.primary,
 798                      fontFamily: dreamNodeStyles.typography.fontFamily,
 799                      fontSize: `${Math.max(12, nodeSize * 0.08)}px`,
 800                      textAlign: 'center',
 801                      padding: '8px'
 802                    }}
 803                  >
 804                    {dreamNode.name}
 805                  </div>
 806                </div>
 807              )}
 808            </div>
 809          )}
 810  
 811          {/* Empty state text - when no media */}
 812          {!dreamNode.dreamTalkMedia[0] && (
 813            <div
 814              style={{
 815                width: '100%',
 816                height: '100%',
 817                display: 'flex',
 818                alignItems: 'center',
 819                justifyContent: 'center',
 820                color: dreamNodeStyles.colors.text.primary,
 821                fontFamily: dreamNodeStyles.typography.fontFamily,
 822                fontSize: `${Math.max(12, nodeSize * 0.08)}px`,
 823                textAlign: 'center',
 824                padding: '8px'
 825              }}
 826            >
 827              {dreamNode.name}
 828            </div>
 829          )}
 830  
 831  
 832          {/* Node label */}
 833          <div
 834            style={{
 835              position: 'absolute',
 836              bottom: `-${nodeSize * 0.25}px`,
 837              left: '50%',
 838              transform: 'translateX(-50%)',
 839              color: dreamNodeStyles.colors.text.primary,
 840              fontFamily: dreamNodeStyles.typography.fontFamily,
 841              fontSize: `${Math.max(12, nodeSize * 0.1)}px`,
 842              textAlign: 'center',
 843              background: 'rgba(0, 0, 0, 0.8)',
 844              padding: '4px 8px',
 845              borderRadius: '4px',
 846              whiteSpace: 'nowrap',
 847              pointerEvents: 'none'
 848            }}
 849          >
 850            {dreamNode.name}
 851          </div>
 852          
 853          {/* Flip button (bottom-center, only when hovering and has DreamSong) */}
 854          {shouldShowFlipButton && (
 855            <div
 856              style={{
 857                position: 'absolute',
 858                bottom: '8px',
 859                left: '50%',
 860                transform: 'translateX(-50%)',
 861                width: '84px',
 862                height: '84px',
 863                borderRadius: '12px',
 864                background: 'rgba(0, 0, 0, 0.1)',
 865                border: 'none',
 866                backdropFilter: 'blur(4px)',
 867                display: 'flex',
 868                alignItems: 'center',
 869                justifyContent: 'center',
 870                cursor: 'pointer !important',
 871                fontSize: '12px',
 872                color: '#fff',
 873                boxShadow: '0 2px 12px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.1)',
 874                filter: 'drop-shadow(0 0 4px rgba(255, 255, 255, 0.3))',
 875                transition: 'all 0.2s ease',
 876                zIndex: 20,
 877                pointerEvents: 'auto'
 878              }}
 879              onClick={(e) => {
 880                e.stopPropagation(); // Prevent event from bubbling to node
 881                handleFlipClick(e);
 882              }}
 883              onMouseEnter={(e) => {
 884                e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
 885                e.currentTarget.style.transform = 'translateX(-50%) scale(1.1)';
 886              }}
 887              onMouseLeave={(e) => {
 888                e.currentTarget.style.background = 'rgba(0, 0, 0, 0.1)';
 889                e.currentTarget.style.transform = 'translateX(-50%) scale(1)';
 890              }}
 891              ref={(el) => {
 892                if (el) {
 893                  // Clear existing content and add Obsidian icon
 894                  el.innerHTML = '';
 895                  setIcon(el, 'lucide-flip-horizontal');
 896                  // Scale icon for larger button
 897                  const iconElement = el.querySelector('.lucide-flip-horizontal');
 898                  if (iconElement) {
 899                    (iconElement as HTMLElement).style.width = '36px';
 900                    (iconElement as HTMLElement).style.height = '36px';
 901                  }
 902                }
 903              }}
 904            >
 905            </div>
 906          )}
 907            </div>
 908  
 909            {/* Back side (DreamSong) - rotated 180 degrees with Z offset */}
 910            <div
 911              style={{
 912                position: 'absolute',
 913                width: '100%',
 914                height: '100%',
 915                borderRadius: dreamNodeStyles.dimensions.borderRadius,
 916                border: `${borderWidth}px ${gitStyle.borderStyle} ${nodeColors.border}`,
 917                background: nodeColors.fill,
 918                overflow: 'hidden',
 919                cursor: 'pointer !important',
 920                transition: `${dreamNodeStyles.transitions.default}, ${dreamNodeStyles.transitions.gitState}`,
 921                transform: `rotateY(180deg) translateZ(-2px) ${isHovered ? `scale(${dreamNodeStyles.states.hover.scale})` : 'scale(1)'}`,
 922                animation: gitStyle.animation,
 923                backfaceVisibility: 'hidden',
 924                boxShadow: (() => {
 925                  // Priority 1: Git status glow (always highest priority)
 926                  if (gitStyle.glowIntensity > 0) {
 927                    return getGitGlow(gitState, gitStyle.glowIntensity);
 928                  }
 929                  
 930                  // Priority 2: Edit mode relationship glow
 931                  if (isEditModeActive && isPendingRelationship) {
 932                    return getEditModeGlow(25); // Strong gold glow for relationships
 933                  }
 934                  
 935                  // Priority 3: Hover glow (fallback)
 936                  return isHovered ? getNodeGlow(dreamNode.type, dreamNodeStyles.states.hover.glowIntensity) : 'none';
 937                })()
 938              }}
 939              onMouseEnter={handleMouseEnter}
 940              onMouseLeave={handleMouseLeave}
 941              onClick={handleClick}
 942              onDoubleClick={handleDoubleClick}
 943            >
 944              {/* DreamSong content */}
 945              {dreamSongData ? (
 946                <DreamSong 
 947                  dreamSongData={dreamSongData}
 948                  className="flip-enter"
 949                  maxHeight={`${nodeSize}px`}
 950                />
 951              ) : isLoadingDreamSong ? (
 952                <div
 953                  style={{
 954                    width: '100%',
 955                    height: '100%',
 956                    display: 'flex',
 957                    alignItems: 'center',
 958                    justifyContent: 'center',
 959                    color: dreamNodeStyles.colors.text.primary
 960                  }}
 961                >
 962                  Loading DreamSong...
 963                </div>
 964              ) : (
 965                <div
 966                  style={{
 967                    width: '100%',
 968                    height: '100%',
 969                    display: 'flex',
 970                    alignItems: 'center',
 971                    justifyContent: 'center',
 972                    color: dreamNodeStyles.colors.text.primary
 973                  }}
 974                >
 975                  No DreamSong available
 976                </div>
 977              )}
 978  
 979              {/* Flip button (bottom-center, on back side) */}
 980              {shouldShowFlipButton && (
 981                <div
 982                  style={{
 983                    position: 'absolute',
 984                    bottom: '8px',
 985                    left: '50%',
 986                    transform: 'translateX(-50%)',
 987                    width: '84px',
 988                    height: '84px',
 989                    borderRadius: '12px',
 990                    background: 'rgba(0, 0, 0, 0.1)',
 991                    border: 'none',
 992                    backdropFilter: 'blur(4px)',
 993                    display: 'flex',
 994                    alignItems: 'center',
 995                    justifyContent: 'center',
 996                    cursor: 'pointer !important',
 997                    fontSize: '12px',
 998                    color: '#fff',
 999                    boxShadow: '0 2px 12px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.1)',
1000                    filter: 'drop-shadow(0 0 4px rgba(255, 255, 255, 0.3))',
1001                    transition: 'all 0.2s ease',
1002                    zIndex: 20,
1003                pointerEvents: 'auto'
1004                  }}
1005                  onClick={(e) => {
1006                    e.stopPropagation(); // Prevent event from bubbling to node
1007                    handleFlipClick(e);
1008                  }}
1009                  onMouseEnter={(e) => {
1010                    e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
1011                    e.currentTarget.style.transform = 'translateX(-50%) scale(1.1)';
1012                  }}
1013                  onMouseLeave={(e) => {
1014                    e.currentTarget.style.background = 'rgba(0, 0, 0, 0.1)';
1015                    e.currentTarget.style.transform = 'translateX(-50%) scale(1)';
1016                  }}
1017                  ref={(el) => {
1018                    if (el) {
1019                      // Clear existing content and add Obsidian icon
1020                      el.innerHTML = '';
1021                      setIcon(el, 'lucide-flip-horizontal');
1022                      // Scale icon for larger button
1023                      const iconElement = el.querySelector('.lucide-flip-horizontal');
1024                      if (iconElement) {
1025                        (iconElement as HTMLElement).style.width = '36px';
1026                        (iconElement as HTMLElement).style.height = '36px';
1027                      }
1028                    }
1029                  }}
1030                >
1031                </div>
1032              )}
1033            </div>
1034          </div>
1035        </Html>
1036      
1037      {/* Invisible hit detection sphere - travels with visual node as unified object */}
1038      <mesh 
1039        ref={hitSphereRef}
1040        position={[0, 0, 0]}
1041        userData={{ dreamNodeId: dreamNode.id, dreamNode: dreamNode }}
1042      >
1043        <sphereGeometry args={[12, 8, 8]} />
1044        <meshBasicMaterial 
1045          transparent={true} 
1046          opacity={0}
1047        />
1048      </mesh>
1049    </group>
1050    );
1051  });
1052  
1053  DreamNode3D.displayName = 'DreamNode3D';
1054  
1055  export default DreamNode3D;
1056  
1057  /**
1058   * Renders different types of media in the DreamTalk circle
1059   */
1060  function MediaRenderer({ media }: { media: MediaFile }) {
1061    const mediaStyle = {
1062      width: '100%',
1063      height: '100%',
1064      objectFit: 'cover' as const,
1065      borderRadius: '50%'
1066    };
1067  
1068    if (media.type.startsWith('image/')) {
1069      return (
1070        <img 
1071          src={media.data} 
1072          alt="DreamTalk symbol"
1073          style={mediaStyle}
1074          draggable={false}
1075        />
1076      );
1077    }
1078  
1079    if (media.type.startsWith('video/')) {
1080      return (
1081        <video 
1082          src={media.data}
1083          style={mediaStyle}
1084          muted
1085          loop
1086          autoPlay
1087          playsInline
1088        />
1089      );
1090    }
1091  
1092    if (media.type.startsWith('audio/')) {
1093      return (
1094        <div
1095          style={{
1096            ...mediaStyle,
1097            display: 'flex',
1098            alignItems: 'center',
1099            justifyContent: 'center',
1100            background: 'rgba(0, 0, 0, 0.8)'
1101          }}
1102        >
1103          <audio 
1104            controls 
1105            src={media.data}
1106            style={{ 
1107              width: '90%', 
1108              maxWidth: '80px',
1109              filter: 'invert(1)'
1110            }}
1111          />
1112        </div>
1113      );
1114    }
1115  
1116    return (
1117      <div
1118        style={{
1119          ...mediaStyle,
1120          display: 'flex',
1121          alignItems: 'center',
1122          justifyContent: 'center',
1123          color: '#FFFFFF',
1124          fontSize: '10px',
1125          background: 'rgba(0, 0, 0, 0.8)'
1126        }}
1127      >
1128        {media.type}
1129      </div>
1130    );
1131  }