{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "deadline",
  "type": "registry:component",
  "title": "Deadline",
  "description": "Due-date marker line with a fading solid, gradient, or striped region to its right on date x axes",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "@bklit/chart-context",
    "@bklit/background"
  ],
  "files": [
    {
      "path": "src/charts/deadline.tsx",
      "content": "\"use client\";\n\nimport { m } from \"motion/react\";\nimport { type ReactNode, useId, useMemo } from \"react\";\nimport { useChartStable } from \"./chart-context\";\nimport { firstIndexOnOrAfter, toValidDate } from \"./date-ranges\";\nimport { type PatternPresetId, renderPatternPreset } from \"./pattern-preset\";\nimport { type CategorySlots, useCategorySlots } from \"./use-category-slots\";\nimport { isReferenceAreaVisiblePhase } from \"./y-domain-utils\";\n\nexport type DeadlineFillType = \"pattern\" | \"solid\" | \"gradient\" | \"none\";\n\nexport type DeadlineDate = Date | number | string;\n\nexport interface DeadlineProps {\n  /** Date of the marker line (left edge of the region). */\n  date: DeadlineDate;\n  /** Where the region ends. Default: the right edge of the plot. */\n  endDate?: DeadlineDate;\n  /** Line and fill colour. Default: `var(--chart-negative)` (red) */\n  color?: string;\n  /** Default: `\"pattern\"` (diagonal stripes). `\"none\"` draws only the line. */\n  fillType?: DeadlineFillType;\n  /** Default: 0.6 for `pattern`, 0.12 for `solid`, 0.25 for `gradient`. */\n  fillOpacity?: number;\n  /** `gradient` only — opacity at the region's right edge. Default: 0 */\n  gradientToOpacity?: number;\n  /** Pattern preset when `fillType` is `\"pattern\"`. Default: `\"diagonal-up\"` */\n  pattern?: PatternPresetId;\n  /** Default: 1.5 */\n  patternScale?: number;\n  /** Default: 1 */\n  patternStrokeWidth?: number;\n  /** Fade the region out towards its right edge. Default: true */\n  fade?: boolean;\n  /** Fraction (0–1) of the region, measured from the right, that fades. Default: 0.5 */\n  fadeLength?: number;\n  /** Draw the vertical marker line. Default: true */\n  showLine?: boolean;\n  /** Line colour. Default: `color` */\n  stroke?: string;\n  /** Default: 1 */\n  strokeWidth?: number;\n  /** Default: undefined (solid). */\n  strokeDasharray?: string;\n  /** Default: 1 */\n  strokeOpacity?: number;\n  /** Text drawn beside the marker line. */\n  label?: string;\n  /** Default: `\"top\"` */\n  labelPosition?: \"top\" | \"bottom\";\n  /** Default: `color` */\n  labelColor?: string;\n  /**\n   * Band (bar chart) scales only — data key holding each row's date. Default:\n   * first field holding a `Date` or `\"YYYY-MM-DD\"` string.\n   */\n  dateKey?: string;\n  className?: string;\n}\n\nconst DEFAULT_COLOR = \"var(--chart-negative, var(--color-red-500))\";\nconst ENTER_FADE_MS = 420;\nconst LABEL_INSET = 6;\nconst LABEL_FLIP_WIDTH = 96;\n\nfunction defaultFillOpacity(fillType: DeadlineFillType): number {\n  if (fillType === \"pattern\") {\n    return 0.6;\n  }\n  if (fillType === \"gradient\") {\n    return 0.25;\n  }\n  return 0.12;\n}\n\ninterface DeadlineRegion {\n  /** Unclipped pixel bounds — fade gradients stay pinned to data while panning. */\n  x0: number;\n  x1: number;\n}\n\nfunction resolveDeadlineRegion(\n  startTime: number | undefined,\n  endTime: number | undefined,\n  {\n    innerWidth,\n    slots,\n    isBandScale,\n    xScale,\n  }: {\n    innerWidth: number;\n    slots: CategorySlots | null;\n    isBandScale: boolean;\n    xScale: (value: Date) => number;\n  }\n): DeadlineRegion | null {\n  if (startTime == null) {\n    return null;\n  }\n  if (slots) {\n    if (slots.axis !== \"x\") {\n      return null;\n    }\n    const startIndex = firstIndexOnOrAfter(slots.dates, new Date(startTime));\n    if (startIndex < 0) {\n      return null;\n    }\n    const endIndex =\n      endTime == null\n        ? -1\n        : firstIndexOnOrAfter(slots.dates, new Date(endTime));\n    return {\n      x0: slots.slotStart(startIndex),\n      x1: endIndex < 0 ? innerWidth : slots.slotStart(endIndex),\n    };\n  }\n  if (isBandScale) {\n    return null;\n  }\n  return {\n    x0: xScale(new Date(startTime)),\n    x1: endTime == null ? innerWidth : xScale(new Date(endTime)),\n  };\n}\n\ninterface DeadlineRegionFillProps {\n  region: DeadlineRegion;\n  clipX0: number;\n  clipX1: number;\n  innerHeight: number;\n  uid: string;\n  color: string;\n  fillType: DeadlineFillType;\n  opacity: number;\n  gradientToOpacity: number;\n  patternNode: ReactNode;\n  patternId: string;\n  fade: boolean;\n  fadeLength: number;\n}\n\nfunction DeadlineRegionFill({\n  region,\n  clipX0,\n  clipX1,\n  innerHeight,\n  uid,\n  color,\n  fillType,\n  opacity,\n  gradientToOpacity,\n  patternNode,\n  patternId,\n  fade,\n  fadeLength,\n}: DeadlineRegionFillProps) {\n  const fadeMaskId = `chart-deadline-fade-${uid}`;\n  const fillGradientId = `chart-deadline-fill-${uid}`;\n  const fadeStart = 1 - Math.min(1, Math.max(0, fadeLength));\n  const useFade = fade && fadeLength > 0;\n  let regionFill = color;\n  if (fillType === \"gradient\") {\n    regionFill = `url(#${fillGradientId})`;\n  } else if (patternNode) {\n    regionFill = `url(#${patternId})`;\n  }\n  const gradientAttrs = {\n    gradientUnits: \"userSpaceOnUse\" as const,\n    x1: region.x0,\n    x2: region.x1,\n    y1: 0,\n    y2: 0,\n  };\n\n  return (\n    <>\n      <defs>\n        {useFade ? (\n          <>\n            <linearGradient id={`${fadeMaskId}-gradient`} {...gradientAttrs}>\n              <stop offset={0} stopColor=\"white\" stopOpacity={1} />\n              <stop offset={fadeStart} stopColor=\"white\" stopOpacity={1} />\n              <stop offset={1} stopColor=\"white\" stopOpacity={0} />\n            </linearGradient>\n            <mask id={fadeMaskId}>\n              <rect\n                fill={`url(#${fadeMaskId}-gradient)`}\n                height={innerHeight}\n                width={clipX1 - clipX0}\n                x={clipX0}\n                y={0}\n              />\n            </mask>\n          </>\n        ) : null}\n        {fillType === \"gradient\" ? (\n          <linearGradient id={fillGradientId} {...gradientAttrs}>\n            <stop offset={0} stopColor={color} stopOpacity={1} />\n            <stop\n              offset={1}\n              stopColor={color}\n              stopOpacity={opacity > 0 ? gradientToOpacity / opacity : 0}\n            />\n          </linearGradient>\n        ) : null}\n      </defs>\n      {patternNode}\n      <rect\n        fill={regionFill}\n        height={innerHeight}\n        mask={useFade ? `url(#${fadeMaskId})` : undefined}\n        opacity={opacity}\n        width={clipX1 - clipX0}\n        x={clipX0}\n        y={0}\n      />\n    </>\n  );\n}\n\nfunction DeadlineMarkerLine({\n  x,\n  innerHeight,\n  color,\n  stroke,\n  strokeWidth,\n  strokeDasharray,\n  strokeOpacity,\n  label,\n  labelColor,\n  labelPosition,\n  labelFlipped,\n}: {\n  x: number;\n  innerHeight: number;\n  color: string;\n  stroke?: string;\n  strokeWidth: number;\n  strokeDasharray?: string;\n  strokeOpacity: number;\n  label?: string;\n  labelColor?: string;\n  labelPosition: \"top\" | \"bottom\";\n  labelFlipped: boolean;\n}) {\n  const bottom = labelPosition === \"bottom\";\n  return (\n    <>\n      <line\n        stroke={stroke ?? color}\n        strokeDasharray={strokeDasharray}\n        strokeOpacity={strokeOpacity}\n        strokeWidth={strokeWidth}\n        x1={x}\n        x2={x}\n        y1={0}\n        y2={innerHeight}\n      />\n      {label ? (\n        <text\n          dominantBaseline={bottom ? \"auto\" : \"hanging\"}\n          fill={labelColor ?? color}\n          fontSize={11}\n          fontWeight={500}\n          textAnchor={labelFlipped ? \"end\" : \"start\"}\n          x={labelFlipped ? x - LABEL_INSET : x + LABEL_INSET}\n          y={bottom ? innerHeight - LABEL_INSET : LABEL_INSET}\n        >\n          {label}\n        </text>\n      ) : null}\n    </>\n  );\n}\n\n/**\n * Due-date / point-of-no-return marker: a vertical line at `date` with a\n * region extending right (to `endDate` or the plot edge) that fades out.\n * Works on time scales (line / area / composed) and vertical bar charts.\n */\nexport function Deadline({\n  date,\n  endDate,\n  color = DEFAULT_COLOR,\n  fillType = \"pattern\",\n  fillOpacity,\n  gradientToOpacity = 0,\n  pattern = \"diagonal-up\",\n  patternScale = 1.5,\n  patternStrokeWidth = 1,\n  fade = true,\n  fadeLength = 0.5,\n  showLine = true,\n  stroke,\n  strokeWidth = 1,\n  strokeDasharray,\n  strokeOpacity = 1,\n  label,\n  labelPosition = \"top\",\n  labelColor,\n  dateKey,\n  className,\n}: DeadlineProps) {\n  const {\n    innerWidth,\n    innerHeight,\n    xScale,\n    barScale,\n    chartPhase,\n    enterTransition,\n  } = useChartStable();\n  const slots = useCategorySlots(dateKey);\n  const uid = useId().replace(/:/g, \"\");\n  const patternId = `chart-deadline-pattern-${uid}`;\n\n  const startTime = toValidDate(date)?.getTime();\n  const endTime = endDate == null ? undefined : toValidDate(endDate)?.getTime();\n  const isBandScale = barScale != null;\n\n  const region = useMemo(\n    () =>\n      innerWidth > 0 && innerHeight > 0\n        ? resolveDeadlineRegion(startTime, endTime, {\n            innerWidth,\n            slots,\n            isBandScale,\n            xScale,\n          })\n        : null,\n    [startTime, endTime, innerWidth, innerHeight, slots, isBandScale, xScale]\n  );\n\n  const usesPattern = fillType === \"pattern\" && pattern !== \"none\";\n  const patternNode = useMemo(\n    () =>\n      usesPattern\n        ? renderPatternPreset(pattern, patternId, {\n            color,\n            scale: patternScale,\n            strokeWidth: patternStrokeWidth,\n          })\n        : null,\n    [usesPattern, pattern, patternId, color, patternScale, patternStrokeWidth]\n  );\n\n  if (!region || region.x1 <= region.x0) {\n    return null;\n  }\n\n  const { x0 } = region;\n  const clipX0 = Math.max(0, x0);\n  const clipX1 = Math.min(innerWidth, region.x1);\n  const lineVisible = showLine && x0 >= 0 && x0 <= innerWidth;\n  const hasFill = fillType !== \"none\" && clipX1 > clipX0;\n  if (!(lineVisible || hasFill)) {\n    return null;\n  }\n\n  const visible = isReferenceAreaVisiblePhase(chartPhase);\n  const fadeTransition =\n    enterTransition && typeof enterTransition === \"object\"\n      ? enterTransition\n      : { duration: ENTER_FADE_MS / 1000, ease: \"easeOut\" as const };\n\n  return (\n    <m.g\n      animate={{ opacity: visible ? 1 : 0 }}\n      aria-hidden=\"true\"\n      className={className ?? \"chart-deadline\"}\n      initial={{ opacity: 0 }}\n      transition={fadeTransition}\n    >\n      {hasFill ? (\n        <DeadlineRegionFill\n          clipX0={clipX0}\n          clipX1={clipX1}\n          color={color}\n          fade={fade}\n          fadeLength={fadeLength}\n          fillType={fillType}\n          gradientToOpacity={gradientToOpacity}\n          innerHeight={innerHeight}\n          opacity={fillOpacity ?? defaultFillOpacity(fillType)}\n          patternId={patternId}\n          patternNode={patternNode}\n          region={region}\n          uid={uid}\n        />\n      ) : null}\n      {lineVisible ? (\n        <DeadlineMarkerLine\n          color={color}\n          innerHeight={innerHeight}\n          label={label}\n          labelColor={labelColor}\n          labelFlipped={x0 > innerWidth - LABEL_FLIP_WIDTH}\n          labelPosition={labelPosition}\n          stroke={stroke}\n          strokeDasharray={strokeDasharray}\n          strokeOpacity={strokeOpacity}\n          strokeWidth={strokeWidth}\n          x={x0}\n        />\n      ) : null}\n    </m.g>\n  );\n}\n\nDeadline.displayName = \"Deadline\";\n\nexport default Deadline;\n",
      "type": "registry:component",
      "target": "components/charts/deadline.tsx"
    },
    {
      "path": "src/charts/date-ranges.ts",
      "content": "/** Day-of-week numbers as returned by `Date#getDay()` (0 = Sunday … 6 = Saturday). */\nexport type WeekdayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6;\n\n/** Saturday + Sunday. */\nexport const DEFAULT_WEEKEND_DAYS: readonly WeekdayIndex[] = [6, 0];\n\n/**\n * How a highlighted day maps onto the x axis.\n * - `\"calendar\"` — midnight to midnight (points at 00:00 sit on the band's left edge)\n * - `\"centered\"` — shifted half a day earlier so each day's point / column sits in the middle\n */\nexport type DateRangeAlign = \"calendar\" | \"centered\";\n\nexport interface DateRange {\n  start: Date;\n  end: Date;\n}\n\nexport interface ComputeWeekendRangesOptions {\n  weekendDays?: readonly number[];\n  /** Use UTC day boundaries instead of local time. Default: false */\n  utc?: boolean;\n  align?: DateRangeAlign;\n  /** Bail out (return `[]`) when the domain spans more days than this. Default: 20_000 */\n  maxDays?: number;\n}\n\nconst HALF_DAY_MS = 12 * 60 * 60 * 1000;\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\nfunction startOfDay(date: Date, utc: boolean): Date {\n  return utc\n    ? new Date(\n        Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())\n      )\n    : new Date(date.getFullYear(), date.getMonth(), date.getDate());\n}\n\n/** Next midnight — calendar arithmetic so DST days stay aligned. */\nfunction addDays(date: Date, days: number, utc: boolean): Date {\n  return utc\n    ? new Date(\n        Date.UTC(\n          date.getUTCFullYear(),\n          date.getUTCMonth(),\n          date.getUTCDate() + days\n        )\n      )\n    : new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);\n}\n\nexport function isWeekendDate(\n  date: Date,\n  weekendDays: readonly number[] = DEFAULT_WEEKEND_DAYS,\n  utc = false\n): boolean {\n  const day = utc ? date.getUTCDay() : date.getDay();\n  return weekendDays.includes(day);\n}\n\n/**\n * Contiguous weekend spans that intersect `[domainStart, domainEnd]`, clipped\n * to the domain. Consecutive weekend days merge into one range (Sat + Sun →\n * one span; Fri + Sat locales work the same way).\n */\nexport function computeWeekendRanges(\n  domainStart: Date,\n  domainEnd: Date,\n  options: ComputeWeekendRangesOptions = {}\n): DateRange[] {\n  const {\n    weekendDays = DEFAULT_WEEKEND_DAYS,\n    utc = false,\n    align = \"calendar\",\n    maxDays = 20_000,\n  } = options;\n\n  let lo = domainStart.getTime();\n  let hi = domainEnd.getTime();\n  if (!(Number.isFinite(lo) && Number.isFinite(hi))) {\n    return [];\n  }\n  if (lo > hi) {\n    [lo, hi] = [hi, lo];\n  }\n  if (weekendDays.length === 0 || (hi - lo) / DAY_MS > maxDays) {\n    return [];\n  }\n\n  const shift = align === \"centered\" ? HALF_DAY_MS : 0;\n  const ranges: DateRange[] = [];\n  let runStart: number | null = null;\n\n  // Start one day early so a centered span (or a span already in progress at\n  // the domain start) is still picked up.\n  let day = addDays(startOfDay(new Date(lo + shift), utc), -1, utc);\n  while (day.getTime() - shift <= hi) {\n    const next = addDays(day, 1, utc);\n    if (isWeekendDate(day, weekendDays, utc)) {\n      if (runStart === null) {\n        runStart = day.getTime() - shift;\n      }\n    } else if (runStart !== null) {\n      pushClipped(ranges, runStart, day.getTime() - shift, lo, hi);\n      runStart = null;\n    }\n    day = next;\n  }\n  if (runStart !== null) {\n    pushClipped(ranges, runStart, day.getTime() - shift, lo, hi);\n  }\n  return ranges;\n}\n\nfunction pushClipped(\n  ranges: DateRange[],\n  start: number,\n  end: number,\n  lo: number,\n  hi: number\n) {\n  const s = Math.max(start, lo);\n  const e = Math.min(end, hi);\n  if (e > s) {\n    ranges.push({ start: new Date(s), end: new Date(e) });\n  }\n}\n\n/**\n * Inclusive `[first, last]` index runs of consecutive weekend rows — for band\n * (category) scales where each row is one column. Rows with no date break a run.\n */\nexport function computeWeekendIndexRuns(\n  dates: ReadonlyArray<Date | null | undefined>,\n  options: Pick<ComputeWeekendRangesOptions, \"weekendDays\" | \"utc\"> = {}\n): [number, number][] {\n  const { weekendDays = DEFAULT_WEEKEND_DAYS, utc = false } = options;\n  const runs: [number, number][] = [];\n  let runStart = -1;\n  dates.forEach((date, index) => {\n    const weekend =\n      date != null &&\n      !Number.isNaN(date.getTime()) &&\n      isWeekendDate(date, weekendDays, utc);\n    if (weekend) {\n      if (runStart < 0) {\n        runStart = index;\n      }\n    } else if (runStart >= 0) {\n      runs.push([runStart, index - 1]);\n      runStart = -1;\n    }\n  });\n  if (runStart >= 0) {\n    runs.push([runStart, dates.length - 1]);\n  }\n  return runs;\n}\n\nconst ISO_DATE_ONLY = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\n\n/**\n * Coerce a user date-ish value to a valid `Date` (or `null`). Date-only ISO\n * strings (`\"2024-01-06\"`) parse as local midnight, not UTC, so the weekday\n * matches the calendar date.\n */\nexport function toValidDate(value: unknown): Date | null {\n  if (value instanceof Date) {\n    return Number.isNaN(value.getTime()) ? null : value;\n  }\n  if (typeof value === \"string\") {\n    const match = ISO_DATE_ONLY.exec(value.trim());\n    if (match) {\n      return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));\n    }\n  }\n  if (typeof value === \"number\" || typeof value === \"string\") {\n    const date = new Date(value);\n    return Number.isNaN(date.getTime()) ? null : date;\n  }\n  return null;\n}\n\n/**\n * Key of the first field in `row` holding a `Date` (or a date-only ISO string).\n * Used to find dates on band-scale (bar) charts whose context only exposes\n * category labels.\n */\nexport function detectDateKey(\n  row: Record<string, unknown> | undefined\n): string | null {\n  if (!row) {\n    return null;\n  }\n  for (const [key, value] of Object.entries(row)) {\n    if (value instanceof Date) {\n      return key;\n    }\n    if (typeof value === \"string\" && ISO_DATE_ONLY.test(value.trim())) {\n      return key;\n    }\n  }\n  return null;\n}\n\n/** Index of the first row whose date is on or after `target`; `-1` when none. */\nexport function firstIndexOnOrAfter(\n  dates: ReadonlyArray<Date | null | undefined>,\n  target: Date\n): number {\n  const t = target.getTime();\n  return dates.findIndex((date) => date != null && date.getTime() >= t);\n}\n",
      "type": "registry:component",
      "target": "components/charts/date-ranges.ts"
    },
    {
      "path": "src/charts/use-category-slots.ts",
      "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { useChartStable } from \"./chart-context\";\nimport { detectDateKey, toValidDate } from \"./date-ranges\";\n\nexport interface CategorySlots {\n  /** Per-row date (null when the row has no parseable date). */\n  dates: (Date | null)[];\n  /** Leading pixel edge of each row's slot, including half the inner padding. */\n  slotStart: (index: number) => number;\n  /** Full slot size (`bandwidth + inner padding`). */\n  step: number;\n  /** `\"x\"` for vertical bars (columns), `\"y\"` for horizontal bars. */\n  axis: \"x\" | \"y\";\n}\n\n/**\n * Date lookup for band (category) scales — bar charts expose only category\n * labels in context, so read dates from `dateKey` (auto-detected when omitted).\n * Returns `null` on time / linear scales.\n */\nexport function useCategorySlots(dateKey?: string): CategorySlots | null {\n  const { barScale, barXAccessor, data, orientation } = useChartStable();\n\n  return useMemo(() => {\n    if (!(barScale && barXAccessor)) {\n      return null;\n    }\n    const key = dateKey ?? detectDateKey(data[0]);\n    const dates = data.map((row) => (key ? toValidDate(row[key]) : null));\n    const step = barScale.step();\n    const gap = step - barScale.bandwidth();\n    return {\n      dates,\n      step,\n      axis: orientation === \"horizontal\" ? \"y\" : \"x\",\n      slotStart: (index: number) => {\n        const row = data[index];\n        const start = row ? barScale(barXAccessor(row)) : undefined;\n        return (start ?? 0) - gap / 2;\n      },\n    };\n  }, [barScale, barXAccessor, data, orientation, dateKey]);\n}\n",
      "type": "registry:component",
      "target": "components/charts/use-category-slots.ts"
    }
  ]
}