<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vladimir</title>
    <description>The latest articles on DEV Community by Vladimir (@vladimirpal).</description>
    <link>https://dev.to/vladimirpal</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F709242%2F93b41022-af8c-4582-b0f6-425b76e0abac.jpeg</url>
      <title>DEV Community: Vladimir</title>
      <link>https://dev.to/vladimirpal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vladimirpal"/>
    <language>en</language>
    <item>
      <title>I needed a collision engine for a React timeline. Here is how I built it.</title>
      <dc:creator>Vladimir</dc:creator>
      <pubDate>Wed, 29 Jul 2026 08:59:39 +0000</pubDate>
      <link>https://dev.to/vladimirpal/i-needed-a-collision-engine-for-a-react-timeline-here-is-how-i-built-it-19if</link>
      <guid>https://dev.to/vladimirpal/i-needed-a-collision-engine-for-a-react-timeline-here-is-how-i-built-it-19if</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/it-pal-net/react-timespace" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg06yx2nd4w5qmrqrgfm1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg06yx2nd4w5qmrqrgfm1.png" alt="Four time-zone rows sharing one live time marker" width="799" height="325"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When I started building Timespace in January 2024, I expected time zones to be&lt;br&gt;
the hard part.&lt;/p&gt;

&lt;p&gt;I was wrong. Time zones were difficult, but the problem that took the most&lt;br&gt;
thought was more visual: how do you stop several absolutely positioned DOM&lt;br&gt;
labels from covering one another while the user is dragging them?&lt;/p&gt;

&lt;p&gt;Timespace puts several local days on parallel timelines. Every horizontal&lt;br&gt;
position represents the same moment, so a user can drag one interval and see&lt;br&gt;
what it means in New York, London, Bangkok, or anywhere else.&lt;/p&gt;

&lt;p&gt;The hour grid was easy. The labels were trouble.&lt;/p&gt;

&lt;p&gt;There is a clock attached to the live "now" line, another clock on every&lt;br&gt;
interval endpoint, and a time-zone name at the edge of each row. Every label&lt;br&gt;
can sit on the left or right of its anchor. Its width changes with the font,&lt;br&gt;
theme, time format, and whether seconds are visible.&lt;/p&gt;

&lt;p&gt;Near the right edge, a clock can leave the container. Two nearby endpoints can&lt;br&gt;
cover each other. The current-time clock can run into an interval or the row&lt;br&gt;
name. Everything moves during a drag, and every pixel becomes stale after a&lt;br&gt;
resize.&lt;/p&gt;

&lt;p&gt;CSS could position the elements, but it could not make the decision I needed:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Try the other side of the anchor. Keep it there only if it fits and does not&lt;br&gt;
hit another label. If neither side works, move it into a vertical lane.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is how a timeline widget ended up with a small collision engine.&lt;/p&gt;
&lt;h2&gt;
  
  
  Time is the value; pixels are only the current view
&lt;/h2&gt;

&lt;p&gt;The first thing I needed was a stable coordinate model.&lt;/p&gt;

&lt;p&gt;A day contains 86,400 seconds. The timeline has a width measured from the real&lt;br&gt;
DOM. The conversion is linear:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SECONDS_IN_DAY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;secondsToX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;SECONDS_IN_DAY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;xToSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;SECONDS_IN_DAY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each interval stores two representations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;xPos1DayOffsetSeconds&lt;/code&gt; and &lt;code&gt;xPos2DayOffsetSeconds&lt;/code&gt; describe the time;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;xPos1&lt;/code&gt; and &lt;code&gt;xPos2&lt;/code&gt; describe where to render it right now.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the widget changes width, it regenerates the pixel positions from the&lt;br&gt;
seconds. A point at 14:00 stays at 14:00 instead of staying at an old&lt;br&gt;
x-coordinate.&lt;/p&gt;

&lt;p&gt;The production code also deals with two different offsets: the hour strip&lt;br&gt;
inside the component and the component inside the viewport. This detail caused&lt;br&gt;
some painful early bugs. CSS positions are local to the component, while&lt;br&gt;
pointer events report viewport coordinates. Collision detection only works&lt;br&gt;
when all of its inputs use the same coordinate space.&lt;/p&gt;

&lt;p&gt;The conversion helpers are in&lt;br&gt;
&lt;a href="https://github.com/it-pal-net/react-timespace/blob/main/core/timeLineMath.js" rel="noopener noreferrer"&gt;&lt;code&gt;core/timeLineMath.js&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Measuring instead of guessing
&lt;/h2&gt;

&lt;p&gt;I did not want to estimate clock widths from character counts. Different&lt;br&gt;
fonts, themes, and time formats make that unreliable.&lt;/p&gt;

