# TimeLens

> Max's agenda: hour marks, ticks and tasks in one scrolling column, read through a fixed lens that shows the time under it.

## Facts

- **Product**: Max
- **Family**: Data display
- **Global**: `window.DigitalCrew.TimeLens`
- **Source**: `max-agent: src/components/ui/time-lens.tsx`
- **Import in the app**: `import { TimeLens } from "@/components/ui/time-lens";`
- **Live preview**: /design/components/time-lens/preview.html
- **Page**: /design/components/time-lens

## Guidelines

**Use it for** the day plan in the Actions cockpit — tasks and meetings in time order, with proposals (overdue, unplanned) that can be allocated at the time under the lens.

**What you provide**: `rows` sorted by time — `divider` (hour labels), `tick`, `moment` (a task or meeting with `status`: done, current, overdue, suggested, scheduled; `people`; `minutes`; `executor` human or AI) and `wheel` (a deck of unplaced work) — plus `currentId`, `pick` (count, minutes, cost at the crew's rates), `timeZones`, `dayScale`, and `onSelect`/`onAllocate`/`onSkip`. `now` pins the clock for screenshots.

**Behaviour**: rows swell as they pass the lens and quiet as they leave (a raised-cosine wave driven by scroll). At rest the lens follows the live clock; scrolling unlocks it and a Now button returns. The current moment takes `primary`; suggested ones are violet. Give it a fixed height (`className="h-full"` in a sized flex column).

## API

```ts
export type TimeLensStatus = "done" | "current" | "overdue" | "suggested" | "scheduled";
/** Who would carry a task out, and on what terms. The transparency line. */
export interface TimeLensExecutor {
    kind: "human" | "ai";
    name: string;
    hourlyRate: number;
    automatable: boolean;
    runCount: number;
}
/** One person a moment is FOR or WITH — a prospect, or a meeting attendee. */
export interface TimeLensPerson {
    name: string;
    detail: string | null;
}
export interface TimeLensMoment {
    id: string;
    taskId: string | null;
    calendarItemId: string | null;
    kind: "task" | "meeting";
    title: string;
    description: string | null;
    at: string;
    endsAt: string | null;
    timeLabel: string;
    statusLabel: string;
    status: TimeLensStatus;
    proposal: boolean;
    people: TimeLensPerson[];
    minutes: number | null;
    cost: number | null;
    currency: string | null;
    executor: TimeLensExecutor | null;
}
export type TimeLensRow = {
    kind: "divider";
    key: string;
    at: string;
    label: string;
    isNow: boolean;
} | {
    kind: "tick";
    key: string;
    at: string;
} | {
    kind: "moment";
    key: string;
    at: string;
    moment: TimeLensMoment;
}
/** The Task Wheel: unplaced work dealt at now as one swipeable deck. */
 | {
    kind: "wheel";
    key: string;
    at: string;
    moments: TimeLensMoment[];
};
/** What is on offer: count, time to execute, money at the crew's rates. */
export interface TimeLensPick {
    count: number;
    minutes: number;
    cost: number;
    currency: string;
}
export interface TimeLensProps {
    /** Sorted ascending by instant. */
    rows: TimeLensRow[];
    /** The header's headline: what is waiting to be won, at the crew's rates. */
    pick?: TimeLensPick | null;
    /** The moment under way — gets the primary accent. */
    currentId?: string | null;
    /** Extra clocks beside the reader's own, as IANA zone names. */
    timeZones?: string[];
    /**
     * True when rows carry clock times (a single day); false when they carry
     * dates. Drives the lens readout and how an allocation time is rounded.
     */
    dayScale?: boolean;
    /** Pin the clock (tests, screenshots). Omit for the live second hand. */
    now?: Date;
    onSelect?: (moment: TimeLensMoment) => void;
    /** A proposal was accepted — booked at `atIso`, the time under the lens. */
    onAllocate?: (moment: TimeLensMoment, atIso: string) => void;
    onSkip?: (moment: TimeLensMoment) => void;
    label?: string;
    className?: string;
}
/**
 * The time↔offset mapping of a stream: piecewise-linear between the rows'
 * instants and their vertical centers. This is what lets the lens READ the
 * instant under it, and the stream place the NOW line and center on now.
 * Exported for tests.
 */
export declare function buildTimeScale(rows: readonly TimeLensRow[], centers: readonly number[]): {
    msForY: (y: number) => number | null;
    yForMs: (value: number) => number | null;
    empty: boolean;
};
/** The instant an Allocate press books, from the instant under the lens. */
export declare function allocationInstant(lensMs: number, nowMs: number, dayScale: boolean): string;
export declare function TimeLens({ rows, pick, currentId, timeZones, dayScale, now, onSelect, onAllocate, onSkip, label, className, }: TimeLensProps): any;
export default TimeLens;
```

## Example

```html
<div id="root" class="p-6"></div>
<script>
(function () {
  var h = React.createElement, DC = window.DigitalCrew, I = DC.Icons;
var DAY = "2026-10-02T";
function at(hm) { return new Date(DAY + hm + ":00").toISOString(); }
function M(id, hm, title, status, extra) {
  return Object.assign({ id: id, taskId: id, calendarItemId: null, kind: "task", title: title, description: null, at: at(hm), endsAt: null,
    timeLabel: hm, statusLabel: { done: "Done", current: "In progress", overdue: "Overdue", suggested: "Suggested", scheduled: "Scheduled" }[status],
    status: status, proposal: false, people: [], minutes: 15, cost: null, currency: "EUR", executor: null }, extra || {});
}
var ROWS = [];
for (var hr = 8; hr <= 14; hr++) {
  var hh = (hr < 10 ? "0" : "") + hr;
  ROWS.push({ kind: "divider", key: "d" + hr, at: at(hh + ":00"), label: hh + ":00", isNow: false });
  ROWS.push({ kind: "tick", key: "t" + hr, at: at(hh + ":30") });
}
ROWS.push({ kind: "moment", key: "m1", at: at("09:10"), moment: M("m1", "09:10", "Reply to Claire Dubois", "done", { people: [{ name: "Claire Dubois", detail: "CFO · Northwind" }] }) });
ROWS.push({ kind: "moment", key: "m2", at: at("10:40"), moment: M("m2", "10:40", "Discovery call — Northwind", "current", { kind: "meeting", minutes: 30, people: [{ name: "Marcus Webb", detail: "VP Sales" }] }) });
ROWS.push({ kind: "moment", key: "m3", at: at("11:50"), moment: M("m3", "11:50", "Send the pricing one-pager", "scheduled", { executor: { kind: "ai", name: "Max", hourlyRate: 0, automatable: true, runCount: 14 } }) });
ROWS.push({ kind: "moment", key: "m4", at: at("13:20"), moment: M("m4", "13:20", "Qualify 12 new inbound leads", "suggested", { minutes: 40 }) });
ROWS.sort(function (a, b) { return Date.parse(a.at) - Date.parse(b.at); });
function App() {
  return h("div", { className: "flex h-[430px] flex-col rounded-lg border overflow-hidden" },
    h(DC.TimeLens, { rows: ROWS, currentId: "m2", now: new Date(DAY + "10:52:00"), label: "Today", className: "h-full" }));
}
  ReactDOM.createRoot(document.getElementById("root")).render(h(DC.TooltipProvider, { delayDuration: 80 }, h(App)));
})();
</script>
```

## More in Data display

- [AgentFace](/design/components/agent-face.md): A Digital Worker's face: its approved portrait, or its initials on its accent tile.
- [AnimateDigits](/design/components/animate-digits.md): A number whose digits spring in and out as it changes (motion), direction following the change.
- [Avatar](/design/components/avatar.md): A round image with an initials fallback (Radix Avatar), 32px by default.
- [CampaignStatusBadge](/design/components/campaign-status-badge.md): The campaign lifecycle pill: Draft, Active, Paused, Stopped, Completed, Archived.
- [ChannelBadge](/design/components/channel-badge.md): Marks which channel a conversation or step runs on: email (amber), LinkedIn (sky), WhatsApp (emerald).
- [EmailVerificationBadge](/design/components/email-verification-badge.md): The deliverability verdict for an email address, with an icon and an optional score.
- [HeroMetric](/design/components/hero-metric.md): One big number with its previous-period comparison, a trend pill and a pacing bar.
- [KpiCard](/design/components/kpi-card.md): A KPI tile — label, value, trend — and `KpiSection`, the glass panel that groups them.
- [MaxIconSet](/design/components/max-icon-set.md): Max's own SVG icons, exported as `DC.MaxIcons`: integration and channel logos, message delivery states, and the appearance-picker tiles.
- [MetricTile](/design/components/metric-tile.md): A compact glass tile for a scored metric: label, value toned by score band, hint.
- [ProspectAvatar](/design/components/prospect-avatar.md): A prospect's photo, or a gradient disc hashed from their name, with an optional unread halo.
- [Snippet](/design/components/snippet.md): A monospace command block with a copy button whose icon turns into a check after copying.
- [WorldGlobe](/design/components/world-globe.md): An interactive d3-geo globe on canvas with avatar markers that merge into heat-coloured count bubbles.
