{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chart-progress",
  "title": "Chart Progress",
  "description": "Progress toward a real target: a wavy, grainy fill over a hatched remainder, with a thermometer marker, quota line and stacked segments.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://trytweenly.vercel.app/r/chart-kit.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/new-york/chart-progress/chart-progress.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useId, useMemo, useRef, useState } from \"react\"\nimport { animate as animateValue, motion, useMotionValue, useTransform, type MotionValue } from \"motion/react\"\nimport { cn } from \"@/lib/utils\"\nimport {\n  CHART_ACCENT,\n  ChartCard,\n  ChartLegend,\n  ChartPattern,\n  chartTheme,\n  createFormatter,\n  resolveSeries,\n  textureFill,\n  useChartReveal,\n  type ChartBaseProps,\n  type ResolvedSeries,\n} from \"@/registry/new-york/lib/chart-kit\"\n\nexport interface ChartProgressSegment {\n  label: string\n  value: number\n}\n\nexport interface ChartProgressProps extends ChartBaseProps {\n  /** Current amount, in the same unit as max. Ignored when segments are given. */\n  value: number\n  /** Amount that fills the bar. Default: 100 */\n  max?: number\n  /** Marker for a goal or quota, in the same unit as value. */\n  target?: number\n  /** Label of the target marker. Default: \"Target\" */\n  targetLabel?: string\n  /** Caption above the bar. Default: \"Progress\" */\n  label?: string\n  /** Show the share of max as a big percentage. Default: true */\n  showPercent?: boolean\n  /** Split the fill into stacked parts; their sum becomes the value. */\n  segments?: ChartProgressSegment[]\n  /** Bar thickness in px. Default: 40 */\n  thickness?: number\n  /** Bar length in px when vertical. Default: 220 */\n  length?: number\n  /** Look of the fill. Default: \"wave\" */\n  texture?: \"wave\" | \"solid\" | \"grain\"\n  /** Look of the unfilled part. Default: \"hatch\" */\n  remainder?: \"hatch\" | \"track\" | \"none\"\n  /** Show the thermometer marker at the end of the fill. Default: true */\n  showMarker?: boolean\n  /** Bar direction. Default: \"horizontal\" */\n  orientation?: \"horizontal\" | \"vertical\"\n}\n\nconst EASE = [0.22, 1, 0.36, 1] as const\nconst PAD_START = 12\nconst PAD_END = 18\n\nfunction useWidth() {\n  const [node, setNode] = useState<HTMLDivElement | null>(null)\n  const [width, setWidth] = useState(0)\n  useEffect(() => {\n    if (!node) return\n    const ro = new ResizeObserver(([entry]) => setWidth(entry.contentRect.width))\n    ro.observe(node)\n    return () => ro.disconnect()\n  }, [node])\n  return [setNode, width] as const\n}\n\nfunction wave(length: number, y: number, amp: number) {\n  let d = `M -48 ${y}`\n  for (let x = -48; x < length + 48; x += 24) d += ` q 6 ${-amp} 12 0 t 12 0`\n  return d\n}\n\nfunction ArrowGlyph() {\n  return (\n    <svg viewBox=\"0 0 16 16\" className=\"size-3.5\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.8} strokeLinecap=\"round\" strokeLinejoin=\"round\">\n      <path d=\"M5 11 11 5M6 5h5v5\" />\n    </svg>\n  )\n}\n\nexport function ChartProgress({\n  value,\n  max = 100,\n  target,\n  targetLabel = \"Target\",\n  label = \"Progress\",\n  showPercent = true,\n  segments,\n  thickness = 40,\n  length = 220,\n  texture = \"wave\",\n  remainder = \"hatch\",\n  showMarker = true,\n  orientation = \"horizontal\",\n  title,\n  description,\n  titleSize,\n  surface = \"dark\",\n  accent = CHART_ACCENT,\n  palette,\n  icon,\n  radius,\n  bare,\n  animate = true,\n  duration = 1.4,\n  delay = 0,\n  once = true,\n  valueFormat = \"number\",\n  currency,\n  decimals,\n  className,\n}: ChartProgressProps) {\n  const theme = chartTheme(surface, accent)\n  const { ref, shown, still } = useChartReveal(once, animate)\n  const format = useMemo(() => createFormatter(valueFormat, { currency, decimals }), [valueFormat, currency, decimals])\n  const id = useId().replace(/[^a-zA-Z0-9_-]/g, \"\")\n  const vertical = orientation === \"vertical\"\n\n  const parts = segments?.length ? segments : undefined\n  const total = parts ? parts.reduce((a, s) => a + Math.max(0, s.value), 0) : value\n  const cap = max > 0 ? max : 1\n  const amount = Math.max(0, Math.min(total, cap))\n  const series = parts\n    ? resolveSeries(parts.map((s, i) => ({ key: `s${i}`, label: s.label })), theme, palette)\n    : []\n\n  const progress = useMotionValue(still ? amount : 0)\n  const valueRef = useRef<HTMLSpanElement>(null)\n  const percentRef = useRef<HTMLSpanElement>(null)\n\n  useEffect(() => {\n    const write = (v: number) => {\n      const shownValue = (v / Math.max(amount, 1e-9)) * total\n      if (valueRef.current) valueRef.current.textContent = format(amount === total ? v : shownValue)\n      if (percentRef.current) percentRef.current.textContent = `${Math.round((v / cap) * 100)}%`\n    }\n    const unsubscribe = progress.on(\"change\", write)\n    if (still) {\n      progress.set(amount)\n      write(amount)\n      return unsubscribe\n    }\n    write(progress.get())\n    if (!shown) return unsubscribe\n    const controls = animateValue(progress, amount, { duration, delay, ease: EASE })\n    return () => {\n      controls.stop()\n      unsubscribe()\n    }\n  }, [progress, amount, total, cap, format, shown, still, duration, delay])\n\n  const [measureRef, measured] = useWidth()\n  const L = vertical ? length : measured\n  const T = thickness\n  const cross = PAD_START + T + PAD_END\n  const svgW = vertical ? cross : L\n  const svgH = vertical ? L : cross\n  const initial = still ? amount : 0\n\n  const head = (\n    <div className={cn(\"flex gap-3\", vertical ? \"flex-col\" : \"items-end justify-between\")}>\n      <div className=\"min-w-0\">\n        <p className=\"text-[13px] font-medium\" style={{ color: theme.muted }}>\n          {label}\n        </p>\n        <p className=\"mt-1 text-[13px] tabular-nums\" style={{ color: theme.muted }}>\n          {showPercent && (\n            <span ref={valueRef} className=\"font-semibold\" style={{ color: theme.fg }}>\n              {format(initial)}\n            </span>\n          )}{\" \"}\n          of {format(max)}\n        </p>\n      </div>\n      <span\n        ref={showPercent ? percentRef : valueRef}\n        className=\"text-[2.1rem] font-bold leading-[0.9] tracking-tight tabular-nums\"\n      >\n        {showPercent ? `${Math.round((initial / cap) * 100)}%` : format(initial)}\n      </span>\n    </div>\n  )\n\n  const bar = L > 0 && (\n    <BarSvg\n      id={id}\n      L={L}\n      T={T}\n      width={svgW}\n      height={svgH}\n      vertical={vertical}\n      progress={progress}\n      cap={cap}\n      target={target}\n      parts={parts}\n      series={series}\n      texture={texture}\n      remainder={remainder}\n      showMarker={showMarker}\n      theme={theme}\n      color={palette?.[0] ?? accent}\n      still={still}\n    />\n  )\n\n  const targetPos = target !== undefined ? Math.max(0, Math.min(1, target / cap)) : null\n  const targetTag =\n    targetPos !== null && L > 0 ? (\n      <span\n        className=\"pointer-events-none absolute whitespace-nowrap text-[11px] tabular-nums\"\n        style={{\n          color: theme.muted,\n          ...(vertical\n            ? { left: svgW + 6, top: L - targetPos * L, transform: \"translateY(-50%)\" }\n            : {\n                top: 0,\n                left: Math.max(0, Math.min(L, targetPos * L)),\n                transform: `translateX(${targetPos > 0.85 ? \"-100%\" : targetPos < 0.15 ? \"0\" : \"-50%\"})`,\n              }),\n        }}\n      >\n        {targetLabel} <span style={{ color: theme.fg }}>{format(target!)}</span>\n      </span>\n    ) : null\n\n  return (\n    <ChartCard\n      theme={theme}\n      title={title}\n      description={description}\n      titleSize={titleSize}\n      icon={icon === undefined ? <ArrowGlyph /> : icon}\n      radius={radius}\n      bare={bare}\n      className={className}\n    >\n      <div ref={ref} className={cn(\"flex\", vertical ? \"items-end gap-6\" : \"flex-col gap-2\")}>\n        {vertical ? (\n          <>\n            <div className=\"relative shrink-0\" style={{ width: svgW + (targetTag ? 64 : 0), height: L }}>\n              {bar}\n              {targetTag}\n            </div>\n            <div className=\"min-w-0 pb-1\">{head}</div>\n          </>\n        ) : (\n          <>\n            {head}\n            <div ref={measureRef} className=\"relative w-full\" style={{ paddingTop: targetTag ? 16 : 0 }}>\n              {targetTag}\n              {bar}\n            </div>\n          </>\n        )}\n      </div>\n      {parts && (\n        <div className=\"mt-4\">\n          <ChartLegend series={series} theme={theme} patternPrefix={id} />\n        </div>\n      )}\n    </ChartCard>\n  )\n}\n\nfunction BarSvg({\n  id,\n  L,\n  T,\n  width,\n  height,\n  vertical,\n  progress,\n  cap,\n  target,\n  parts,\n  series,\n  texture,\n  remainder,\n  showMarker,\n  theme,\n  color,\n  still,\n}: {\n  id: string\n  L: number\n  T: number\n  width: number\n  height: number\n  vertical: boolean\n  progress: MotionValue<number>\n  cap: number\n  target?: number\n  parts?: ChartProgressSegment[]\n  series: ResolvedSeries[]\n  texture: \"wave\" | \"solid\" | \"grain\"\n  remainder: \"hatch\" | \"track\" | \"none\"\n  showMarker: boolean\n  theme: ReturnType<typeof chartTheme>\n  color: string\n  still: boolean\n}) {\n  const fillLen = useTransform(progress, (v) => Math.max(0, (v / cap) * L))\n  const markerX = useTransform(fillLen, (x) => Math.min(L - 2, Math.max(2, x + 5)))\n  const markerOpacity = useTransform(fillLen, [0, 6], [0, 1])\n  const y0 = PAD_START\n  const r = T / 2\n  const pos = (v: number) => Math.max(0, Math.min(L, (v / cap) * L))\n  const starts = (parts ?? []).map((_, i, all) => all.slice(0, i).reduce((a, s) => a + Math.max(0, s.value), 0))\n\n  return (\n    <svg width={width} height={height} className=\"block overflow-visible\">\n      <defs>\n        <pattern id={`${id}-rest`} width=\"6\" height=\"6\" patternUnits=\"userSpaceOnUse\" patternTransform=\"rotate(45)\">\n          <rect width=\"6\" height=\"6\" fill={theme.track} />\n          <line x1=\"0\" y1=\"0\" x2=\"0\" y2=\"6\" stroke={theme.subtle} strokeWidth=\"2\" />\n        </pattern>\n        <pattern id={`${id}-grain`} width=\"4\" height=\"4\" patternUnits=\"userSpaceOnUse\">\n          <circle cx=\"1\" cy=\"1\" r=\"0.6\" fill=\"#fff\" fillOpacity=\"0.3\" />\n          <circle cx=\"3\" cy=\"3\" r=\"0.6\" fill=\"#000\" fillOpacity=\"0.16\" />\n        </pattern>\n        <clipPath id={`${id}-fill`}>\n          <motion.rect x=\"0\" y={y0} height={T} rx={r} width={fillLen} />\n        </clipPath>\n        {series.map((s) => (\n          <ChartPattern key={s.key} id={`${id}-${s.key}`} color={s.color} texture={s.texture} theme={theme} />\n        ))}\n      </defs>\n\n      <g transform={vertical ? `translate(0 ${L}) rotate(-90)` : undefined}>\n        {remainder !== \"none\" && (\n          <rect x=\"0\" y={y0} width={L} height={T} rx={r} fill={remainder === \"hatch\" ? `url(#${id}-rest)` : theme.track} />\n        )}\n\n        <g clipPath={`url(#${id}-fill)`}>\n          {parts ? (\n            <g>\n              {parts.map((s, i) => (\n                <SegmentRect\n                  key={i}\n                  fillLen={fillLen}\n                  start={pos(starts[i])}\n                  end={pos(starts[i] + Math.max(0, s.value))}\n                  y={y0}\n                  T={T}\n                  fill={textureFill(`${id}-${series[i].key}`, series[i].texture, series[i].color)}\n                  bg={series[i].texture === \"solid\" ? undefined : theme.bg}\n                  divider={i > 0 ? theme.bg : undefined}\n                />\n              ))}\n            </g>\n          ) : (\n            <rect x=\"0\" y={y0} width={L} height={T} fill={color} />\n          )}\n          {texture === \"wave\" && (\n            <>\n              <motion.path\n                d={wave(L, y0 + T * 0.68, T * 0.16)}\n                fill=\"none\"\n                stroke=\"#000\"\n                strokeOpacity=\"0.16\"\n                strokeWidth=\"3\"\n                animate={still ? undefined : { x: [0, -24] }}\n                transition={{ duration: 2.4, repeat: Infinity, ease: \"linear\" }}\n              />\n              <motion.path\n                d={wave(L, y0 + T * 0.36, T * 0.16)}\n                fill=\"none\"\n                stroke=\"#fff\"\n                strokeOpacity=\"0.14\"\n                strokeWidth=\"2\"\n                animate={still ? undefined : { x: [-24, 0] }}\n                transition={{ duration: 3.2, repeat: Infinity, ease: \"linear\" }}\n              />\n            </>\n          )}\n          {texture !== \"solid\" && <rect x=\"0\" y={y0} width={L} height={T} fill={`url(#${id}-grain)`} />}\n        </g>\n\n        {target !== undefined && (\n          <line\n            x1={pos(target)}\n            x2={pos(target)}\n            y1={y0 - 6}\n            y2={y0 + T + 4}\n            stroke={theme.fg}\n            strokeOpacity=\"0.7\"\n            strokeWidth=\"1.5\"\n            strokeDasharray=\"3 3\"\n          />\n        )}\n\n        {showMarker && (\n          <motion.g style={{ x: markerX, opacity: markerOpacity }}>\n            <line x1=\"0\" x2=\"0\" y1={y0 - 8} y2={y0 + T + 8} stroke={theme.fg} strokeWidth=\"3\" strokeLinecap=\"round\" />\n            <circle cx=\"0\" cy={y0 + T + 11} r=\"5.5\" fill={theme.fg} />\n            <circle cx=\"0\" cy={y0 + T + 11} r=\"2.2\" fill={color} />\n          </motion.g>\n        )}\n      </g>\n    </svg>\n  )\n}\n\nfunction SegmentRect({\n  fillLen,\n  start,\n  end,\n  y,\n  T,\n  fill,\n  bg,\n  divider,\n}: {\n  fillLen: MotionValue<number>\n  start: number\n  end: number\n  y: number\n  T: number\n  fill: string\n  bg?: string\n  divider?: string\n}) {\n  const width = useTransform(fillLen, (x) => Math.max(0, Math.min(end, x) - start))\n  return (\n    <g>\n      {bg && <motion.rect x={start} y={y} height={T} width={width} fill={bg} />}\n      <motion.rect x={start} y={y} height={T} width={width} fill={fill} />\n      {divider && <rect x={start - 1} y={y} width={2} height={T} fill={divider} />}\n    </g>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Docs, live playground and every prop: https://trytweenly.vercel.app/docs/components/chart-progress",
  "type": "registry:component"
}