{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chart-utils",
  "type": "registry:lib",
  "title": "Chart Utilities",
  "description": "Shared Intl formatters, time-series decimation, and RAF tooltip scheduling for Bklit charts",
  "files": [
    {
      "path": "src/charts/chart-formatters.ts",
      "content": "export const shortDateFmt = new Intl.DateTimeFormat(\"en-US\", {\n  month: \"short\",\n  day: \"numeric\",\n});\n\nexport const weekdayDateFmt = new Intl.DateTimeFormat(\"en-US\", {\n  weekday: \"short\",\n  month: \"short\",\n  day: \"numeric\",\n});\n\nexport const hmsTimeFmt = new Intl.DateTimeFormat(\"en-US\", {\n  hour: \"2-digit\",\n  minute: \"2-digit\",\n  second: \"2-digit\",\n  hour12: false,\n});\n\n// `Intl.NumberFormat.prototype.format` is a bound getter — safe to extract.\nexport const intFmt = new Intl.NumberFormat(\"en-US\").format;\n",
      "type": "registry:lib",
      "target": "components/charts/chart-formatters.ts"
    },
    {
      "path": "src/charts/decimate-time-series.ts",
      "content": "/**\n * Largest-Triangle-Three-Buckets downsampling for time-series SVG paths.\n * Keeps first/last points and picks visually significant points per bucket.\n */\nexport function decimateTimeSeries<T extends Record<string, unknown>>(\n  data: T[],\n  maxPoints: number,\n  valueKeys: string[] = []\n): T[] {\n  const len = data.length;\n  if (maxPoints >= len || maxPoints < 3) {\n    return data;\n  }\n\n  const getY = (point: T, index: number): number => {\n    if (valueKeys.length === 0) {\n      for (const val of Object.values(point)) {\n        if (typeof val === \"number\") {\n          return val;\n        }\n      }\n      return index;\n    }\n\n    let sum = 0;\n    let count = 0;\n    for (const key of valueKeys) {\n      const val = point[key];\n      if (typeof val === \"number\") {\n        sum += val;\n        count++;\n      }\n    }\n    return count > 0 ? sum / count : index;\n  };\n\n  const sampled: T[] = [data[0] as T];\n  const bucketSize = (len - 2) / (maxPoints - 2);\n  let previousIndex = 0;\n\n  for (let i = 0; i < maxPoints - 2; i++) {\n    const rangeStart = Math.floor((i + 1) * bucketSize) + 1;\n    const rangeEnd = Math.min(Math.floor((i + 2) * bucketSize) + 1, len - 1);\n\n    const nextRangeStart = Math.floor((i + 2) * bucketSize) + 1;\n    const nextRangeEnd = Math.min(Math.floor((i + 3) * bucketSize) + 1, len);\n    const nextCount = Math.max(0, nextRangeEnd - nextRangeStart);\n\n    let avgX = len - 1;\n    let avgY = getY(data[len - 1] as T, len - 1);\n    if (nextCount > 0) {\n      avgX = 0;\n      avgY = 0;\n      for (let j = nextRangeStart; j < nextRangeEnd; j++) {\n        avgX += j;\n        avgY += getY(data[j] as T, j);\n      }\n      avgX /= nextCount;\n      avgY /= nextCount;\n    }\n\n    const pointA = data[previousIndex] as T;\n    const ax = previousIndex;\n    const ay = getY(pointA, previousIndex);\n\n    let maxArea = -1;\n    let maxIndex = rangeStart;\n\n    for (let j = rangeStart; j < rangeEnd; j++) {\n      const area =\n        Math.abs(\n          (ax - avgX) * (getY(data[j] as T, j) - ay) - (ax - j) * (avgY - ay)\n        ) * 0.5;\n      if (area > maxArea) {\n        maxArea = area;\n        maxIndex = j;\n      }\n    }\n\n    sampled.push(data[maxIndex] as T);\n    previousIndex = maxIndex;\n  }\n\n  sampled.push(data[len - 1] as T);\n  return sampled;\n}\n\n/** ~1.5 points per pixel — enough for crisp curves without over-drawing. */\nexport function maxRenderPointsForWidth(innerWidth: number): number {\n  return Math.max(64, Math.ceil(innerWidth * 1.5));\n}\n\n/** Bucket OHLC rows into fewer candles while preserving high/low extremes. */\nexport function decimateOhlcData<T extends Record<string, unknown>>(\n  data: T[],\n  maxPoints: number\n): T[] {\n  const len = data.length;\n  if (maxPoints >= len || maxPoints < 2) {\n    return data;\n  }\n\n  const bucketSize = len / maxPoints;\n  const sampled: T[] = [];\n\n  for (let i = 0; i < maxPoints; i++) {\n    const start = Math.floor(i * bucketSize);\n    const end = Math.min(len, Math.floor((i + 1) * bucketSize));\n    if (start >= end) {\n      continue;\n    }\n\n    const bucket = data.slice(start, end);\n    const first = bucket[0] as T;\n    const last = bucket.at(-1) as T;\n\n    let high = Number.NEGATIVE_INFINITY;\n    let low = Number.POSITIVE_INFINITY;\n    for (const row of bucket) {\n      const rowHigh = row.high;\n      const rowLow = row.low;\n      if (typeof rowHigh === \"number\" && rowHigh > high) {\n        high = rowHigh;\n      }\n      if (typeof rowLow === \"number\" && rowLow < low) {\n        low = rowLow;\n      }\n    }\n\n    sampled.push({\n      ...last,\n      open: first.open,\n      high: Number.isFinite(high) ? high : last.high,\n      low: Number.isFinite(low) ? low : last.low,\n      close: last.close,\n    } as T);\n  }\n\n  return sampled;\n}\n",
      "type": "registry:lib",
      "target": "components/charts/decimate-time-series.ts"
    },
    {
      "path": "src/charts/use-scheduled-tooltip.ts",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nexport interface ScheduledTooltipControls<T> {\n  tooltipData: T | null;\n  setTooltipData: React.Dispatch<React.SetStateAction<T | null>>;\n  scheduleTooltip: (tooltip: T, dedupeKey?: string) => void;\n  clearTooltip: () => void;\n  resetTooltipDedupe: () => void;\n}\n\nfunction defaultDedupeKey<T>(tooltip: T): string {\n  if (\n    typeof tooltip === \"object\" &&\n    tooltip !== null &&\n    \"index\" in tooltip &&\n    typeof (tooltip as { index: unknown }).index === \"number\"\n  ) {\n    const { index, x } = tooltip as { index: number; x?: number };\n    if (typeof x === \"number\") {\n      return `${index}:${Math.round(x)}`;\n    }\n    return String(index);\n  }\n  return JSON.stringify(tooltip);\n}\n\nexport function useScheduledTooltip<T>(): ScheduledTooltipControls<T> {\n  const [tooltipData, setTooltipData] = useState<T | null>(null);\n  const lastKeyRef = useRef<string | null>(null);\n  const pendingRef = useRef<T | null>(null);\n  const rafRef = useRef<number | null>(null);\n  const pendingKeyRef = useRef<string | null>(null);\n\n  useEffect(() => {\n    return () => {\n      if (rafRef.current !== null) {\n        cancelAnimationFrame(rafRef.current);\n      }\n    };\n  }, []);\n\n  const commitTooltip = useCallback((tooltip: T, dedupeKey: string) => {\n    if (dedupeKey === lastKeyRef.current) {\n      return;\n    }\n    lastKeyRef.current = dedupeKey;\n    setTooltipData(tooltip);\n  }, []);\n\n  const scheduleTooltip = useCallback(\n    (tooltip: T, dedupeKey?: string) => {\n      const key = dedupeKey ?? defaultDedupeKey(tooltip);\n      pendingRef.current = tooltip;\n      pendingKeyRef.current = key;\n      if (key === lastKeyRef.current) {\n        return;\n      }\n      if (rafRef.current !== null) {\n        return;\n      }\n      rafRef.current = requestAnimationFrame(() => {\n        rafRef.current = null;\n        const next = pendingRef.current;\n        const nextKey = pendingKeyRef.current;\n        if (next && nextKey) {\n          commitTooltip(next, nextKey);\n        }\n      });\n    },\n    [commitTooltip]\n  );\n\n  const clearTooltip = useCallback(() => {\n    if (rafRef.current !== null) {\n      cancelAnimationFrame(rafRef.current);\n      rafRef.current = null;\n    }\n    pendingRef.current = null;\n    pendingKeyRef.current = null;\n    lastKeyRef.current = null;\n    setTooltipData(null);\n  }, []);\n\n  const resetTooltipDedupe = useCallback(() => {\n    lastKeyRef.current = null;\n  }, []);\n\n  return {\n    tooltipData,\n    setTooltipData,\n    scheduleTooltip,\n    clearTooltip,\n    resetTooltipDedupe,\n  };\n}\n",
      "type": "registry:lib",
      "target": "components/charts/use-scheduled-tooltip.ts"
    }
  ]
}