Skip to content
UI Kit
Accent color

Candlestick chart

Prices as open, high, low and close. As candles, candles with volume below, a range switch from one month to a year, or OHLC bars with a moving average. Rising days hollow, falling days filled.

One of 6 components in KPI, live and map charts

4 variants

Variants

Candles

The last ninety trading days, forty-four on phones. The crosshair finds the day, the close sits as a tag on the axis.

Open in Studio

With volume

The traded volume in its own panel below, on its own scale and the same days, so there is never a second y axis.

Open in Studio

Range switch

One month, three months or a year. The year folds into weeks, so every candle keeps its body.

Open in Studio

OHLC bars

Open and close as ticks on a thin bar, quieter than candles, with a twenty day average drawn in the accent.

Open in Studio

Details

What Candlestick chart brings along, read from the registry and the source.

Variants
4
Version
1.0.0 · since 1.1.0
File
components/ui/candlestick-chart.tsx
Brings along
utilskitchartchart-figure
npm packages
None
Languages
English and German, 13 texts of its own

Props

The CandlestickChartProps interface from components/ui/candlestick-chart.tsx, with the comments from the code. Also takes every prop of Omit<ChartA11y, 'label'>, ChartFigureProps.

  • labeloptional
    string

    Accessible name of the chart. Default: the title.

  • variantoptional
    CandlestickVariant

    candleswith-volumerangesohlc

  • datarequired
    Candle[]

    Oldest first

  • formatoptional
    Intl.NumberFormatOptions

    Price format, e.g. { style: 'currency', currency: 'EUR' }

  • rangesoptional
    { label: string; days: number }[]

    ranges: the switch, days per option, 0 for everything as weeks. Default 1M, 3M, 1Y.

  • averageoptional
    number

    ohlc: days of the moving average. Default 20.

  • heightoptional
    number

    Height of the price plot in px. Default 300.

Built-in texts

Candlestick chart writes these texts itself. The language follows the KitProvider, the labels prop replaces single ones.

Show all 13 texts
openEnglish OpenGerman Eröffnung
highEnglish HighGerman Hoch
lowEnglish LowGerman Tief
closeEnglish CloseGerman Schluss
changeEnglish ChangeGerman Veränderung
volumeEnglish VolumeGerman Umsatz
riseEnglish Closed higherGerman Höher geschlossen
fallEnglish Closed lowerGerman Tiefer geschlossen
averageEnglish {n} day averageGerman Schnitt {n} Tage
rangeEnglish PeriodGerman Zeitraum
dateEnglish DateGerman Datum
weekEnglish Week of {d}German Woche ab {d}
summaryEnglish From {a} to {b}, low {lo}, high {hi}.German Von {a} bis {b}, Tief {lo}, Hoch {hi}.

Accessibility

Read from the source of the component.

  • Candlestick chart handles the keys Escape, Arrow up, Arrow down, Arrow left, Arrow right, Home, End, Page up and Page down itself.
  • Sets aria-checked and aria-label, so screen readers announce purpose and state.
  • Uses the roles radio and radiogroup.
  • The server renders the starting state in full, JavaScript only adds the interaction.

Motion

  • Brings animations of its own, calmly timed and without wobble.
  • With reduced motion (prefers-reduced-motion) it stays still. 1 places in the code switch motion off then.

Source

The file aiw add puts into your project. From then on it is yours.

Show the file, 356 lines
components/ui/candlestick-chart.tsx
'use client';

import { useState } from 'react';
import { cn, round, useTexts } from '@/lib/kit';
import { ChartRoot, ChartSurface, ChartTable, ChartTooltip, Swatch, niceTicks, numberFormatter, textWidth, tickFormatter, useEntrance, useIntlLocale, useShown, useWidth, type ChartA11y, type TooltipRow } from '@/components/ui/chart';
import { ChartFigure, PlotAxes, linear, type ChartFigureProps } from '@/components/ui/chart-figure';

/*
 * Prices over time as open, high, low and close.
 *   candles      a body from open to close and a wick from low to high
 *   with-volume  the candles and the traded volume in a second panel below,
 *                its own scale, the same time axis (never a second y axis)
 *   ranges       a switch for one month, three months and a year, the year
 *                folded into weeks so every candle stays readable
 *   ohlc         open and close as ticks on a bar, with a moving average
 *
 * Rising days are hollow, falling days filled: the shape tells the
 * direction, the success and destructive tones only repeat it. A crosshair
 * finds the day, the price of the day sits as a tag on the axis.
 */

