const { useState, useRef, useCallback, useMemo, useEffect } = React;

function GridLines({ totalSec, pxPerSec }) {
  const lines = useMemo(() => {
    const out = [];
    for (let t=0; t<=totalSec+60; t+=30) out.push({ t, major:t%60===0 });
    return out;
  }, [totalSec, pxPerSec]);
  return (
    <div style={{ position:'absolute', inset:0, pointerEvents:'none', zIndex:0 }}>
      {lines.map(({ t, major }) => (
        <div key={t} style={{ position:'absolute', left:0, right:0, top:t*pxPerSec, height:1, background:major?'rgba(22,20,15,0.09)':'rgba(22,20,15,0.045)' }} />
      ))}
    </div>
  );
}

function TimeRuler({ totalSec, pxPerSec, onSeek }) {
  const step  = pxPerSec > 1.5 ? 60 : pxPerSec > 0.8 ? 120 : 300;
  const minor = step / 2;
  const ticks = useMemo(() => {
    const out = [];
    for (let t=0; t<=totalSec+step; t+=minor) out.push({ t, major:t%step===0 });
    return out;
  }, [totalSec, step, minor]);
  const handleClick = e => {
    if (!onSeek) return;
    const rect = e.currentTarget.getBoundingClientRect();
    const sec = (e.clientY - rect.top) / pxPerSec;
    onSeek(sec);
  };
  return (
    <div onClick={handleClick}
      title={onSeek ? 'Klikk for å sette spillehode' : undefined}
      style={{ width:56, minWidth:56, flexShrink:0, position:'relative', borderRight:`1px solid ${PC.line}`, height:(totalSec+60)*pxPerSec, background:PC.bg, cursor: onSeek ? 'pointer' : 'default' }}>
      {ticks.map(({ t, major }) => (
        <div key={t} style={{ position:'absolute', top:t*pxPerSec, left:0, right:0 }}>
          {major && <span style={{ position:'absolute', right:10, top:0, transform:'translateY(-50%)', fontFamily:"'Space Mono',monospace", fontSize:10, color:PC.muted, whiteSpace:'nowrap', pointerEvents:'none' }}>{fmt(t)}</span>}
          <div style={{ position:'absolute', right:0, top:0, width:major?12:5, height:1, background:major?PC.line2:PC.line, pointerEvents:'none' }} />
        </div>
      ))}
    </div>
  );
}

function PartBlock({ part, topPx, heightPx, selected, onSelect, onPartDragStart, isDragging }) {
  const col = ptColor(part.type);
  const isDraggingRef = useRef(false);

  const handleDragHandle = useCallback(e => {
    e.stopPropagation(); e.preventDefault();
    const startY = e.clientY;
    const onMove = me => {
      if (Math.abs(me.clientY - startY) > 6) {
        window.removeEventListener('mousemove', onMove);
        window.removeEventListener('mouseup', onUp);
        isDraggingRef.current = true;
        onPartDragStart && onPartDragStart(part.id, me);
      }
    };
    const onUp = () => {
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
      requestAnimationFrame(() => { isDraggingRef.current = false; });
    };
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
  }, [part.id, onPartDragStart]);

  return (
    <div
      onClick={e => { e.stopPropagation(); if (!isDraggingRef.current) onSelect(); }}
      style={{
        position:'absolute', top:topPx+1, left:4, right:4, height:Math.max(heightPx-2,8),
        background:`${col}14`, border:`1px solid ${col}40`, borderTop:`3px solid ${col}`,
        borderRadius:4, padding:'8px 10px', overflow:'hidden', cursor:'pointer',
        boxShadow: selected ? `0 0 0 2px ${PC.accent}` : 'none', zIndex:2,
        opacity: isDragging ? 0.35 : 1,
        transition: 'opacity 80ms',
      }}>
      {heightPx > 24 && (
        <div onMouseDown={handleDragHandle}
          title="Dra for å sortere"
          style={{ position:'absolute', top:5, right:6, cursor:'grab', color:`${col}99`, fontSize:13, lineHeight:1, userSelect:'none', zIndex:3, padding:'2px 3px' }}>
          ⠿
        </div>
      )}
      {heightPx>16 && <div style={{ fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:9, letterSpacing:'0.14em', textTransform:'uppercase', color:col }}>{ptLabel(part.type)}</div>}
      {heightPx>32 && <div style={{ fontFamily:"'Archivo',sans-serif", fontWeight:800, fontSize:13, letterSpacing:'-0.01em', marginTop:4, color:PC.ink, textTransform:'uppercase', overflow:'hidden', display:'-webkit-box', WebkitLineClamp:2, WebkitBoxOrient:'vertical' }}>{part.name}</div>}
      {heightPx>56 && <div style={{ fontFamily:"'Space Mono',monospace", fontSize:9, color:PC.muted, marginTop:4 }}>{fmt(partDur(part))} · {part.intervals.length} iv</div>}
    </div>
  );
}

