{
  "name": "CallTimer",
  "type": "pattern",
  "description": "Elapsed call duration as a pill, ticking once a second, with a duration formatter that grows an hour segment",
  "export": "@gtmi/strix-react/atoms/call-timer",
  "path": "@gtmi/strix-react/atoms/call-timer",
  "figma": {
    "file": "zT3vGm4QlFEI2Jp9nxyFKI",
    "fileName": "Demo components library",
    "nodes": [
      {
        "node": "22:4034",
        "name": ".timer",
        "section": "Atoms"
      },
      {
        "node": "22:4272",
        "name": ".timer-animation",
        "section": "Animations"
      }
    ],
    "note": "Dual membership like Badge: .timer is an Atom, .timer-animation an Animation, and the pair is one component in two states (static and ticking)."
  },
  "props": [
    {
      "name": "seconds",
      "type": "number",
      "default": "0",
      "description": "Elapsed seconds. The starting value when isRunning, or the whole readout when controlled"
    },
    {
      "name": "isRunning",
      "type": "boolean",
      "default": "false",
      "description": "Advance once a second from seconds. False holds the displayed value where it reached"
    },
    {
      "name": "onTick",
      "type": "(seconds: number) => void",
      "description": "Fires on each tick with the new value, so a caller can lift the elapsed time"
    },
    {
      "name": "icon",
      "type": "React.ReactNode",
      "default": "<CallIcon size=\"sm\" />",
      "description": "Leading 16px glyph. The two source atoms disagree; the handset is the default"
    },
    {
      "name": "hideIcon",
      "type": "boolean",
      "default": "false",
      "description": "Drop the glyph for a bare readout"
    },
    {
      "name": "label",
      "type": "string",
      "default": "'Call duration'",
      "description": "Accessible name describing what is being timed. The digits themselves are hidden from AT"
    }
  ],
  "parts": [
    {
      "name": "call-timer",
      "description": "The pill, via data-slot, carrying role=\"timer\""
    },
    {
      "name": "call-timer-icon",
      "description": "The leading glyph wrapper, aria-hidden"
    },
    {
      "name": "call-timer-value",
      "description": "The formatted digits, aria-hidden and tabular-nums"
    }
  ],
  "ai_tags": ["timer", "duration", "elapsed", "call", "voice", "clock", "stopwatch", "mm:ss", "formatter", "pattern"],
  "since": "0.4.0",
  "source": "Figma zT3vGm4QlFEI2Jp9nxyFKI atoms .timer (22:4034) and .timer-animation (22:4272), read in Dev Mode",
  "tokens_used": [
    "--color-text-secondary",
    "--text-size-figma-14",
    "--leading-height-figma-14",
    "--spacing-gap-150",
    "--spacing-gap-200",
    "--radius-2x-large"
  ],
  "notes": "WHY THIS EXISTS, when both atoms were mapped to Badge and .timer-animation was declined outright. The decline said the eight frames are 'successive clock values, i.e. the caller's state rather than a design'. Dev Mode shows the frames are 00:01..00:08 — ONE SECOND PER FRAME — so the frame interval IS the specification and the thing dismissed as eight arbitrary strings is a tick rate. The decline had been written from a REST extraction that reported the frames' widths (63-66px, i.e. the digits) and never their content in sequence. THE GLYPHS DIFFER BETWEEN THE TWO ATOMS and no previous note recorded it: .timer draws `Time/stop-watch`, .timer-animation draws `Product Logos/twilio_programmable-voice`, a handset. The handset is the default because the animation is the atom that establishes this as a CALL duration rather than a generic clock; the stopwatch is available via `icon`. Both the old binding and the old .timer story rendered ClockIcon, which is neither. NOT BADGE, and the old binding had already worked out why without acting on it — it wrote that 'the likelier reading is that .timer is not really a badge in the Figma author's head'. It filed two divergences as `deferred`: a 14/20 label against Badge's 12/18 ramp, and a 32px pill against the 22px Badge derives from that ramp. There is a third it did not name: Badge hard-codes `[&>svg]:size-3`, so its icon slot is 12px and cannot hold the 16px glyph both atoms draw. Three overrides on a badge is a different component. NO NEW TOKENS WERE MINTED, which is the evidence the geometry is native rather than forced: --text-size-figma-14/--leading-height-figma-14 (14/20) already existed for SelectableTile, and the 6px vertical padding Figma specifies is --spacing-gap-150. 20px leading + 6px twice = the 32px height, so the height is derived rather than fixed — the objection .badge raised against its own 24px does not apply here. Radius is --radius-2x-large, Figma's own 50px name. THE FORMATTER IS A DECISION THE DESIGN CANNOT MAKE. Figma never rolls past 00:08, so it never reveals the mm:ss -> hh:mm:ss boundary, the padding rule past an hour, or whether hours appear conditionally. formatDuration is zero-padded mm:ss below an hour and an unpadded h:mm:ss past one (3753 -> 1:02:33, not 62:33), which is platform convention for a call timer; it is exported and tested at every boundary, including the 3599/3600 crossing the design never shows. Input is clamped and floored rather than rejected, because a caller computing `now - startedAt` can legitimately produce -0.4 on the first frame. THE ACCESSIBILITY DECISION IS THE SUBSTANTIVE ONE and it is the reason a naive port of this atom would be harmful. A value that changes every second must not be announced every second: an aria-live=\"polite\" region ticking once a second is a screen-reader denial of service, which .timer's binding correctly identified and deferred. The resolution is role=\"timer\" with an accessible name, aria-live=\"off\" stated rather than defaulted, and aria-hidden on both the digits and the glyph — so a screen reader reads the duration ON DEMAND and is never interrupted. A test asserts the ABSENCE of any polite/assertive region and of role=status, because that is the defect that would creep back. Same reasoning that makes AudioVisualizer decorative by default: the state lives in the application, not the graphic. REDUCED MOTION IS DELIBERATELY NOT HONOURED, and this is the one place in the Animations section where that is correct: a clock is information, not decoration, and freezing it would withhold the thing the component reports. That is also why the tick is a setInterval over a value rather than a CSS animation on an --animate-* token, unlike WaitingIndicator and Badge's pulse. The interval is cleared on unmount and re-armed only when isRunning changes; onTick is held in a ref so a fresh closure per render cannot double the rate, and a test proves the rate is unchanged across a rerender. tabular-nums so the pill does not jitter as the digits change. Contrast derived from the component's own cva output and mutation-tested: swapping the ink for text-highlight fails at 1.84:1 on the light page. Note the ink is --color-text-secondary, not the atom's literal gray-300 — that hex measures 7.43:1 on Figma's dark page but 2.61:1 on a white one, which the .timer binding had already flagged as the port hazard."
}