export type CandlestickVariant = 'candles' | 'with-volume' | 'ranges' | 'ohlc';

export interface Candle {
  /** "2026-03-14" */
  date: string;
  open: number;
  high: number;
  low: number;
  close: number;
  volume?: number;
}

export interface CandlestickChartProps extends Omit<ChartA11y, 'label'>, ChartFigureProps {
  /** Accessible name of the chart. Default: the title. */
  label?: string;
  variant?: CandlestickVariant;
  /** Oldest first */
  data: Candle[];
  /** Price format, e.g. { style: 'currency', currency: 'EUR' } */
  format?: Intl.NumberFormatOptions;
  /** ranges: the switch, days per option, 0 for everything as weeks. Default 1M, 3M, 1Y. */
  ranges?: { label: string; days: number }[];
  /** ohlc: days of the moving average. Default 20. */
  average?: number;
  /** Height of the price plot in px. Default 300. */
  height?: number;
  className?: string;
}

const TEXTS = {
  en: { open: 'Open', high: 'High', low: 'Low', close: 'Close', change: 'Change', volume: 'Volume', rise: 'Closed higher', fall: 'Closed lower', average: '{n} day average', range: 'Period', date: 'Date', week: 'Week of {d}', summary: 'From {a} to {b}, low {lo}, high {hi}.' },
  de: { open: 'Eröffnung', high: 'Hoch', low: 'Tief', close: 'Schluss', change: 'Veränderung', volume: 'Umsatz', rise: 'Höher geschlossen', fall: 'Tiefer geschlossen', average: 'Schnitt {n} Tage', range: 'Zeitraum', date: 'Datum', week: 'Woche ab {d}', summary: 'Von {a} bis {b}, Tief {lo}, Hoch {hi}.' },
};

const UP = 'var(--kitfg-up)';
const DOWN = 'var(--kitfg-down)';

/** Folds days into weeks (Monday based) */
function weekly(days: Candle[]): Candle[] {
  const out: Candle[] = [];
  for (const d of days) {
    const dt = new Date(`${d.date}T00:00:00Z`);
    const monday = new Date(dt);
    monday.setUTCDate(dt.getUTCDate() - ((dt.getUTCDay() + 6) % 7));
    const key = monday.toISOString().slice(0, 10);
    const last = out[out.length - 1];
    if (last && last.date === key) {
      last.high = Math.max(last.high, d.high);
      last.low = Math.min(last.low, d.low);
      last.close = d.close;
      last.volume = (last.volume ?? 0) + (d.volume ?? 0);
    } else out.push({ ...d, date: key });
  }
  return out;
}

/**
 * Open, high, low and close per day: candles, with volume, with a range switch, or as OHLC bars.
 *   <CandlestickChart variant="with-volume" data={[{ date: '2026-03-02', open: 41.2, high: 42.8, low: 40.9, close: 42.5, volume: 182000 }]} />
 */