function IvBlock({ iv, topPx, heightPx, partId, pxPerSec, selected, onSelect, dispatch, silentUpdate, setSelection, onDragStart, isDragging }) {
  const col   = zc(iv.zone);
  const ssCol = ssColor(iv.sitStand||'sit');
  const ssLbl = ssLabel(iv.sitStand||'sit');
  const isDraggingRef = useRef(false);

  const startResize = useCallback(e => {
    e.stopPropagation(); e.preventDefault();
    const startY = e.clientY, origDur = iv.duration;
    const apply = (me, commit) => {
      const d = Math.max(10, Math.round(origDur + (me.clientY-startY)/pxPerSec));
      const fn = s => { const p=s.parts.find(x=>x.id===partId); if(p){const i=p.intervals.find(x=>x.id===iv.id);if(i)i.duration=d;} };
      commit ? dispatch(fn) : silentUpdate(fn);
    };
    const onMove = me => apply(me, false);
    const onUp   = me => { apply(me, true); window.removeEventListener('mousemove',onMove); window.removeEventListener('mouseup',onUp); };
    window.addEventListener('mousemove',onMove); window.addEventListener('mouseup',onUp);
  }, [iv.id, iv.duration, partId, pxPerSec, dispatch, silentUpdate]);

  const handleMouseDown = useCallback(e => {
    if (e.target.dataset && e.target.dataset.resize) return;

    if (e.ctrlKey || e.metaKey) {
      e.stopPropagation(); e.preventDefault();
      const nid = uid();
      dispatch(s => {
        const p = s.parts.find(x=>x.id===partId); if(!p) return;
        const idx = p.intervals.findIndex(x=>x.id===iv.id); if (idx<0) return;
        p.intervals.splice(idx+1, 0, { ...deepCopy(iv), id:nid });
      });
      setSelection && setSelection({ type:'interval', id:nid, partId });
      const startY = e.clientY, origDur = iv.duration;
      const apply = (me, commit) => {
        const d = Math.max(10, Math.round(origDur + (me.clientY-startY)/pxPerSec));
        const fn = s => { const p=s.parts.find(x=>x.id===partId); if(p){const i=p.intervals.find(x=>x.id===nid);if(i)i.duration=d;} };
        commit ? dispatch(fn) : silentUpdate(fn);
      };
      const onMove = me => apply(me, false);
      const onUp   = me => { apply(me, true); window.removeEventListener('mousemove',onMove); window.removeEventListener('mouseup',onUp); };
      window.addEventListener('mousemove',onMove); window.addEventListener('mouseup',onUp);
      return;
    }

    e.preventDefault(); // prevent text selection during drag
    // Drag detection — threshold 6px
    isDraggingRef.current = false;
    const startY = e.clientY;
    const onMove = me => {
      if (Math.abs(me.clientY - startY) > 6) {
        isDraggingRef.current = true;
        window.removeEventListener('mousemove', onMove);
        window.removeEventListener('mouseup', onUp);
        onDragStart && onDragStart(iv.id, partId, me);
      }
    };
    const onUp = () => {
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
      requestAnimationFrame(() => { isDraggingRef.current = false; });
    };
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
  }, [iv.id, iv.duration, partId, pxPerSec, dispatch, silentUpdate, setSelection, onDragStart]);

  return (
    <div
      onClick={e => { e.stopPropagation(); if (!isDraggingRef.current) onSelect(); }}
      onMouseDown={handleMouseDown}
      style={{
        position:'absolute', top:topPx+1, left:4, right:4, height:Math.max(heightPx-2,10),
        background:`${col}14`, border:`1px solid ${col}40`, borderLeft:`3px solid ${col}`,
        borderRadius:4, padding:'4px 8px', overflow:'hidden', cursor:'grab', zIndex:2,
        boxShadow: selected ? `0 0 0 2px ${PC.accent}` : 'none',
        opacity: isDragging ? 0.3 : 1,
        transition: 'opacity 80ms',
      }}>
      {heightPx>12 && <div style={{ fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:11, letterSpacing:'0.03em', textTransform:'uppercase', color:col, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis', lineHeight:1.1 }}>{iv.name}</div>}
      {heightPx>28 && (
        <div style={{ display:'flex', gap:5, alignItems:'center', marginTop:3, flexWrap:'wrap' }}>
          <span style={{ padding:'1px 5px', borderRadius:3, background:`${ssCol}22`, color:ssCol, fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:9, letterSpacing:'0.06em' }}>{ssLbl}</span>
          <span style={{ fontFamily:"'Space Mono',monospace", fontSize:9, color:PC.muted }}>{fmt(iv.duration)}</span>
          {heightPx>42 && <span style={{ fontFamily:"'Space Mono',monospace", fontSize:9, color:PC.dim }}>{iv.bpm} BPM</span>}
        </div>
      )}
      <div onMouseDown={startResize} data-resize="1" style={{ position:'absolute', bottom:0, left:0, right:0, height:6, cursor:'ns-resize', zIndex:4 }}>
        <div style={{ position:'absolute', bottom:2, left:'50%', transform:'translateX(-50%)', width:18, height:2, borderRadius:1, background:`${col}99` }} />
      </div>
    </div>
  );
}

function SsColumn({ parts, pxPerSec, totalSec }) {
  const segs = useMemo(() => {
    const out=[]; let acc=0;
    for (const p of parts) for (const iv of p.intervals) {
      const ss = iv.sitStand||'sit';
      if (out.length && out[out.length-1].ss===ss) out[out.length-1].dur+=iv.duration;
      else out.push({ start:acc, dur:iv.duration, ss });
      acc += iv.duration;
    }
    return out;
  }, [parts]);
  return (
    <div style={{ position:'relative', height:(totalSec+60)*pxPerSec }}>
      {segs.map((seg,i) => {
        const col = ssColor(seg.ss);
        const h   = Math.max(seg.dur*pxPerSec-2, 4);
        return (
          <div key={i} style={{ position:'absolute', top:seg.start*pxPerSec+1, left:3, right:3, height:h, background:`${col}14`, borderLeft:`3px solid ${col}`, borderRadius:3, display:'flex', alignItems:'center', justifyContent:'center', color:col, fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:h>36?9:0, textTransform:'uppercase', letterSpacing:'0.04em', overflow:'hidden' }}>
            {h>22 ? ssLabel(seg.ss) : ''}
          </div>
        );
      })}
    </div>
  );
}

function TrackBlock({ track, pxPerSec, selected, onSelect, dispatch, silentUpdate, boundaries }) {
  const top = track.startSec * pxPerSec;
  const h   = Math.max(track.durationSec * pxPerSec, 18);
  const snapThr = 12 / pxPerSec;

  const startDrag = useCallback(e => {
    if (e.target.dataset.resize) return;
    e.stopPropagation(); e.preventDefault();
    const startY=e.clientY, orig=track.startSec, origDur=track.durationSec;
    const apply=(me,commit)=>{
      const raw = Math.max(0, Math.round(orig+(me.clientY-startY)/pxPerSec));
      const s1 = snapSec(raw, boundaries, snapThr);
      const s2 = snapSec(raw+origDur, boundaries, snapThr);
      let v = raw;
      if (s1.snapped && (!s2.snapped || Math.abs(s1.sec-raw) <= Math.abs(s2.sec-(raw+origDur)))) v = s1.sec;
      else if (s2.snapped) v = s2.sec - origDur;
      v = Math.max(0, v);
      const fn=s=>{const t=s.tracks.find(x=>x.id===track.id);if(t)t.startSec=v;};
      commit?dispatch(fn):silentUpdate(fn);
    };
    const onMove=me=>apply(me,false);
    const onUp=me=>{apply(me,true);window.removeEventListener('mousemove',onMove);window.removeEventListener('mouseup',onUp);};
    window.addEventListener('mousemove',onMove); window.addEventListener('mouseup',onUp);
  }, [track.id, track.startSec, track.durationSec, pxPerSec, dispatch, silentUpdate, boundaries, snapThr]);

  const startResizeB = useCallback(e => {
    e.stopPropagation(); e.preventDefault();
    const startY=e.clientY, orig=track.durationSec, startSec=track.startSec;
    const apply=(me,commit)=>{
      const raw = Math.max(10, Math.round(orig+(me.clientY-startY)/pxPerSec));
      const snap = snapSec(startSec+raw, boundaries, snapThr);
      const v = snap.snapped ? Math.max(10, snap.sec - startSec) : raw;
      const fn=s=>{const t=s.tracks.find(x=>x.id===track.id);if(t)t.durationSec=v;};
      commit?dispatch(fn):silentUpdate(fn);
    };
    const onMove=me=>apply(me,false);
    const onUp=me=>{apply(me,true);window.removeEventListener('mousemove',onMove);window.removeEventListener('mouseup',onUp);};
    window.addEventListener('mousemove',onMove); window.addEventListener('mouseup',onUp);
  }, [track.id, track.durationSec, track.startSec, pxPerSec, dispatch, silentUpdate, boundaries, snapThr]);

  return (
    <div onClick={e=>{e.stopPropagation();onSelect();}} onMouseDown={startDrag} style={{ position:'absolute', top, left:4, right:4, height:h, background:PC.paper, border:`1px solid ${selected?PC.accent:PC.line2}`, borderRadius:4, overflow:'hidden', display:'flex', cursor:'grab', boxShadow:selected?`0 0 0 2px ${PC.accent}55`:'none', zIndex:3 }}>
      <div style={{ width:3, background:PC.accent, flexShrink:0 }} />
      <div style={{ flex:1, padding:'5px 9px', minWidth:0, display:'flex', flexDirection:'column', justifyContent:'center' }}>
        {h>14 && <div style={{ fontFamily:"'Archivo',sans-serif", fontSize:12, fontWeight:600, color:PC.ink, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{track.title}</div>}
        {h>28 && <div style={{ fontFamily:"'Archivo',sans-serif", fontSize:10, color:PC.muted, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{track.artist}</div>}
        {h>44 && <div style={{ fontFamily:"'Space Mono',monospace", fontSize:9, color:PC.dim, marginTop:1 }}>{track.bpm>0?`${track.bpm} BPM · `:''}{fmt(track.durationSec)}</div>}
      </div>
      <div data-resize="1" onMouseDown={startResizeB} style={{ position:'absolute', bottom:0, left:0, right:0, height:6, cursor:'ns-resize', zIndex:4 }}>
        <div style={{ position:'absolute', bottom:2, left:'50%', transform:'translateX(-50%)', width:18, height:2, borderRadius:1, background:`${PC.accent}cc` }} />
      </div>
    </div>
  );
}

// ─── Drag helpers ─────────────────────────────────────────────

function getInsertKey(clientY, flatLayout, scrollEl) {
  if (!scrollEl) return null;
  const rect = scrollEl.getBoundingClientRect();
  const canvasY = clientY - rect.top + scrollEl.scrollTop;

  for (const { part, ivs } of flatLayout) {
    for (let i = 0; i < ivs.length; i++) {
      const { iv, topPx, heightPx } = ivs[i];
      if (canvasY < topPx + heightPx / 2) return `${part.id}:${iv.id}`;
      if (i === ivs.length - 1 && canvasY <= topPx + heightPx) return `${part.id}:end`;
    }
  }
  const last = flatLayout[flatLayout.length - 1];
  return last ? `${last.part.id}:end` : null;
}

function getPartDropIdx(clientY, flatLayout, scrollEl) {
  if (!scrollEl) return flatLayout.length;
  const rect = scrollEl.getBoundingClientRect();
  const canvasY = clientY - rect.top + scrollEl.scrollTop;
  for (let i = 0; i < flatLayout.length; i++) {
    const { topPx, heightPx } = flatLayout[i];
    if (canvasY < topPx + heightPx / 2) return i;
  }
  return flatLayout.length;
}

// ─── Timeline ─────────────────────────────────────────────────

function Timeline({ session, pxPerSec, selection, setSelection, dispatch, silentUpdate, scrollRef, onDropTrack, playheadSec, onSeek, testPlaying }) {
  const total   = totalDur(session);
  const canvasH = Math.max((total+60)*pxPerSec, 400);
  const [dropY, setDropY] = useState(null);
  const boundaries = useMemo(() => intervalBoundaries(session), [session.parts]);
  const snapThr = 12 / pxPerSec;

  // Drag state
  const [ivDrag,   setIvDrag]   = useState(null); // { ivId, partId, insertKey, ghostX, ghostY }
  const [partDrag, setPartDrag] = useState(null); // { partId, dropIdx, ghostX, ghostY }
  const ivDragRef   = useRef(null); // latest drag state — read synchronously in onUp
  const partDragRef = useRef(null);
  const programColRef = useRef(null);
  const partsColRef   = useRef(null);

  const flatLayout = useMemo(() => {
    const out=[]; let pAcc=0;
    for (const p of session.parts) {
      const dur=partDur(p); const ivs=[]; let iAcc=pAcc;
      for (const iv of p.intervals) { ivs.push({ iv, topPx:iAcc*pxPerSec, heightPx:iv.duration*pxPerSec }); iAcc+=iv.duration; }
      out.push({ part:p, topPx:pAcc*pxPerSec, heightPx:dur*pxPerSec, ivs });
      pAcc+=dur;
    }
    return out;
  }, [session.parts, pxPerSec]);

  // ── Interval drag ──────────────────────────────────────────
  const handleIvDragStart = useCallback((ivId, partId, startEvent) => {
    document.body.style.userSelect = 'none';
    const key = getInsertKey(startEvent.clientY, flatLayout, scrollRef.current);
    const initDrag = { ivId, partId, insertKey: key, ghostX: startEvent.clientX, ghostY: startEvent.clientY };
    ivDragRef.current = initDrag;
    setIvDrag(initDrag);

    const onMove = e => {
      const k = getInsertKey(e.clientY, flatLayout, scrollRef.current);
      const d = { ivId, partId, insertKey: k, ghostX: e.clientX, ghostY: e.clientY };
      ivDragRef.current = d;
      setIvDrag(d);
    };
    const onUp = () => {
      document.body.style.userSelect = '';
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
      const latest = ivDragRef.current;
      ivDragRef.current = null;
      setIvDrag(null);
      if (!latest?.insertKey) return;
      const { insertKey, partId: srcPartId, ivId: srcIvId } = latest;
      dispatch(s => {
        const colonIdx = insertKey.indexOf(':');
        const dstPartId = insertKey.slice(0, colonIdx);
        const beforeId  = insertKey.slice(colonIdx + 1);
        const srcPart = s.parts.find(p => p.id === srcPartId); if (!srcPart) return;
        const ivIdx = srcPart.intervals.findIndex(iv => iv.id === srcIvId); if (ivIdx < 0) return;
        const samePartDrop = String(srcPartId) === dstPartId;
        if (samePartDrop) {
          const arr = srcPart.intervals;
          const dstIdx = beforeId === 'end' ? arr.length : arr.findIndex(iv => String(iv.id) === beforeId);
          if (dstIdx === ivIdx || dstIdx === ivIdx + 1) return;
        }
        const [movedIv] = srcPart.intervals.splice(ivIdx, 1);
        const dstPart = samePartDrop ? srcPart : s.parts.find(p => String(p.id) === dstPartId);
        if (!dstPart) { srcPart.intervals.splice(ivIdx, 0, movedIv); return; }
        if (beforeId === 'end') {
          dstPart.intervals.push(movedIv);
        } else {
          let ins = dstPart.intervals.findIndex(iv => String(iv.id) === beforeId);
          if (ins < 0) ins = dstPart.intervals.length;
          dstPart.intervals.splice(ins, 0, movedIv);
        }
      });
    };
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
  }, [flatLayout, dispatch, scrollRef]);

  // ── Part drag ──────────────────────────────────────────────
  const handlePartDragStart = useCallback((partId, startEvent) => {
    document.body.style.userSelect = 'none';
    const idx = getPartDropIdx(startEvent.clientY, flatLayout, scrollRef.current);
    const initDrag = { partId, dropIdx: idx, ghostX: startEvent.clientX, ghostY: startEvent.clientY };
    partDragRef.current = initDrag;
    setPartDrag(initDrag);

    const onMove = e => {
      const i = getPartDropIdx(e.clientY, flatLayout, scrollRef.current);
      const d = { partId, dropIdx: i, ghostX: e.clientX, ghostY: e.clientY };
      partDragRef.current = d;
      setPartDrag(d);
    };
    const onUp = () => {
      document.body.style.userSelect = '';
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
      const latest = partDragRef.current;
      partDragRef.current = null;
      setPartDrag(null);
      if (latest === null) return;
      dispatch(s => {
        const idx = s.parts.findIndex(p => p.id === latest.partId); if (idx < 0) return;
        const [part] = s.parts.splice(idx, 1);
        const ins = latest.dropIdx > idx ? latest.dropIdx - 1 : latest.dropIdx;
        s.parts.splice(Math.max(0, Math.min(s.parts.length, ins)), 0, part);
      });
    };
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
  }, [flatLayout, dispatch, scrollRef]);

  // ── Drop line positions ─────────────────────────────────────
  const ivInsertLineY = useMemo(() => {
    if (!ivDrag?.insertKey) return null;
    const colonIdx = ivDrag.insertKey.indexOf(':');
    const dstPartId = ivDrag.insertKey.slice(0, colonIdx);
    const beforeId  = ivDrag.insertKey.slice(colonIdx + 1);
    for (const { part, ivs } of flatLayout) {
      if (String(part.id) !== dstPartId) continue;
      if (beforeId === 'end') {
        const last = ivs[ivs.length - 1];
        return last ? last.topPx + last.heightPx + 1 : (flatLayout.find(x => String(x.part.id) === dstPartId)?.topPx || 0);
      }
      for (const { iv, topPx } of ivs) { if (String(iv.id) === beforeId) return topPx; }
    }
    return null;
  }, [ivDrag, flatLayout]);

  const partInsertLineY = useMemo(() => {
    if (!partDrag) return null;
    const idx = partDrag.dropIdx;
    if (idx === 0) return flatLayout[0]?.topPx ?? 0;
    if (idx >= flatLayout.length) {
      const last = flatLayout[flatLayout.length - 1];
      return last ? last.topPx + last.heightPx : 0;
    }
    return flatLayout[idx]?.topPx ?? null;
  }, [partDrag, flatLayout]);

  // ── Ghost interval ──────────────────────────────────────────
  const ivGhost = useMemo(() => {
    if (!ivDrag) return null;
    for (const { part, ivs } of flatLayout) {
      for (const { iv } of ivs) {
        if (iv.id === ivDrag.ivId) return { iv, col: zc(iv.zone) };
      }
    }
    return null;
  }, [ivDrag, flatLayout]);

  // ── Ghost part ──────────────────────────────────────────────
  const partGhost = useMemo(() => {
    if (!partDrag) return null;
    const part = session.parts.find(p => p.id === partDrag.partId);
    return part ? { part, col: ptColor(part.type) } : null;
  }, [partDrag, session.parts]);

  const hdrCell = (label, w, flex=false) => (
    <div style={{ width:w, minWidth:w, flex:flex?'1':'0 0 auto', display:'flex', alignItems:'center', justifyContent:'center', borderRight:`1px solid ${PC.line}`, fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:9, letterSpacing:'0.16em', textTransform:'uppercase', color:PC.muted }}>{label}</div>
  );

  return (
    <div style={{ flex:1, display:'flex', flexDirection:'column', overflow:'hidden', background:PC.bg, color:PC.ink }}>
      {/* Column headers */}
      <div style={{ display:'flex', height:34, background:PC.paper, borderBottom:`1px solid ${PC.line}`, flexShrink:0 }}>
        <div style={{ width:56, minWidth:56, borderRight:`1px solid ${PC.line}`, display:'flex', alignItems:'center', justifyContent:'center', fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:9, letterSpacing:'0.16em', textTransform:'uppercase', color:PC.muted }}>Tid</div>
        {hdrCell('Deler', 140)}
        {hdrCell('Program · Sone', 220)}
        {hdrCell('Pos.', 72)}
        {hdrCell('Musikk', null, true)}
      </div>

      {/* Scrollable body */}
      <div ref={scrollRef} style={{ flex:1, overflowY:'auto', overflowX:'auto', display:'flex', position:'relative' }}>
        <TimeRuler totalSec={total+60} pxPerSec={pxPerSec} onSeek={onSeek} />

        <div style={{ flex:1, position:'relative', height:canvasH, display:'flex', minWidth:530 }}>
          <GridLines totalSec={total+60} pxPerSec={pxPerSec} />

          {/* Parts col */}
          <div ref={partsColRef} style={{ width:140, minWidth:140, flexShrink:0, position:'relative', borderRight:`1px solid ${PC.line}`, height:canvasH, zIndex:1 }}>
            {flatLayout.map(({ part, topPx, heightPx }) => (
              <PartBlock key={part.id} part={part} topPx={topPx} heightPx={heightPx}
                selected={selection?.type==='part' && selection?.id===part.id}
                onSelect={()=>setSelection({type:'part',id:part.id})}
                onPartDragStart={handlePartDragStart}
                isDragging={partDrag?.partId === part.id} />
            ))}
            {/* Part insertion line */}
            {partInsertLineY !== null && (
              <div style={{ position:'absolute', left:2, right:2, top:partInsertLineY, height:2, background:PC.accent, borderRadius:1, boxShadow:`0 0 6px ${PC.accent}80`, pointerEvents:'none', zIndex:10 }} />
            )}
          </div>

          {/* Program col */}
          <div ref={programColRef} style={{ width:220, minWidth:220, flexShrink:0, position:'relative', borderRight:`1px solid ${PC.line}`, height:canvasH, zIndex:1 }}>
            {flatLayout.map(({ part, topPx },i) => i>0 && (
              <div key={`sep${part.id}`} style={{ position:'absolute', left:4, right:4, top:topPx, borderTop:`2px dashed ${ptColor(part.type)}55`, pointerEvents:'none' }} />
            ))}
            {flatLayout.map(({ part, ivs }) => ivs.map(({ iv, topPx, heightPx }) => (
              <IvBlock key={iv.id} iv={iv} topPx={topPx} heightPx={heightPx} partId={part.id} pxPerSec={pxPerSec}
                selected={selection?.type==='interval' && selection?.id===iv.id}
                onSelect={()=>setSelection({type:'interval',id:iv.id,partId:part.id})}
                setSelection={setSelection}
                dispatch={dispatch} silentUpdate={silentUpdate}
                onDragStart={handleIvDragStart}
                isDragging={ivDrag?.ivId === iv.id} />
            )))}
            {/* Interval insertion line */}
            {ivInsertLineY !== null && (
              <div style={{ position:'absolute', left:2, right:2, top:ivInsertLineY, height:2, background:PC.accent, borderRadius:1, boxShadow:`0 0 6px ${PC.accent}80`, pointerEvents:'none', zIndex:10 }} />
            )}
          </div>

          {/* Sit/stand col */}
          <div style={{ width:72, minWidth:72, flexShrink:0, borderRight:`1px solid ${PC.line}`, height:canvasH, zIndex:1 }}>
            <SsColumn parts={session.parts} pxPerSec={pxPerSec} totalSec={total+60} />
          </div>

          {/* Playhead — horizontal line across all columns at current test-run position */}
          {(playheadSec > 0 || testPlaying) && (
            <div
              onMouseDown={e => {
                e.preventDefault();
                document.body.style.userSelect = 'none';
                document.body.style.cursor = 'ns-resize';
                const startClientY = e.clientY;
                const startSec = playheadSec;
                const onMove = ev => {
                  const newSec = Math.max(0, Math.min(total, startSec + (ev.clientY - startClientY) / pxPerSec));
                  if (onSeek) onSeek(newSec, { phase: 'drag' });
                };
                const onUp = ev => {
                  document.body.style.userSelect = '';
                  document.body.style.cursor = '';
                  window.removeEventListener('mousemove', onMove);
                  window.removeEventListener('mouseup', onUp);
                  const newSec = Math.max(0, Math.min(total, startSec + (ev.clientY - startClientY) / pxPerSec));
                  if (onSeek) onSeek(newSec, { phase: 'end' });
                };
                window.addEventListener('mousemove', onMove);
                window.addEventListener('mouseup', onUp);
              }}
              title="Dra for å flytte spillehodet"
              style={{
                position:'absolute', left:0, right:0, top:playheadSec * pxPerSec - 7,
                height:16, cursor:'ns-resize', zIndex:20,
              }}>
              {/* Synlig 2px linje */}
              <div style={{
                position:'absolute', left:0, right:0, top:7, height:2, background:PC.accent,
                pointerEvents:'none',
                boxShadow: testPlaying ? `0 0 8px ${PC.accent}aa` : `0 0 4px ${PC.accent}66`,
              }} />
              {/* Pil-handle */}
              <div style={{ position:'absolute', left:-1, top:2, width:0, height:0, pointerEvents:'none',
                borderTop:`6px solid transparent`, borderBottom:`6px solid transparent`,
                borderLeft:`8px solid ${PC.accent}` }} />
              {/* Tids-pill */}
              <div style={{ position:'absolute', right:8, top:-15, background:PC.accent, color:'#fff',
                padding:'2px 8px', borderRadius:3, fontFamily:"'Space Mono',monospace", fontSize:11,
                fontWeight:700, whiteSpace:'nowrap', pointerEvents:'none' }}>
                {fmt(playheadSec)}
              </div>
            </div>
          )}

          {/* Music col */}
          <div style={{ flex:1, position:'relative', minWidth:160, height:canvasH, zIndex:1 }}
            onDragOver={e=>{
              if (!e.dataTransfer.types.includes('spotify')) return;
              e.preventDefault();
              const rect=e.currentTarget.getBoundingClientRect();
              const sec = Math.max(0, e.clientY - rect.top) / pxPerSec;
              const snap = snapSec(sec, boundaries, snapThr);
              setDropY(snap.sec * pxPerSec);
            }}
            onDragLeave={()=>setDropY(null)}
            onDrop={e=>{
              e.preventDefault();
              setDropY(null);
              const raw=e.dataTransfer.getData('spotify'); if(!raw) return;
              const data=JSON.parse(raw);
              const rect=e.currentTarget.getBoundingClientRect();
              const rawSec = Math.max(0, (e.clientY-rect.top)/pxPerSec);
              const snap = snapSec(rawSec, boundaries, snapThr);
              onDropTrack(data, Math.round(snap.sec));
            }}
            onClick={e=>{if(e.target===e.currentTarget)setSelection(null);}}>
            {session.tracks.map(tr => (
              <TrackBlock key={tr.id} track={tr} pxPerSec={pxPerSec}
                selected={selection?.type==='track' && selection?.id===tr.id}
                onSelect={()=>setSelection({type:'track',id:tr.id})}
                dispatch={dispatch} silentUpdate={silentUpdate} boundaries={boundaries} />
            ))}
            {dropY !== null && (
              <>
                <div style={{ position:'absolute', left:0, right:0, top:dropY, height:2, background:PC.accent, boxShadow:`0 0 8px ${PC.accent}55`, pointerEvents:'none', zIndex:5 }} />
                <div style={{ position:'absolute', left:8, top:dropY-22, background:PC.accent, color:'#fff', padding:'2px 8px', borderRadius:3, fontFamily:"'Space Mono',monospace", fontSize:11, fontWeight:700, pointerEvents:'none', zIndex:5, whiteSpace:'nowrap' }}>{fmt(dropY/pxPerSec)}</div>
              </>
            )}
          </div>
        </div>
      </div>

      {/* Zone legend */}
      <div style={{ height:32, display:'flex', alignItems:'center', gap:14, padding:'0 16px', background:PC.paper, borderTop:`1px solid ${PC.line}`, flexShrink:0 }}>
        {ZONES.map(z=>(
          <div key={z.id} style={{ display:'flex', alignItems:'center', gap:5 }}>
            <span style={{ width:8, height:8, borderRadius:2, background:z.color, flexShrink:0 }} />
            <span style={{ fontFamily:"'Archivo',sans-serif", fontWeight:600, fontSize:10, color:PC.muted }}>{z.name}</span>
          </div>
        ))}
      </div>

      {/* Drag ghosts — fixed position, follow cursor */}
      {ivGhost && ivDrag && (
        <div style={{ position:'fixed', top:ivDrag.ghostY - 18, left:ivDrag.ghostX - 100, width:208, pointerEvents:'none', zIndex:1000 }}>
          <div style={{ background:`${ivGhost.col}22`, border:`1px solid ${ivGhost.col}80`, borderLeft:`3px solid ${ivGhost.col}`, borderRadius:4, padding:'5px 8px', boxShadow:'0 4px 20px rgba(0,0,0,0.18)', opacity:0.9 }}>
            <div style={{ fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:11, color:ivGhost.col, textTransform:'uppercase', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{ivGhost.iv.name}</div>
            <div style={{ fontFamily:"'Space Mono',monospace", fontSize:9, color:PC.muted, marginTop:2 }}>{fmt(ivGhost.iv.duration)} · {ivGhost.iv.bpm} BPM</div>
          </div>
        </div>
      )}
      {partGhost && partDrag && (
        <div style={{ position:'fixed', top:partDrag.ghostY - 18, left:partDrag.ghostX - 60, width:128, pointerEvents:'none', zIndex:1000 }}>
          <div style={{ background:`${partGhost.col}14`, border:`1px solid ${partGhost.col}40`, borderTop:`3px solid ${partGhost.col}`, borderRadius:4, padding:'8px 10px', boxShadow:'0 4px 20px rgba(0,0,0,0.18)', opacity:0.9 }}>
            <div style={{ fontFamily:"'Archivo',sans-serif", fontSize:9, letterSpacing:'0.14em', textTransform:'uppercase', color:partGhost.col }}>{ptLabel(partGhost.part.type)}</div>
            <div style={{ fontFamily:"'Archivo',sans-serif", fontSize:12, fontWeight:800, color:PC.ink, marginTop:3, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{partGhost.part.name}</div>
          </div>
        </div>
      )}
    </div>
  );
}

Object.assign(window, { GridLines, TimeRuler, PartBlock, IvBlock, SsColumn, TrackBlock, Timeline });
