/* Timeline View (Screen G) — Vertical scrolling tweet timeline */

var TWEET_TYPE_COLORS = {
  original: 'var(--text-primary)',
  reply: 'var(--text-tertiary)',
  quote: 'var(--accent)',
  retweet: 'var(--text-secondary)',
};
var TWEET_TYPE_LABELS = {
  original: 'Original', reply: 'Reply', quote: 'Quote', retweet: 'RT',
};
/* rgba glows for hover halo */
var TWEET_TYPE_GLOW = {
  original: 'rgba(255,255,255,0.25)',
  reply: 'rgba(113,113,122,0.30)',
  quote: 'rgba(249, 115, 22,0.30)',
  retweet: 'rgba(161,161,170,0.25)',
};

function timeAgo(ts) {
  var end = window.TIMELINE_DATA.windowEnd;
  var d = end - ts;
  if (d < 60) return Math.floor(d) + 's';
  if (d < 3600) return Math.floor(d / 60) + 'm';
  if (d < 86400) return Math.floor(d / 3600) + 'h';
  return Math.floor(d / 86400) + 'd';
}
function fmtTweetTime(ts) {
  var d = new Date(ts * 1000);
  var h = d.getHours(), m = d.getMinutes();
  return (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m;
}
function fmtTweetDate(ts) {
  return new Date(ts * 1000).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}

/* Compact engagement number, e.g. 1.2K / 19.5M / 142. */
function fmtEngagement(n) {
  if (!n) return '0';
  if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
  if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
  return String(n);
}

/* ── Mute button (small ⊘ icon on each card; stops click-propagation so the
   card's own openOnX click doesn't fire) ── */
function MuteButton({ handle, onMute }) {
  var [hov, setHov] = React.useState(false);
  if (!onMute) return null;
  return (
    <button
      onClick={function (e) { e.stopPropagation(); onMute(handle); }}
      onMouseEnter={function () { setHov(true); }}
      onMouseLeave={function () { setHov(false); }}
      title={'Mute @' + handle + ' from this Timeline'}
      style={{
        flexShrink: 0,
        width: 18, height: 18, lineHeight: 1,
        border: 'none', borderRadius: 4,
        background: hov ? 'rgba(220,74,83,0.15)' : 'transparent',
        color: hov ? 'var(--rose)' : 'var(--text-disabled)',
        cursor: 'pointer', padding: 0,
        fontSize: 12, fontFamily: 'var(--mono)',
        transition: 'background .12s, color .12s',
      }}>
      ⊘
    </button>
  );
}

/* ── Tweet Card ── */
function TweetCard({ tweet, onMute }) {
  var [hov, setHov] = React.useState(false);
  var typeColor = TWEET_TYPE_COLORS[tweet.type] || 'var(--text-tertiary)';
  var isExt = tweet.isInExternal21;
  /* Highlight high-reach tweets — the >100K view ones are where the algorithm
     actually rewarded the cohort. Makes "where reach lives" pop visually. */
  var highReach = (tweet.views || 0) >= 100000;

  /* Open the canonical x.com URL in a new tab. Falls back to constructing one
     from handle + id if the export ever drops .url for some reason. */
  var openOnX = function () {
    var url = tweet.url || (tweet.handle && tweet.id ? 'https://x.com/' + tweet.handle + '/status/' + tweet.id : null);
    if (url) window.open(url, '_blank', 'noopener,noreferrer');
  };

  /* ── Retweet: two-layer nested card (Twitter-style) ── */
  if (tweet.type === 'retweet') {
    return (
      <div onClick={openOnX} onMouseEnter={function () { setHov(true); }} onMouseLeave={function () { setHov(false); }}
        style={{
          background: hov ? 'var(--bg-elevated)' : 'var(--bg-card)',
          borderTop: '1px solid ' + (hov ? 'var(--border-strong)' : 'var(--border-subtle)'),
          borderRight: '1px solid ' + (hov ? 'var(--border-strong)' : 'var(--border-subtle)'),
          borderBottom: '1px solid ' + (hov ? 'var(--border-strong)' : 'var(--border-subtle)'),
          borderLeft: '3px solid var(--text-secondary)',
          borderRadius: 12, padding: '14px 16px', transition: 'all .15s',
          boxShadow: isExt ? 'inset 0 0 0 1px rgba(249, 115, 22,0.12)' : 'none',
          cursor: 'pointer',
        }}>
        {/* Outer: who reposted */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 7, marginBottom: 10 }}>
          <span style={{ fontSize: 11, color: 'var(--text-secondary)' }}>↻</span>
          <Avatar name={tweet.name} handle={tweet.handle} size={20} />
          <span style={{ fontSize: 11, color: 'var(--text-secondary)' }}>
            <span style={{ fontFamily: 'var(--mono)', color: 'var(--text-primary)', fontWeight: 500 }}>@{tweet.handle}</span> reposted
          </span>
          <span style={{ marginLeft: 'auto', fontSize: 10, fontFamily: 'var(--mono)', color: 'var(--text-tertiary)', fontFeatureSettings: "'tnum' 1" }}>{timeAgo(tweet.ts)} ago</span>
          <MuteButton handle={tweet.handle} onMute={onMute} />
        </div>

        {/* Inner: embedded original tweet */}
        <div style={{ border: '1px solid var(--border-default)', borderRadius: 12, padding: '12px 14px', background: 'var(--bg-surface)' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 7, marginBottom: 6 }}>
            <Avatar name={tweet.retweetedName} handle={tweet.retweetedHandle} size={22} />
            <div style={{ minWidth: 0, overflow: 'hidden' }}>
              <span style={{ fontSize: 11, fontFamily: 'var(--mono)', color: 'var(--text-primary)', fontWeight: 500 }}>@{tweet.retweetedHandle}</span>
              {tweet.retweetedVerified && <span style={{ color: 'var(--blue)', fontSize: 8, marginLeft: 4 }}>●</span>}
              {tweet.retweetedName && <span style={{ fontSize: 11, color: 'var(--text-tertiary)', marginLeft: 6 }}>{tweet.retweetedName}</span>}
            </div>
          </div>
          <div style={{ fontSize: 13, color: 'var(--text-primary)', lineHeight: 1.55, textWrap: 'pretty', marginBottom: 8 }}>{tweet.retweetedText}</div>
          {/* Engagement shown here is the retweet's own view count — we don't
              fetch the original tweet's standalone metrics. */}
          {tweet.views > 0 && (
            <div style={{ display: 'flex', gap: 14, fontSize: 11, color: 'var(--text-tertiary)', fontFamily: 'var(--mono)', fontFeatureSettings: "'tnum' 1" }}>
              <span title={tweet.views.toLocaleString() + ' views on this repost'}
                style={{ color: highReach ? 'var(--accent)' : 'var(--text-tertiary)', fontWeight: highReach ? 600 : 400 }}>
                👁 {fmtEngagement(tweet.views)} <span style={{ opacity: 0.6 }}>on repost</span>
              </span>
            </div>
          )}
        </div>
      </div>
    );
  }

  return (
    <div onClick={openOnX} onMouseEnter={function () { setHov(true); }} onMouseLeave={function () { setHov(false); }}
      style={{
        background: hov ? 'var(--bg-elevated)' : 'var(--bg-card)',
        borderTop: '1px solid ' + (hov ? 'var(--border-strong)' : 'var(--border-subtle)'),
        borderRight: '1px solid ' + (hov ? 'var(--border-strong)' : 'var(--border-subtle)'),
        borderBottom: '1px solid ' + (hov ? 'var(--border-strong)' : 'var(--border-subtle)'),
        borderLeft: '3px solid ' + typeColor,
        borderRadius: 12, padding: '14px 16px', transition: 'all .15s',
        boxShadow: isExt ? 'inset 0 0 0 1px rgba(249, 115, 22,0.12)' : 'none',
        cursor: 'pointer',
      }}>
      {/* Header: avatar + handle + time + mute */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
        <Avatar name={tweet.name} handle={tweet.handle} size={24} />
        <div style={{ flex: 1, minWidth: 0, overflow: 'hidden' }}>
          <span style={{ fontSize: 12, fontFamily: 'var(--mono)', color: 'var(--text-primary)', fontWeight: 500 }}>@{tweet.handle}</span>
          {tweet.name && <span style={{ fontSize: 11, color: 'var(--text-tertiary)', marginLeft: 6 }}>{tweet.name}</span>}
        </div>
        <span style={{ fontSize: 10, fontFamily: 'var(--mono)', color: 'var(--text-tertiary)', fontFeatureSettings: "'tnum' 1", flexShrink: 0 }}>{timeAgo(tweet.ts)} ago</span>
        <MuteButton handle={tweet.handle} onMute={onMute} />
      </div>

      {/* Type indicator */}
      {tweet.type === 'reply' && tweet.replyToHandle && (
        <div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 }}>
          <span style={{ fontSize: 12 }}>↩</span> Reply to <span style={{ fontFamily: 'var(--mono)', color: 'var(--text-secondary)' }}>@{tweet.replyToHandle}</span>
        </div>
      )}
      {tweet.type === 'quote' && tweet.quotedHandle && (
        <div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginBottom: 6, display: 'flex', alignItems: 'center', gap: 4 }}>
          <span style={{ fontSize: 12 }}>📎</span> Quoting <span style={{ fontFamily: 'var(--mono)', color: 'var(--text-secondary)' }}>@{tweet.quotedHandle}</span>
        </div>
      )}

      {/* Text */}
      <div style={{ fontSize: 13, color: 'var(--text-primary)', lineHeight: 1.55, textWrap: 'pretty', marginBottom: 8 }}>
        {tweet.text}
      </div>

      {/* Engagement */}
      <div style={{ display: 'flex', gap: 14, fontSize: 11, color: 'var(--text-tertiary)', fontFamily: 'var(--mono)', fontFeatureSettings: "'tnum' 1", alignItems: 'center' }}>
        <span>♥ {fmtEngagement(tweet.likes)}</span>
        <span>⟳ {fmtEngagement(tweet.retweets)}</span>
        <span>↩ {fmtEngagement(tweet.replies)}</span>
        {tweet.views > 0 && (
          <span style={{
            marginLeft: 'auto',
            color: highReach ? 'var(--accent)' : 'var(--text-tertiary)',
            fontWeight: highReach ? 600 : 400,
          }} title={tweet.views.toLocaleString() + ' views'}>
            👁 {fmtEngagement(tweet.views)}
          </span>
        )}
      </div>
    </div>
  );
}

