{"version":3,"file":"accessibility.mjs","names":[],"sources":["../src/accessibility.ts"],"sourcesContent":["import type { Clip, Track, TimelineReadonly } from '@techsquidtv/canvas-timeline-core';\nimport { subRational, toSeconds, type RationalTime } from '@techsquidtv/canvas-timeline-utils';\n\n/**\n * Options for formatting a single timeline time value for assistive technology.\n */\nexport interface TimelineTimeValueFormatOptions {\n  /** Decimal places to keep for sub-second values. Defaults to 2. */\n  precision?: number;\n}\n\n/**\n * Options for formatting a timeline start/end range for assistive technology.\n */\nexport interface TimelineRangeValueFormatOptions extends TimelineTimeValueFormatOptions {\n  /** Whether to include the computed range duration. Defaults to false. */\n  includeDuration?: boolean;\n}\n\nfunction toTimelineSeconds(value: RationalTime | number | undefined) {\n  if (value === undefined) {\n    return 0;\n  }\n  return typeof value === 'number' ? value : toSeconds(value);\n}\n\nfunction normalizeSeconds(value: number, precision: number) {\n  if (!Number.isFinite(value)) {\n    return 0;\n  }\n\n  const factor = 10 ** precision;\n  const rounded = Math.round(value * factor) / factor;\n  return Object.is(rounded, -0) ? 0 : rounded;\n}\n\nfunction formatSecondsNumber(value: number, precision: number) {\n  const normalized = normalizeSeconds(value, precision);\n  if (precision <= 0) {\n    return Math.round(normalized).toString();\n  }\n\n  return normalized.toFixed(precision).replace(/\\.?0+$/, '');\n}\n\nfunction formatUnit(value: number, singular: string, plural: string) {\n  return `${value} ${Math.abs(value) === 1 ? singular : plural}`;\n}\n\n/**\n * Formats a timeline time for assistive technology.\n *\n * The result intentionally avoids timecode punctuation and raw floating point\n * artifacts so controls can use it as `aria-valuetext`.\n *\n * @param value - Timeline time to format, as rational time or decimal seconds.\n * @param options - Optional decimal precision for the spoken time value.\n */\nexport function formatTimelineTimeValue(\n  value: RationalTime | number | undefined,\n  options: TimelineTimeValueFormatOptions = {}\n) {\n  const precision = options.precision ?? 2;\n  const secondsValue = normalizeSeconds(toTimelineSeconds(value), precision);\n  const sign = secondsValue < 0 ? '-' : '';\n  const absoluteSeconds = Math.abs(secondsValue);\n\n  if (absoluteSeconds < 60) {\n    const seconds = formatSecondsNumber(absoluteSeconds, precision);\n    return `${sign}${formatUnit(Number(seconds), 'second', 'seconds')}`;\n  }\n\n  const minutes = Math.floor(absoluteSeconds / 60);\n  const seconds = absoluteSeconds - minutes * 60;\n  const formattedSeconds = formatSecondsNumber(seconds, precision);\n\n  return `${sign}${formatUnit(minutes, 'minute', 'minutes')}, ${formatUnit(\n    Number(formattedSeconds),\n    'second',\n    'seconds'\n  )}`;\n}\n\n/**\n * Formats a start/end timeline range for assistive technology.\n *\n * @param start - Range start time, as rational time or decimal seconds.\n * @param end - Range end time, as rational time or decimal seconds.\n * @param options - Optional precision and duration display settings.\n */\nexport function formatTimelineRangeValue(\n  start: RationalTime | number | undefined,\n  end: RationalTime | number | undefined,\n  options: TimelineRangeValueFormatOptions = {}\n) {\n  const range = `${formatTimelineTimeValue(start, options)} to ${formatTimelineTimeValue(\n    end,\n    options\n  )}`;\n\n  if (!options.includeDuration) {\n    return range;\n  }\n\n  const duration = Math.max(0, toTimelineSeconds(end) - toTimelineSeconds(start));\n  return `${range}, duration ${formatTimelineTimeValue(duration, options)}`;\n}\n\nfunction getTrackLabel<TrackKind = string>(track?: TimelineReadonly<Track<TrackKind>>) {\n  return track?.name?.trim() || track?.id;\n}\n\n/**\n * Returns a concise clip name suitable for labels or active-item summaries.\n *\n * @param clip - Timeline clip to describe.\n * @param track - Optional track containing the clip.\n * @template TrackKind - App-defined track kind values carried by the containing\n * track.\n * @returns Short label suitable for `aria-label`, active-descendant text, or\n * inspector headings.\n */\nexport function getClipAccessibleName<TrackKind = string>(\n  clip: TimelineReadonly<Clip>,\n  track?: TimelineReadonly<Track<TrackKind>>\n) {\n  const clipLabel = clip.label?.trim() || clip.id;\n  const trackLabel = getTrackLabel(track);\n  return trackLabel ? `${clipLabel} on ${trackLabel}` : clipLabel;\n}\n\n/**\n * Returns a richer clip description without requiring one DOM node per clip.\n *\n * @param clip - Timeline clip whose timing and edit state should be described.\n * @param track - Optional track containing the clip.\n * @template TrackKind - App-defined track kind values carried by the containing\n * track.\n * @returns Spoken description with timing, duration, and relevant edit-state\n * flags.\n */\nexport function getClipAccessibleDescription<TrackKind = string>(\n  clip: TimelineReadonly<Clip>,\n  track?: TimelineReadonly<Track<TrackKind>>\n) {\n  const duration = subRational(clip.timelineEnd, clip.timelineStart);\n  const parts = [\n    `starts at ${formatTimelineTimeValue(clip.timelineStart)}`,\n    `ends at ${formatTimelineTimeValue(clip.timelineEnd)}`,\n    `duration ${formatTimelineTimeValue(duration)}`,\n  ];\n\n  if (track?.locked) {\n    parts.push('track locked');\n  }\n  if (track?.muted) {\n    parts.push('track muted');\n  }\n  if (clip.selected) {\n    parts.push('selected');\n  }\n  if (clip.disabled) {\n    parts.push('disabled');\n  }\n  if (clip.movable === false) {\n    parts.push('movement disabled');\n  }\n  if (clip.resizable === false) {\n    parts.push('trimming disabled');\n  }\n\n  return parts.join(', ');\n}\n"],"mappings":";;AAmBA,SAAS,kBAAkB,OAA0C;CACnE,IAAI,UAAU,KAAA,GACZ,OAAO;CAET,OAAO,OAAO,UAAU,WAAW,QAAQ,UAAU,KAAK;AAC5D;AAEA,SAAS,iBAAiB,OAAe,WAAmB;CAC1D,IAAI,CAAC,OAAO,SAAS,KAAK,GACxB,OAAO;CAGT,MAAM,SAAS,MAAM;CACrB,MAAM,UAAU,KAAK,MAAM,QAAQ,MAAM,IAAI;CAC7C,OAAO,OAAO,GAAG,SAAS,EAAE,IAAI,IAAI;AACtC;AAEA,SAAS,oBAAoB,OAAe,WAAmB;CAC7D,MAAM,aAAa,iBAAiB,OAAO,SAAS;CACpD,IAAI,aAAa,GACf,OAAO,KAAK,MAAM,UAAU,CAAC,CAAC,SAAS;CAGzC,OAAO,WAAW,QAAQ,SAAS,CAAC,CAAC,QAAQ,UAAU,EAAE;AAC3D;AAEA,SAAS,WAAW,OAAe,UAAkB,QAAgB;CACnE,OAAO,GAAG,MAAM,GAAG,KAAK,IAAI,KAAK,MAAM,IAAI,WAAW;AACxD;;;;;;;;;;AAWA,SAAgB,wBACd,OACA,UAA0C,CAAC,GAC3C;CACA,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,eAAe,iBAAiB,kBAAkB,KAAK,GAAG,SAAS;CACzE,MAAM,OAAO,eAAe,IAAI,MAAM;CACtC,MAAM,kBAAkB,KAAK,IAAI,YAAY;CAE7C,IAAI,kBAAkB,IAAI;EACxB,MAAM,UAAU,oBAAoB,iBAAiB,SAAS;EAC9D,OAAO,GAAG,OAAO,WAAW,OAAO,OAAO,GAAG,UAAU,SAAS;CAClE;CAEA,MAAM,UAAU,KAAK,MAAM,kBAAkB,EAAE;CAE/C,MAAM,mBAAmB,oBADT,kBAAkB,UAAU,IACU,SAAS;CAE/D,OAAO,GAAG,OAAO,WAAW,SAAS,UAAU,SAAS,EAAE,IAAI,WAC5D,OAAO,gBAAgB,GACvB,UACA,SACF;AACF;;;;;;;;AASA,SAAgB,yBACd,OACA,KACA,UAA2C,CAAC,GAC5C;CACA,MAAM,QAAQ,GAAG,wBAAwB,OAAO,OAAO,EAAE,MAAM,wBAC7D,KACA,OACF;CAEA,IAAI,CAAC,QAAQ,iBACX,OAAO;CAIT,OAAO,GAAG,MAAM,aAAa,wBADZ,KAAK,IAAI,GAAG,kBAAkB,GAAG,IAAI,kBAAkB,KAAK,CACjB,GAAG,OAAO;AACxE;AAEA,SAAS,cAAkC,OAA4C;CACrF,OAAO,OAAO,MAAM,KAAK,KAAK,OAAO;AACvC;;;;;;;;;;;AAYA,SAAgB,sBACd,MACA,OACA;CACA,MAAM,YAAY,KAAK,OAAO,KAAK,KAAK,KAAK;CAC7C,MAAM,aAAa,cAAc,KAAK;CACtC,OAAO,aAAa,GAAG,UAAU,MAAM,eAAe;AACxD;;;;;;;;;;;AAYA,SAAgB,6BACd,MACA,OACA;CACA,MAAM,WAAW,YAAY,KAAK,aAAa,KAAK,aAAa;CACjE,MAAM,QAAQ;EACZ,aAAa,wBAAwB,KAAK,aAAa;EACvD,WAAW,wBAAwB,KAAK,WAAW;EACnD,YAAY,wBAAwB,QAAQ;CAC9C;CAEA,IAAI,OAAO,QACT,MAAM,KAAK,cAAc;CAE3B,IAAI,OAAO,OACT,MAAM,KAAK,aAAa;CAE1B,IAAI,KAAK,UACP,MAAM,KAAK,UAAU;CAEvB,IAAI,KAAK,UACP,MAAM,KAAK,UAAU;CAEvB,IAAI,KAAK,YAAY,OACnB,MAAM,KAAK,mBAAmB;CAEhC,IAAI,KAAK,cAAc,OACrB,MAAM,KAAK,mBAAmB;CAGhC,OAAO,MAAM,KAAK,IAAI;AACxB"}