export function CandlestickChart(props: CandlestickChartProps) {
  const { variant = 'candles', data, format, average = 20, height: heightProp, label, summary, labels, animate = true, title, value, delta, meta, source, framed, className } = props;
  const t = useTexts(TEXTS, undefined);
  const intl = useIntlLocale();
  const fmt = numberFormatter(intl, format ?? { minimumFractionDigits: 2, maximumFractionDigits: 2 });
  const tick = tickFormatter(intl, format);
  const vol = numberFormatter(intl, { notation: 'compact', maximumFractionDigits: 1 });
  const pct = numberFormatter(intl, { style: 'percent', minimumFractionDigits: 1, maximumFractionDigits: 1, signDisplay: 'exceptZero' });
  const [wrapRef, width] = useWidth<HTMLDivElement>(720);
  const [rootRef, anim] = useEntrance<HTMLDivElement>(animate);
  const [active, setActive] = useState<number | null>(null);
  const { shown, open, jump } = useShown(active);
  const ranges = props.ranges ?? (intl === 'de-DE' ? [{ label: '1 M', days: 22 }, { label: '3 M', days: 65 }, { label: '1 J', days: 0 }] : [{ label: '1M', days: 22 }, { label: '3M', days: 65 }, { label: '1Y', days: 0 }]);
  const [range, setRange] = useState(1);

  const phone = width < 520;
  const name = label ?? title ?? '';
  // Which candles: ranges slice or fold, a phone shows fewer days of the plain variants
  const series: Candle[] = (() => {
    if (variant === 'ranges') {
      const r = ranges[range] ?? ranges[0];
      return r.days ? data.slice(-r.days) : weekly(data);
    }
    const max = phone ? 44 : 90;
    return data.length > max ? data.slice(-max) : data;
  })();
  const n = series.length;
  const showVolume = variant === 'with-volume' && series.some((d) => d.volume !== undefined);

  // Moving average over the full data so the first visible days have a value
  const offset = data.length - n;
  const ma: (number | null)[] =
    variant === 'ohlc'
      ? series.map((_, i) => {
          const end = offset + i;
          if (end + 1 < average) return null;
          let s = 0;
          for (let k = end - average + 1; k <= end; k++) s += data[k].close;
          return s / average;
        })
      : [];

  const lo = Math.min(...series.map((d) => d.low));
  const hi = Math.max(...series.map((d) => d.high));
  const nice = niceTicks(lo - (hi - lo) * 0.04, hi + (hi - lo) * 0.04, 5);
  const yLabels = nice.ticks.map(tick);
  const priceH = heightProp ?? (phone ? 260 : 300);
  const volH = showVolume ? 78 : 0;
  const height = priceH + volH + (showVolume ? 14 : 0);
  const left = Math.max(...yLabels.map((s) => textWidth(s, 11)), 20) + 14;
  const right = width - 8;
  const top = 12;
  const bottom = priceH - 28;
  const Y = linear(nice.min, nice.max, bottom, top);
  const band = (right - left) / Math.max(1, n);
  const cx = (i: number) => round(left + band * (i + 0.5));
  const body = Math.max(1.5, Math.min(11, band * 0.62));
  const vTop = priceH + 14;
  const vBottom = height - 2;
  const vMax = Math.max(1, ...series.map((d) => d.volume ?? 0));

  // Month labels on the first day of each month that fits
  const monthFmt = new Intl.DateTimeFormat(intl, { month: 'short', timeZone: 'UTC' });
  const dayFmt = new Intl.DateTimeFormat(intl, { weekday: 'short', day: 'numeric', month: 'long', year: 'numeric', timeZone: 'UTC' });
  const shortDay = new Intl.DateTimeFormat(intl, { day: 'numeric', month: 'short', timeZone: 'UTC' });
  const xTicks: { at: number; label: string }[] = [];
  let lastX = -Infinity;
  series.forEach((d, i) => {
    const m = d.date.slice(0, 7);
    if (i === 0 || m !== series[i - 1].date.slice(0, 7)) {
      const x = cx(i);
      const label = monthFmt.format(new Date(`${d.date}T00:00:00Z`));
      if (x - lastX > textWidth(label, 11) + 16 && x > left + 10) {
        xTicks.push({ at: x, label });
        lastX = x;
      }
    }
  });

  const weekly_ = variant === 'ranges' && !(ranges[range]?.days);
  const dateTitle = (d: Candle) => (weekly_ ? t.week.replace('{d}', shortDay.format(new Date(`${d.date}T00:00:00Z`))) : dayFmt.format(new Date(`${d.date}T00:00:00Z`)));
  const rows = (i: number): TooltipRow[] => {
    const d = series[i];
    const up = d.close >= d.open;
    const out: TooltipRow[] = [
      { key: 'c', label: t.close, value: fmt(d.close), color: up ? UP : DOWN, shape: 'rect' },
      { key: 'o', label: t.open, value: fmt(d.open) },
      { key: 'h', label: t.high, value: fmt(d.high) },
      { key: 'l', label: t.low, value: fmt(d.low) },
      { key: 'p', label: t.change, value: pct((d.close - d.open) / d.open) },
    ];
    if (d.volume !== undefined && showVolume) out.push({ key: 'v', label: t.volume, value: vol(d.volume) });
    if (variant === 'ohlc' && ma[i] !== null) out.push({ key: 'm', label: t.average.replace('{n}', String(average)), value: fmt(ma[i]!), color: 'var(--primary)', shape: 'line' });
    return out;
  };

  const pick = (clientX: number, el: HTMLElement) => {
    const r = el.getBoundingClientRect();
    const x = (clientX - r.left) * (width / (r.width || width));
    setActive(Math.max(0, Math.min(n - 1, Math.floor((x - left) / band))));
  };
  const onKeyDown = (e: React.KeyboardEvent) => {
    const cur = active ?? n;
    let next: number | null = cur;
    if (e.key === 'ArrowRight') next = Math.min(n - 1, cur + 1);
    else if (e.key === 'ArrowLeft') next = Math.max(0, cur - 1);
    else if (e.key === 'Home') next = 0;
    else if (e.key === 'End') next = n - 1;
    else if (e.key === 'PageUp') next = Math.max(0, cur - 5);
    else if (e.key === 'PageDown') next = Math.min(n - 1, cur + 5);
    else if (e.key === 'Escape') next = null;
    else return;
    e.preventDefault();
    setActive(next);
  };

  const first = series[0];
  const last = series[n - 1];
  const auto = first && last ? t.summary.replace('{a}', fmt(first.open)).replace('{b}', fmt(last.close)).replace('{lo}', fmt(lo)).replace('{hi}', fmt(hi)) : '';
  const live = active !== null && series[active] ? `${dateTitle(series[active])}. ${rows(active).map((r) => `${r.label} ${r.value}`).join(', ')}` : '';
  const table = <ChartTable caption={name} head={[t.date, t.open, t.high, t.low, t.close, ...(showVolume ? [t.volume] : [])]} rows={series.map((d) => [dateTitle(d), fmt(d.open), fmt(d.high), fmt(d.low), fmt(d.close), ...(showVolume ? [vol(d.volume ?? 0)] : [])])} />;

  const delay = (i: number) => (anim ? ({ '--kitch-d': `${round(60 + (i / Math.max(1, n)) * 700, 0)}ms` } as React.CSSProperties) : undefined);
  const activeD = shown !== null ? series[shown] : undefined;
  // The price tag sits on the axis, tick labels under it step aside
  const tagY = activeD ?? series[n - 1] ? Y((activeD ?? series[n - 1]).close) : null;

  const actions =
    variant === 'ranges' ? (
      <div role="radiogroup" aria-label={t.range} className="inline-flex h-9 items-center rounded-md bg-muted p-[3px] shadow-[inset_0_0_0_1px_var(--border)]">
        {ranges.map((r, i) => (
          <button
            key={r.label}
            type="button"
            role="radio"
            aria-checked={range === i}
            tabIndex={range === i ? 0 : -1}
            onClick={() => {
              setRange(i);
              setActive(null);
            }}
            onKeyDown={(e) => {
              const step = e.key === 'ArrowRight' || e.key === 'ArrowDown' ? 1 : e.key === 'ArrowLeft' || e.key === 'ArrowUp' ? -1 : 0;
              if (!step) return;
              e.preventDefault();
              const k = (range + step + ranges.length) % ranges.length;
              setRange(k);
              (e.currentTarget.parentElement?.children[k] as HTMLElement | undefined)?.focus();
            }}
            className={cn("relative h-full min-w-11 rounded-[5px] px-3 text-[13px] font-medium tabular-nums transition-[background-color,color,box-shadow] duration-300 after:absolute after:-inset-y-[7px] after:inset-x-0 after:content-['']", range === i ? 'bg-background text-foreground shadow-[0_0_0_1px_var(--border),var(--shadow-sm)] dark:bg-[color-mix(in_oklab,var(--foreground)_10%,var(--background))]' : 'text-muted-foreground hover:text-foreground')}
          >
            {r.label}
          </button>
        ))}
      </div>
    ) : undefined;

  return (
    <ChartFigure title={title} value={value} delta={delta} meta={meta} source={source} framed={framed} actions={actions} className={className}>
      <ChartRoot anim={anim} rootRef={rootRef}>
        <ul className="mb-4 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-[12px] text-muted-foreground">
          <li className="flex items-center gap-1.5">
            <svg width="10" height="14" viewBox="0 0 10 14" aria-hidden>
              <path d="M5 0V14" stroke={UP} strokeWidth="1.5" />
              <rect x="1.25" y="3.25" width="7.5" height="7.5" rx="1.5" fill="var(--card)" stroke={UP} strokeWidth="1.5" />
            </svg>
            {t.rise}
          </li>
          <li className="flex items-center gap-1.5">
            <svg width="10" height="14" viewBox="0 0 10 14" aria-hidden>
              <path d="M5 0V14" stroke={DOWN} strokeWidth="1.5" />
              <rect x="1" y="3" width="8" height="8" rx="1.5" fill={DOWN} />
            </svg>
            {t.fall}
          </li>
          {variant === 'ohlc' && (
            <li className="flex items-center gap-1.5">
              <Swatch shape="line" color="var(--primary)" />
              {t.average.replace('{n}', String(average))}
            </li>
          )}
        </ul>
        <div ref={wrapRef}>
          <ChartSurface label={name} summary={summary ?? auto} labels={labels} live={live} table={table} onKeyDown={onKeyDown} onFocus={(e) => e.target === e.currentTarget && active === null && e.currentTarget.matches(':focus-visible') && setActive(n - 1)} onBlur={() => setActive(null)} onPointerMove={(e) => pick(e.clientX, e.currentTarget)} onPointerDown={(e) => pick(e.clientX, e.currentTarget)} onPointerLeave={(e) => e.pointerType === 'mouse' && setActive(null)}>
            <svg key={variant === 'ranges' ? range : 0} width="100%" height={height} viewBox={`0 0 ${width} ${height}`} aria-hidden className={cn('block overflow-visible', variant === 'ranges' && 'animate-[kitch-fade_.45s_var(--ease-kit)] motion-reduce:animate-none')}>
              <PlotAxes left={left} right={right} top={top} bottom={bottom} height={priceH} yTicks={nice.ticks.map((v, i) => ({ at: Y(v), label: tagY !== null && Math.abs(Y(v) - tagY) < 14 ? '' : yLabels[i] }))} xTicks={xTicks} />
              {/* Volume panel: own scale, same days */}
              {showVolume && (
                <g>
                  <line x1={left} x2={right} y1={vBottom} y2={vBottom} stroke="var(--kitch-axis)" strokeWidth="1" shapeRendering="crispEdges" />
                  <text x={left - 10} y={vTop + 4} dy="0.34em" textAnchor="end" className="text-[11px] tabular-nums" fill="var(--muted-foreground)">
                    {vol(vMax)}
                  </text>
                  <text x={left - 10} y={vBottom} textAnchor="end" className="text-[11px]" fill="var(--muted-foreground)">
                    {t.volume}
                  </text>
                  {series.map((d, i) => {
                    const h = ((d.volume ?? 0) / vMax) * (vBottom - vTop);
                    return <rect key={d.date} x={round(cx(i) - body / 2)} y={round(vBottom - h)} width={round(body)} height={round(Math.max(0.5, h))} rx="1" fill={d.close >= d.open ? UP : DOWN} opacity={active === i ? 0.75 : 0.4} className={anim ? 'kitch-grow-y' : undefined} style={delay(i)} />;
                  })}
                </g>
              )}
              {/* Crosshair */}
              {activeD && (
                <g className="kitch-glide" style={{ opacity: open ? 1 : 0 }}>
                  <line x1={cx(shown!)} x2={cx(shown!)} y1={top} y2={showVolume ? vBottom : bottom} stroke="var(--kitch-axis)" strokeWidth="1" />
                  <line x1={left} x2={right} y1={Y(activeD.close)} y2={Y(activeD.close)} stroke="var(--kitch-axis)" strokeWidth="1" strokeDasharray="3 3" />
                </g>
              )}
              {/* Moving average */}
              {variant === 'ohlc' && (
                <path
                  d={ma.map((v, i) => (v === null ? '' : `${i && ma[i - 1] !== null ? 'L' : 'M'}${cx(i)} ${Y(v)}`)).join('')}
                  pathLength={1}
                  fill="none"
                  stroke="var(--primary)"
                  strokeWidth="2"
                  strokeLinejoin="round"
                  className={anim ? 'kitch-draw' : undefined}
                  style={anim ? ({ '--kitch-d': '600ms', '--kitch-t': '1.2s' } as React.CSSProperties) : undefined}
                />
              )}
              {/* Candles or bars */}
              <g>
                {series.map((d, i) => {
                  const up = d.close >= d.open;
                  const color = up ? UP : DOWN;
                  const x = cx(i);
                  const yo = Y(d.open);
                  const yc = Y(d.close);
                  const faded = active !== null && active !== i;
                  if (variant === 'ohlc')
                    return (
                      <g key={d.date} className={anim ? 'kitch-fade' : undefined} style={delay(i)} opacity={faded ? 0.6 : 1}>
                        <line x1={x} x2={x} y1={Y(d.high)} y2={Y(d.low)} stroke={color} strokeWidth="1.5" />
                        <line x1={round(x - body / 2)} x2={x} y1={yo} y2={yo} stroke={color} strokeWidth="1.5" />
                        <line x1={x} x2={round(x + body / 2)} y1={yc} y2={yc} stroke={color} strokeWidth="1.5" />
                      </g>
                    );
                  const top_ = Math.min(yo, yc);
                  const h = Math.max(1, Math.abs(yc - yo));
                  return (
                    <g key={d.date} className={anim ? 'kitch-grow-y' : undefined} style={{ ...delay(i), transformOrigin: `${x}px ${round((Y(d.high) + Y(d.low)) / 2)}px`, transformBox: 'view-box' }} opacity={faded ? 0.6 : 1}>
                      <line x1={x} x2={x} y1={Y(d.high)} y2={Y(d.low)} stroke={color} strokeWidth="1" />
                      <rect x={round(x - body / 2 + (up ? 0.6 : 0))} y={round(top_ + (up ? 0.6 : 0))} width={round(body - (up ? 1.2 : 0))} height={round(Math.max(1, h - (up ? 1.2 : 0)))} rx={body > 6 ? 1.5 : 0.5} fill={up ? 'var(--kitch-surface)' : color} stroke={up ? color : 'none'} strokeWidth={up ? 1.2 : 0} />
                    </g>
                  );
                })}
              </g>
              {/* Price tag of the active or last day on the axis */}
              {(() => {
                const d = activeD ?? last;
                if (!d) return null;
                const y = Y(d.close);
                const text = fmt(d.close);
                const w = textWidth(text, 11) + 12;
                const up = d.close >= d.open;
                return (
                  <g className="transition-transform duration-200" style={{ transform: `translateY(${round(y)}px)` }}>
                    <rect x={left - w - 4} y={-9} width={w} height={18} rx="4" fill="var(--foreground)" />
                    <rect x={left - 3} y={-4} width={3} height={8} rx="1" fill={up ? UP : DOWN} />
                    <text x={left - 4 - w / 2} y={0} dy="0.34em" textAnchor="middle" className="text-[11px] font-semibold tabular-nums" fill="var(--background)">
                      {text}
                    </text>
                  </g>
                );
              })()}
            </svg>
            <ChartTooltip open={open} jump={jump} x={shown !== null ? cx(shown) : 0} y={top} width={width} offset={body / 2 + 12} title={activeD ? dateTitle(activeD) : ''} rows={shown !== null && series[shown] ? rows(shown) : []} />
          </ChartSurface>
        </div>
      </ChartRoot>
    </ChartFigure>
  );
}

Frequent questions

How do I install Candlestick chart?

With aiw add @ai-website/candlestick-chart. The command puts components/ui/candlestick-chart.tsx into your project, the code is yours from then on. It brings along chart and chart-figure. It needs no further npm packages.

Which variants does Candlestick chart have?

4 variants, Candles, With volume, Range switch and OHLC bars. You pick one with the variant prop, for example variant="with-volume".

Can I translate the texts of Candlestick chart?

Yes. Candlestick chart carries 13 texts of its own in English and German, such as “Open”, “High” and “Low”. The language follows the KitProvider, single texts change through the labels prop.

Can an AI assistant build with Candlestick chart?

Yes. Through the MCP server of the aiw CLI, Claude, Cursor, GitHub Copilot, Windsurf, Codex and Gemini find Candlestick chart with aiw_components and install it with aiw_add @ai-website/candlestick-chart. Name the variant in your prompt, for example variant="candles", and the assistant writes your content straight into the props.

Can I adjust Candlestick chart without code?

Yes, in the Studio. All 4 variants are editable there, texts, lists and switches. The Studio writes the matching CandlestickChart code to copy.

Often searched

Jump to

Copied to the clipboard