/* ── Fold Marker — collapses 3+ consecutive same-author cards into one row.
   Click to expand, click again to collapse. Sits flush with the centre line
   so it visually replaces the cards it's hiding without breaking the rhythm. */
function FoldMarker({ handle, count, spanMin, expanded, onToggle }) {
  var [hov, setHov] = React.useState(false);
  var subtext = expanded
    ? 'collapse'
    : (spanMin != null && spanMin > 0)
      ? '· ' + (spanMin < 60 ? spanMin + 'm' : Math.round(spanMin / 60) + 'h') + ' stretch'
      : '';
  return (
    <div style={{ display: 'flex', justifyContent: 'center', position: 'relative', zIndex: 2, margin: '6px 0' }}>
      <button onClick={onToggle}
        onMouseEnter={function () { setHov(true); }} onMouseLeave={function () { setHov(false); }}
        style={{
          display: 'inline-flex', alignItems: 'center', gap: 8,
          padding: '4px 12px', borderRadius: 12,
          background: hov ? 'var(--bg-elevated)' : 'var(--bg-card)',
          border: '1px dashed ' + (hov ? 'var(--border-strong)' : 'var(--border-default)'),
          fontSize: 11, fontFamily: 'var(--mono)', color: 'var(--text-secondary)',
          fontFeatureSettings: "'tnum' 1", cursor: 'pointer',
          transition: 'background .15s, border-color .15s',
        }}>
        <span style={{ fontSize: 9, color: 'var(--text-tertiary)' }}>{expanded ? '▲' : '▼'}</span>
        {expanded
          ? <span><span style={{ color: 'var(--text-tertiary)' }}>collapse</span> {count} more from <span style={{ color: 'var(--text-primary)', fontWeight: 500 }}>@{handle}</span></span>
          : <span><span style={{ color: 'var(--accent)', fontWeight: 600 }}>+{count}</span> more from <span style={{ color: 'var(--text-primary)', fontWeight: 500 }}>@{handle}</span> {subtext && <span style={{ color: 'var(--text-tertiary)' }}>{subtext}</span>}</span>
        }
      </button>
    </div>
  );
}