&lt;p&gt;The widget renders sample labels and measures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the width and viewport position of the hour strip;&lt;/li&gt;
&lt;li&gt;the natural width of an interval clock;&lt;/li&gt;
&lt;li&gt;the natural width of the live clock;&lt;/li&gt;
&lt;li&gt;the widest row header;&lt;/li&gt;
&lt;li&gt;the height available for labels.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A targeted &lt;code&gt;ResizeObserver&lt;/code&gt; repeats the measurement when the container changes&lt;br&gt;
size. There is also a short-lived &lt;code&gt;MutationObserver&lt;/code&gt; for clock elements that&lt;br&gt;
are empty on the first render and receive text just after mounting.&lt;/p&gt;

&lt;p&gt;The collision resolver itself never queries the DOM. It receives those&lt;br&gt;
measurements as numbers and returns layout data. This keeps the geometry&lt;br&gt;
deterministic enough to test without mounting React.&lt;/p&gt;

&lt;p&gt;The measurement hook is&lt;br&gt;
&lt;a href="https://github.com/it-pal-net/react-timespace/blob/main/hooks/useTimeLineMeasurements.js" rel="noopener noreferrer"&gt;&lt;code&gt;useTimeLineMeasurements.js&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Turning labels into horizontal intervals
&lt;/h2&gt;

&lt;p&gt;For collision purposes, the important information is horizontal: the anchor&lt;br&gt;
position, the label width, and the side on which it is rendered.&lt;/p&gt;

&lt;p&gt;I reduce each label to a one-dimensional interval:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;boundary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;anchor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;side&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;side&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;left&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;anchor&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;anchor&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;anchor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;anchor&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;overlaps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;second&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;second&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why one dimension when DOM elements are two-dimensional?&lt;/p&gt;

&lt;p&gt;Horizontal position represents time, so it should be preserved whenever&lt;br&gt;
possible. Vertical movement has no meaning in the model. It is only a fallback&lt;br&gt;
when a horizontal conflict cannot be removed.&lt;/p&gt;

&lt;p&gt;The resolver starts with a preferred side: right. Keeping one default stops&lt;br&gt;
labels from moving without a reason.&lt;/p&gt;

&lt;p&gt;For each movable label, it does roughly this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build its boundary on the current side.&lt;/li&gt;
&lt;li&gt;Use the preferred side if it fits.&lt;/li&gt;
&lt;li&gt;If it crosses a timeline boundary, try the other side.&lt;/li&gt;
&lt;li&gt;If it overlaps another item, try the other side again.&lt;/li&gt;
&lt;li&gt;Accept that switch only when it stays inside the timeline and does not
create a new collision.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Moving a label to its other side is just translating it by its measured width:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;moveToOppositeSide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leftEdge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rightEdge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;side&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;left&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;rightEdge&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;right&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;leftEdge&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;left&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The time-zone header is an obstacle too, but its side is chosen from the live&lt;br&gt;
time position. As the "now" line approaches the header, the header moves to the&lt;br&gt;
other end of the row.&lt;/p&gt;

&lt;p&gt;I did not try to build a solver that flips labels repeatedly until it finds a&lt;br&gt;
global optimum. During a drag, predictable output is more useful than a&lt;br&gt;
slightly tighter layout that may jump between frames.&lt;/p&gt;

&lt;p&gt;The complete resolver is in&lt;br&gt;
&lt;a href="https://github.com/it-pal-net/react-timespace/blob/main/core/timeLineCollision.js" rel="noopener noreferrer"&gt;&lt;code&gt;core/timeLineCollision.js&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  When neither side is free
&lt;/h2&gt;

&lt;p&gt;Some collisions cannot be fixed horizontally. Several endpoints can represent&lt;br&gt;
the same time, or a label can be trapped between the live clock and the row&lt;br&gt;
header.&lt;/p&gt;

&lt;p&gt;The remaining horizontal bounds form an interval graph. I sort them by start&lt;br&gt;
position and assign vertical lanes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for each label from left to right:
    use the first lane whose previous label has already ended
    if no lane is free, create a new lane
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A lane can be reused as soon as its previous &lt;code&gt;end&lt;/code&gt; is less than or equal to the&lt;br&gt;
next &lt;code&gt;start&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is one extra stability rule. Consider this chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A overlaps B
B overlaps C
A does not overlap C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three belong to one connected horizontal group. I use one stack size for&lt;br&gt;
the group instead of calculating font size from each label's direct collision&lt;br&gt;
count. Otherwise B can become smaller than A and C even though they are all&lt;br&gt;
part of the same visual collision.&lt;/p&gt;