/* ── Time Gap Marker ── */
function TimeGap({ hours }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '6px 0' }}>
      <div style={{ fontSize: 10, color: 'var(--text-disabled)', fontFamily: 'var(--mono)', fontFeatureSettings: "'tnum' 1", background: 'var(--bg-base)', padding: '2px 10px', borderRadius: 10, border: '1px solid var(--border-subtle)' }}>
        {hours < 1 ? Math.round(hours * 60) + 'm quiet' : Math.round(hours) + 'h quiet'}
      </div>
    </div>
  );
}

/* ── Filter Chip (local) ── */
function TLFilterChip({ label, active, onClick, color }) {
  return (
    <button onClick={onClick} style={{
      padding: '4px 10px', borderRadius: 6,
      border: active ? '1px solid ' + (color || 'var(--emerald)') : '1px solid var(--border-default)',
      background: active ? (color || 'var(--emerald)') + '18' : 'transparent',
      color: active ? (color || 'var(--emerald)') : 'var(--text-secondary)',
      fontSize: 11, fontWeight: 500, cursor: 'pointer', fontFamily: 'var(--sans)',
      transition: 'all .12s', whiteSpace: 'nowrap',
    }}>{label}</button>
  );
}

/* ═══════ TYPE DOT (shape + colour encode tweet type) ═══════ */
function TypeDot({ type, bright }) {
  var c = TWEET_TYPE_COLORS[type] || 'var(--text-tertiary)';
  var g = TWEET_TYPE_GLOW[type] || 'rgba(255,255,255,0.25)';
  var glow = bright ? ', 0 0 0 4px ' + g : '';
  if (type === 'reply') {
    /* hollow ring — a reaction, not a source */
    return <div style={{ width: 11, height: 11, borderRadius: '50%', background: 'var(--bg-base)', border: '2px solid ' + c, boxShadow: bright ? '0 0 0 4px ' + g : 'none', transition: 'box-shadow .15s' }}></div>;
  }
  if (type === 'quote') {
    /* diamond */
    return <div style={{ width: 10, height: 10, background: c, border: '2px solid var(--bg-base)', transform: 'rotate(45deg)', boxShadow: '0 0 0 1px ' + c + glow, transition: 'box-shadow .15s' }}></div>;
  }
  if (type === 'retweet') {
    /* double ring — a repost echoing someone else */
    return <div style={{ width: 11, height: 11, borderRadius: '50%', background: c, border: '2px solid var(--bg-base)', boxShadow: '0 0 0 2px ' + c + glow, transition: 'box-shadow .15s' }}></div>;
  }
  /* original — solid dot */
  return <div style={{ width: 11, height: 11, borderRadius: '50%', background: c, border: '2px solid var(--bg-base)', boxShadow: '0 0 0 1px ' + c + glow, transition: 'box-shadow .15s' }}></div>;
}

/* ═══════ TIMELINE ROW (zigzag card + type-coded connector to the spine) ═══════ */
function TimelineRow({ tweet, isLeft, onMute }) {
  var [hov, setHov] = React.useState(false);
  var c = TWEET_TYPE_COLORS[tweet.type] || 'var(--text-tertiary)';
  /* replies render dashed (reactive); originals/quotes/RTs solid */
  var connBg = tweet.type === 'reply'
    ? 'repeating-linear-gradient(to right, ' + c + ' 0 5px, transparent 5px 10px)'
    : c;
  var connStyle = {
    position: 'absolute', top: 15, height: 2, width: 46, background: connBg,
    opacity: hov ? 0.95 : 0.5, zIndex: 1, transition: 'opacity .15s',
  };
  if (isLeft) connStyle.right = '50%'; else connStyle.left = '50%';

  return (
    <div onMouseEnter={function () { setHov(true); }} onMouseLeave={function () { setHov(false); }}
      style={{ position: 'relative', display: 'flex', alignItems: 'flex-start', marginBottom: 12 }}>
      {/* Type-coded connector linking card → spine dot */}
      <div style={connStyle}></div>

      {/* Left column */}
      <div style={{ flex: 1, display: 'flex', justifyContent: 'flex-end', paddingRight: 24 }}>
        {isLeft && <div style={{ maxWidth: 360, width: '100%' }}><TweetCard tweet={tweet} onMute={onMute} /></div>}
      </div>

      {/* Center dot + time */}
      <div style={{ width: 48, flexShrink: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', paddingTop: 10, position: 'relative', zIndex: 2 }}>
        <TypeDot type={tweet.type} bright={hov} />
        <div style={{ fontSize: 9, fontFamily: 'var(--mono)', color: 'var(--text-disabled)', marginTop: 4, fontFeatureSettings: "'tnum' 1", whiteSpace: 'nowrap' }}>
          {fmtTweetTime(tweet.ts)}
        </div>
      </div>

      {/* Right column */}
      <div style={{ flex: 1, paddingLeft: 24 }}>
        {!isLeft && <div style={{ maxWidth: 360, width: '100%' }}><TweetCard tweet={tweet} onMute={onMute} /></div>}
      </div>
    </div>
  );
}

/* ═══════ MAIN TIMELINE VIEW ═══════ */
function TimelineView({ initialTopic, initialHandle }) {
  var [timeRange, setTimeRange] = React.useState(30);
  var [typeFilters, setTypeFilters] = React.useState({ original: true, reply: true, quote: true, retweet: true });
  var [minViews, setMinViews] = React.useState(0);
  var [handleQuery, setHandleQuery] = React.useState(initialHandle || '');
  /* Muted handles — authors the user wants HIDDEN from the timeline. Persists
     across reloads via localStorage so a noisy RT bot stays gone. Stored as
     an object {handle: true} for cheap lookup. */
  var [mutedHandles, setMutedHandles] = React.useState(function () {
    try { return JSON.parse(localStorage.getItem('xw-muted-handles') || '{}'); }
    catch (e) { return {}; }
  });
  function persistMuted(next) {
    try { localStorage.setItem('xw-muted-handles', JSON.stringify(next)); } catch (e) {}
  }
  function muteHandle(h) {
    setMutedHandles(function (p) {
      var n = Object.assign({}, p); n[h] = true; persistMuted(n); return n;
    });
    setVisibleCount(40);
  }
  function unmuteHandle(h) {
    setMutedHandles(function (p) {
      var n = Object.assign({}, p); delete n[h]; persistMuted(n); return n;
    });
    setVisibleCount(40);
  }
  function clearMutes() {
    setMutedHandles({}); persistMuted({}); setVisibleCount(40);
  }
  /* Topic filter — single slug, set when the user clicks a chip on Activity >
     Topics. Null = no topic filter. Stored as the bare slug (no #-prefix). */
  var [topicFilter, setTopicFilter] = React.useState(initialTopic ? initialTopic.replace(/^#/, '') : null);
  var [visibleCount, setVisibleCount] = React.useState(40);
  var scrollRef = React.useRef(null);

  /* Re-apply when the parent passes a new filter from a Topics-chip click. */
  React.useEffect(function () {
    if (initialTopic != null) { setTopicFilter(initialTopic.replace(/^#/, '')); setVisibleCount(40); }
    if (initialHandle != null) { setHandleQuery(initialHandle); setVisibleCount(40); }
  }, [initialTopic, initialHandle]);

  var toggleType = function (t) {
    setTypeFilters(function (p) { var n = Object.assign({}, p); n[t] = !n[t]; return n; });
    setVisibleCount(40);
  };

  var data = window.TIMELINE_DATA;

  var filtered = React.useMemo(function () {
    var cutoff = data.windowEnd - timeRange * 86400;
    var q = handleQuery.trim().toLowerCase().replace(/^@/, '');
    var tf = topicFilter;
    var mu = mutedHandles;
    return data.tweets.filter(function (t) {
      if (t.ts < cutoff || !typeFilters[t.type]) return false;
      if (mu[t.handle]) return false;   /* mute filter: skip tweets by silenced authors */
      if ((t.views || 0) < minViews) return false;  /* views filter: drop low-engagement posts */
      /* Topic gate — prefer LLM tags, fall back to legacy regex tags. */
      if (tf) {
        var tags = (t.topicTagsLLM && t.topicTagsLLM.length > 0) ? t.topicTagsLLM : (t.topicTags || []);
        var hit = false;
        for (var i = 0; i < tags.length; i++) {
          if (tags[i].replace(/^#/, '') === tf) { hit = true; break; }
        }
        if (!hit) return false;
      }
      if (!q) return true;
      var hay = ((t.handle || '') + ' ' + (t.name || '') + ' ' +
                 (t.replyToHandle || '') + ' ' + (t.quotedHandle || '') + ' ' +
                 (t.retweetedHandle || '')).toLowerCase();
      return hay.indexOf(q) >= 0;
    });
  }, [timeRange, typeFilters, handleQuery, topicFilter, mutedHandles, minViews]);

  var visible = filtered.slice(0, visibleCount);

  /* Same-author run folding. When 3+ consecutive cards share the same handle,
     we collapse the 2nd–Nth into a single "+N more from @user" marker so a
     spammy stretch (e.g. @yuki_arano's Japanese SwiftUI run) doesn't blanket
     the page. State: a Set keyed by the run's lead-tweet id. */
  var [expandedRuns, setExpandedRuns] = React.useState({});
  function toggleRun(key) {
    setExpandedRuns(function (p) { var n = Object.assign({}, p); if (n[key]) delete n[key]; else n[key] = true; return n; });
  }

  /* Pre-compute everything the renderer needs — gap/date labels, isLeft for
     the zigzag, fold markers. Keeps the JSX below a pure map. */
  var renderItems = React.useMemo(function () {
    var items = [];
    var prevTs = null;
    var prevDateStr = null;
    var tweetIdx = 0;

    function pushTweet(tw, inRun) {
      var gap = prevTs != null ? (prevTs - tw.ts) / 3600 : 0;
      var dateStr = fmtTweetDate(tw.ts);
      var showDate = prevDateStr == null || prevDateStr !== dateStr;
      items.push({
        kind: 'tweet', tweet: tw, gap: gap, showDate: showDate,
        isLeft: tweetIdx % 2 === 0, inRun: !!inRun,
      });
      tweetIdx++;
      prevTs = tw.ts;
      prevDateStr = dateStr;
    }

    var i = 0;
    while (i < visible.length) {
      var t = visible[i];
      var runEnd = i + 1;
      while (runEnd < visible.length && visible[runEnd].handle === t.handle) runEnd++;
      var runLen = runEnd - i;

      if (runLen >= 3) {
        pushTweet(t, false);                       /* always show the lead */
        var runKey = t.id;
        var hidden = visible.slice(i + 1, runEnd);
        if (expandedRuns[runKey]) {
          hidden.forEach(function (h) { pushTweet(h, true); });
          items.push({ kind: 'fold-close', runKey: runKey, handle: t.handle, count: hidden.length });
        } else {
          var spanMin = Math.round((hidden[0].ts - hidden[hidden.length - 1].ts) / 60);
          items.push({
            kind: 'fold-open', runKey: runKey, handle: t.handle,
            count: hidden.length, spanMin: spanMin, lastTs: hidden[hidden.length - 1].ts,
          });
          /* Skip the hidden tweets but keep gap/date math consistent: pretend
             the last tweet of the run was the most recent prior timestamp. */
          prevTs = hidden[hidden.length - 1].ts;
          prevDateStr = fmtTweetDate(prevTs);
        }
        i = runEnd;
      } else {
        pushTweet(t, false);
        i++;
      }
    }
    return items;
  }, [visible, expandedRuns]);

  /* Detect scroll to bottom for lazy loading */
  var onScroll = React.useCallback(function (e) {
    var el = e.target;
    if (el.scrollHeight - el.scrollTop - el.clientHeight < 300) {
      setVisibleCount(function (c) { return Math.min(c + 30, filtered.length); });
    }
  }, [filtered.length]);

  /* Detect time gaps (>2h between consecutive tweets) */
  function getGapHours(prev, curr) {
    return (prev.ts - curr.ts) / 3600;
  }

  return (
    <div style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
      {/* Sticky Toolbar */}
      <div style={{ padding: '10px 28px', borderBottom: '1px solid var(--border-subtle)', display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', flexShrink: 0, background: 'var(--bg-surface)' }}>
        {/* Time range */}
        {[{ v: 1, l: '24h' }, { v: 7, l: '7d' }, { v: 14, l: '14d' }, { v: 30, l: '30d' }].map(function (r) {
          return <TLFilterChip key={r.v} label={r.l} active={timeRange === r.v} onClick={function () { setTimeRange(r.v); setVisibleCount(40); }} />;
        })}

        <div style={{ width: 1, height: 20, background: 'var(--border-default)', margin: '0 4px' }}></div>

        {/* Type filters */}
        {[{ t: 'original', l: 'Originals', c: 'var(--text-primary)' },
          { t: 'reply', l: 'Replies', c: 'var(--text-tertiary)' },
          { t: 'quote', l: 'Quotes', c: 'var(--accent)' },
          { t: 'retweet', l: 'RTs', c: 'var(--text-secondary)' }
        ].map(function (f) {
          return <TLFilterChip key={f.t} label={f.l} active={typeFilters[f.t]} onClick={function () { toggleType(f.t); }} color={f.c} />;
        })}

        <div style={{ width: 1, height: 20, background: 'var(--border-default)', margin: '0 4px' }}></div>

        {/* Min views filter — hide low-'View' noise / low reach posts */}
        {[{ v: 0, l: 'All' }, { v: 100, l: '≥100' }, { v: 1000, l: '≥1K' }, { v: 10000, l: '≥10K' }, { v: 100000, l: '≥100K' }].map(function (m) {
          return <TLFilterChip key={m.v} label={m.l} active={minViews === m.v} onClick={function () { setMinViews(m.v); setVisibleCount(40); }} />;
        })}

        <div style={{ width: 1, height: 20, background: 'var(--border-default)', margin: '0 4px' }}></div>

        {/* Handle search — matches author, reply target, quote target, or RT'd handle */}
        <div style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
          <span style={{ position: 'absolute', left: 9, fontSize: 11, color: 'var(--text-tertiary)', pointerEvents: 'none' }}>🔍</span>
          <input
            type="text"
            value={handleQuery}
            onChange={function (e) { setHandleQuery(e.target.value); setVisibleCount(40); }}
            placeholder="filter handle…"
            style={{
              padding: '5px 28px 5px 26px',
              borderRadius: 6,
              border: '1px solid var(--border-default)',
              background: 'var(--bg-elevated)',
              color: 'var(--text-primary)',
              fontSize: 12,
              fontFamily: 'var(--mono)',
              width: 160,
              outline: 'none',
            }}
          />
          {handleQuery && (
            <button onClick={function () { setHandleQuery(''); setVisibleCount(40); }}
              title="clear"
              style={{ position: 'absolute', right: 6, border: 'none', background: 'transparent', color: 'var(--text-tertiary)', cursor: 'pointer', fontSize: 14, padding: 0, lineHeight: 1 }}>×</button>
          )}
        </div>

        {topicFilter && (
          <div style={{
            display: 'inline-flex', alignItems: 'center', gap: 6,
            padding: '4px 8px 4px 10px', borderRadius: 6,
            background: 'var(--accent-muted)', border: '1px solid rgba(249, 115, 22,0.35)',
            fontSize: 11, fontFamily: 'var(--mono)', color: 'var(--accent)',
          }}>
            topic: #{topicFilter}
            <button onClick={function () { setTopicFilter(null); setVisibleCount(40); }}
              title="clear topic filter"
              style={{ border: 'none', background: 'transparent', color: 'var(--accent)', cursor: 'pointer', fontSize: 14, padding: 0, lineHeight: 1 }}>×</button>
          </div>
        )}

        <div style={{ flex: 1 }}></div>
        <span style={{ fontSize: 12, color: 'var(--text-tertiary)', fontFamily: 'var(--mono)', fontFeatureSettings: "'tnum' 1" }}>
          {filtered.length} tweets
        </span>
      </div>

      {/* Muted-handles strip — only renders when there's at least one mute. */}
      {Object.keys(mutedHandles).length > 0 && (
        <div style={{ padding: '7px 28px', display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', flexShrink: 0, background: 'var(--bg-surface)', borderBottom: '1px solid var(--border-subtle)' }}>
          <span style={{ fontSize: 10, fontFamily: 'var(--mono)', color: 'var(--text-tertiary)', letterSpacing: '0.06em', textTransform: 'uppercase', fontWeight: 600 }}>
            Muted
          </span>
          {Object.keys(mutedHandles).map(function (h) {
            return (
              <span key={h} style={{
                display: 'inline-flex', alignItems: 'center', gap: 4,
                padding: '3px 6px 3px 8px', borderRadius: 4,
                background: 'rgba(220,74,83,0.10)', border: '1px solid rgba(220,74,83,0.25)',
                fontSize: 11, fontFamily: 'var(--mono)', color: 'var(--rose)',
              }}>
                @{h}
                <button onClick={function () { unmuteHandle(h); }}
                  title={'Unmute @' + h}
                  style={{ border: 'none', background: 'transparent', color: 'var(--rose)', cursor: 'pointer', fontSize: 13, padding: 0, lineHeight: 1, marginLeft: 2 }}>×</button>
              </span>
            );
          })}
          <button onClick={clearMutes}
            style={{ fontSize: 10, fontFamily: 'var(--mono)', color: 'var(--text-tertiary)', background: 'transparent', border: 'none', cursor: 'pointer', textDecoration: 'underline', padding: 0, marginLeft: 4 }}>
            clear all
          </button>
        </div>
      )}

      {/* Type legend — colour + shape key */}
      <div style={{ padding: '7px 28px', display: 'flex', gap: 18, alignItems: 'center', flexShrink: 0, background: 'var(--bg-surface)', borderBottom: '1px solid var(--border-subtle)' }}>
        {[['original', 'Original'], ['reply', 'Reply'], ['quote', 'Quote'], ['retweet', 'Retweet']].map(function (x) {
          return (
            <span key={x[0]} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 11, color: 'var(--text-secondary)' }}>
              <span style={{ display: 'flex', width: 14, height: 14, alignItems: 'center', justifyContent: 'center' }}><TypeDot type={x[0]} /></span>
              {x[1]}
            </span>
          );
        })}
        <span style={{ marginLeft: 'auto', fontSize: 10, color: 'var(--text-disabled)', fontStyle: 'italic' }}>connector &amp; dot encode each tweet’s type</span>
      </div>

      {/* Timeline Content */}
      <div ref={scrollRef} onScroll={onScroll} style={{ flex: 1, overflowY: 'auto', padding: '20px 0' }}>
        <div style={{ maxWidth: 900, margin: '0 auto', position: 'relative', padding: '0 20px' }}>
          {/* Center timeline line */}
          <div style={{ position: 'absolute', left: '50%', top: 0, bottom: 0, width: 2, background: 'var(--border-default)', transform: 'translateX(-50%)' }}></div>

          {/* NOW label */}
          <div style={{ textAlign: 'center', position: 'relative', zIndex: 2, marginBottom: 16 }}>
            <span style={{ fontSize: 10, fontWeight: 600, fontFamily: 'var(--mono)', color: 'var(--accent)', background: 'var(--bg-base)', padding: '2px 12px', borderRadius: 10, border: '1px solid var(--accent)', letterSpacing: 0.5 }}>▲ NOW</span>
          </div>

          {renderItems.map(function (item) {
            if (item.kind === 'fold-open') {
              return (
                <FoldMarker key={'fold-' + item.runKey}
                  handle={item.handle} count={item.count} spanMin={item.spanMin}
                  expanded={false}
                  onToggle={function () { toggleRun(item.runKey); }} />
              );
            }
            if (item.kind === 'fold-close') {
              return (
                <FoldMarker key={'fold-close-' + item.runKey}
                  handle={item.handle} count={item.count}
                  expanded={true}
                  onToggle={function () { toggleRun(item.runKey); }} />
              );
            }
            var tweet = item.tweet;
            return (
              <React.Fragment key={tweet.id}>
                {item.gap > 2 && <TimeGap hours={item.gap} />}

                {item.showDate && (
                  <div style={{ textAlign: 'center', position: 'relative', zIndex: 2, margin: '12px 0' }}>
                    <span style={{ fontSize: 10, fontFamily: 'var(--mono)', color: 'var(--text-tertiary)', background: 'var(--bg-base)', padding: '2px 10px', borderRadius: 10, border: '1px solid var(--border-subtle)' }}>{fmtTweetDate(tweet.ts)}</span>
                  </div>
                )}

                {/* inRun = part of an expanded same-author streak; render slightly dimmer to signal it's a continuation, not new content */}
                <div style={{ opacity: item.inRun ? 0.7 : 1, transition: 'opacity .15s' }}>
                  <TimelineRow tweet={tweet} isLeft={item.isLeft} onMute={muteHandle} />
                </div>
              </React.Fragment>
            );
          })}

          {/* Load more indicator */}
          {visibleCount < filtered.length && (
            <div style={{ textAlign: 'center', padding: '20px 0', position: 'relative', zIndex: 2 }}>
              <span style={{ fontSize: 11, color: 'var(--text-tertiary)', fontFamily: 'var(--mono)' }}>
                scroll for more · {filtered.length - visibleCount} remaining
              </span>
            </div>
          )}

          {/* END label */}
          {visibleCount >= filtered.length && filtered.length > 0 && (
            <div style={{ textAlign: 'center', position: 'relative', zIndex: 2, marginTop: 8 }}>
              <span style={{ fontSize: 10, fontFamily: 'var(--mono)', color: 'var(--text-disabled)', background: 'var(--bg-base)', padding: '2px 12px', borderRadius: 10, border: '1px solid var(--border-subtle)' }}>▼ END · {timeRange}d window</span>
            </div>
          )}

          {filtered.length === 0 && (
            <div style={{ textAlign: 'center', padding: '60px 20px', color: 'var(--text-tertiary)', fontSize: 14, position: 'relative', zIndex: 2 }}>
              No tweets match the current filters.
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

window.TimelineView = TimelineView;