&lt;p&gt;The resolver returns the chosen side, overlapping indexes, lane number, and&lt;br&gt;
total lanes in the connected group.&lt;/p&gt;

&lt;p&gt;A React hook turns that result into &lt;code&gt;top&lt;/code&gt;, &lt;code&gt;fontSize&lt;/code&gt;, &lt;code&gt;scale&lt;/code&gt;, and &lt;code&gt;zIndex&lt;/code&gt;.&lt;br&gt;
It also checks whether anything actually changed before updating state. That&lt;br&gt;
guard matters because a render-measure-resolve loop can easily trigger itself&lt;br&gt;
forever.&lt;/p&gt;
&lt;h2&gt;
  
  
  A feedback loop I did not expect
&lt;/h2&gt;

&lt;p&gt;One of the stranger bugs came from measuring a time-zone name after shrinking&lt;br&gt;
it.&lt;/p&gt;

&lt;p&gt;The loop was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Measure a wide header.&lt;/li&gt;
&lt;li&gt;Detect a collision and reduce its font.&lt;/li&gt;
&lt;li&gt;Measure the now-narrow header.&lt;/li&gt;
&lt;li&gt;Decide the collision is gone and restore the font.&lt;/li&gt;
&lt;li&gt;Measure the wide header again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The layout could oscillate without any user interaction.&lt;/p&gt;

&lt;p&gt;The fix was to separate measured geometry from visual scale. The resolver&lt;br&gt;
always uses the header's natural width. When the header needs to shrink, the&lt;br&gt;
renderer uses a CSS transform. Its appearance changes, but the measurement&lt;br&gt;
that produced the collision decision stays stable.&lt;/p&gt;
&lt;h2&gt;
  
  
  Dragging outside the timeline
&lt;/h2&gt;

&lt;p&gt;My first drag implementation depended too much on events from the timeline&lt;br&gt;
element. It worked until the pointer left the list or the release happened&lt;br&gt;
outside it.&lt;/p&gt;

&lt;p&gt;The current version starts with &lt;code&gt;pointerdown&lt;/code&gt; on an invisible 16-pixel grab&lt;br&gt;
strip, then installs &lt;code&gt;pointermove&lt;/code&gt; and &lt;code&gt;pointerup&lt;/code&gt; listeners on &lt;code&gt;window&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the drag continues outside the component;&lt;/li&gt;
&lt;li&gt;mouse, touch, and pen use the same event path;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pointercancel&lt;/code&gt; and window blur end the session safely;&lt;/li&gt;
&lt;li&gt;Escape restores the interval captured at the start of the drag.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pointer moves are coalesced with &lt;code&gt;requestAnimationFrame&lt;/code&gt;. The latest position&lt;br&gt;
wins, and no more than one geometry and collision pass runs per frame.&lt;/p&gt;

&lt;p&gt;The interaction code is in&lt;br&gt;
&lt;a href="https://github.com/it-pal-net/react-timespace/blob/main/hooks/useTimeIntervalDrag.js" rel="noopener noreferrer"&gt;&lt;code&gt;useTimeIntervalDrag.js&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  State management: two different speeds
&lt;/h2&gt;

&lt;p&gt;Timespace uses a plain React context and reducer rather than a separate state&lt;br&gt;
library.&lt;/p&gt;

&lt;p&gt;Timelines and intervals are normalized into ID lists and maps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;timeLinesIds       + timeLinesMap
timeIntervalsIds   + timeIntervalsMap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The maps make individual updates simple. Memoized arrays provide ordered&lt;br&gt;
collections for rendering. Public actions such as &lt;code&gt;setTimelines&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;updateTimeline&lt;/code&gt;, and &lt;code&gt;addTimeInterval&lt;/code&gt; keep host applications away from the&lt;br&gt;
reducer internals.&lt;/p&gt;

&lt;p&gt;The more important decision was splitting the provider into two contexts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data context:  timelines, intervals, settings, dispatch
clock context: current time, zoned clocks, day progress
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those values change at different speeds. Structural data changes occasionally.&lt;br&gt;
The clock changes continuously.&lt;/p&gt;

&lt;p&gt;The ticker schedules itself on the next exact interval boundary. After every&lt;br&gt;
tick it reads &lt;code&gt;Date.now()&lt;/code&gt; again, so a stalled main thread does not accumulate&lt;br&gt;
timer drift.&lt;/p&gt;

&lt;p&gt;Even a separate clock context would rerender its consumers every second. The&lt;br&gt;
current version uses two small synchronization components instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one writes the current x-position into a CSS custom property;&lt;/li&gt;
&lt;li&gt;another caches the clock DOM elements and updates their text.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;React still owns the structure and rebuilds the cache when rows change. Direct&lt;br&gt;
DOM writes are limited to isolated, high-frequency presentation data. The 24&lt;br&gt;
hour cells in every row do not need to rerender because &lt;code&gt;11:26&lt;/code&gt; became &lt;code&gt;11:27&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The provider is in&lt;br&gt;
&lt;a href="https://github.com/it-pal-net/react-timespace/blob/main/state/timeZonesProvider.jsx" rel="noopener noreferrer"&gt;&lt;code&gt;state/timeZonesProvider.jsx&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This boundary could still improve. During a drag, a local draft interval could&lt;br&gt;
be rendered without committing each frame to context, followed by one update&lt;br&gt;
on pointer-up.&lt;/p&gt;
&lt;h2&gt;
  
  
  The other hard problem: time zones
&lt;/h2&gt;

&lt;p&gt;A JavaScript &lt;code&gt;Date&lt;/code&gt; represents an instant. It does not carry an IANA time zone&lt;br&gt;
such as &lt;code&gt;America/New_York&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The provider uses cached &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt; instances and&lt;br&gt;
&lt;code&gt;formatToParts()&lt;/code&gt; to produce clock text and abbreviations. For numeric offsets,&lt;br&gt;
it requests &lt;code&gt;timeZoneName: "longOffset"&lt;/code&gt; and parses values such as&lt;br&gt;
&lt;code&gt;GMT+07:00&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The offset is evaluated for an actual date. Berlin is not treated as&lt;br&gt;
permanently UTC+1, and half-hour or quarter-hour zones are not rounded to a&lt;br&gt;
whole hour.&lt;/p&gt;

&lt;p&gt;Availability highlighting also works from actual instants. To put a local&lt;br&gt;
&lt;code&gt;08:00–17:00&lt;/code&gt; window onto the displayed home day, it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Finds the instant corresponding to the start of the home-zone day.&lt;/li&gt;
&lt;li&gt;Samples each minute across the visual 24-hour range.&lt;/li&gt;
&lt;li&gt;Formats that instant in each row's IANA zone.&lt;/li&gt;
&lt;li&gt;Checks whether the local minute is inside that row's availability.&lt;/li&gt;
&lt;li&gt;Intersects the configured rows to find the time available to everyone.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is an old workaround I would not use in a new project. The home-clock&lt;br&gt;
path formats a date in the chosen zone and parses that formatted string back&lt;br&gt;
into &lt;code&gt;Date&lt;/code&gt;. Locale-dependent parsing is brittle, and it mixes the concepts of&lt;br&gt;
an instant and a wall-clock representation.&lt;/p&gt;

&lt;p&gt;JavaScript now has a much better model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;instant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;instant&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bangkok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;instant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toZonedDateTimeISO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Asia/Bangkok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;bangkok&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;bangkok&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// "+07:00"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;homeStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Now&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zonedDateTimeISO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;America/New_York&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startOfDay&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Temporal&lt;/code&gt; is now Stage 4 and available in current Firefox, Chrome, and Node,&lt;br&gt;
but it is not yet universal across major browsers. A library still needs to&lt;br&gt;
choose between requiring newer engines, including a polyfill, or retaining an&lt;br&gt;
&lt;code&gt;Intl&lt;/code&gt; fallback.&lt;/p&gt;

&lt;p&gt;Replacing the remaining formatted-string workaround is one of the next things&lt;br&gt;
I want to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would improve next
&lt;/h2&gt;

&lt;p&gt;The biggest missing piece is accessibility. The interval handles need keyboard&lt;br&gt;
control, focus management, and better ARIA descriptions.&lt;/p&gt;

&lt;p&gt;Other useful improvements would be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep drag state local and commit it on pointer-up;&lt;/li&gt;
&lt;li&gt;add property-based tests for the collision resolver;&lt;/li&gt;
&lt;li&gt;add more tests around DST boundaries;&lt;/li&gt;
&lt;li&gt;progressively adopt &lt;code&gt;Temporal&lt;/code&gt; as browser support improves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The current pure geometry tests are in&lt;br&gt;
&lt;a href="https://github.com/it-pal-net/react-timespace/tree/main/core/__tests__" rel="noopener noreferrer"&gt;&lt;code&gt;core/__tests__&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The full source is&lt;br&gt;
&lt;a href="https://github.com/it-pal-net/react-timespace" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;, and the&lt;br&gt;
&lt;a href="https://synccontact.com/timespace" rel="noopener noreferrer"&gt;interactive demo&lt;/a&gt; does not require an&lt;br&gt;
account.&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
