{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "consumption-chart",
  "title": "ConsumptionChart",
  "description": "Interactive consumption time series with labelled axes, unit-aware totals, a scrubbable readout, and a legend that filters.",
  "dependencies": [
    "recharts"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.neon.com/r/neon-tokens.json",
    "https://ui.neon.com/r/skeleton.json",
    "https://ui.neon.com/r/chart.json"
  ],
  "files": [
    {
      "path": "src/components/consumption-chart/consumption-chart.tsx",
      "content": "\"use client\";\n\nimport type { ComponentProps, ReactNode } from \"react\";\nimport {\n  Area,\n  AreaChart,\n  Bar,\n  BarChart,\n  CartesianGrid,\n  XAxis,\n  YAxis,\n} from \"recharts\";\n\nimport type { ChartConfig } from \"@/components/ui/chart\";\nimport {\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n} from \"@/components/ui/chart\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { useControllableState } from \"@/hooks/use-controllable-state\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ConsumptionGranularityOption = \"hourly\" | \"daily\" | \"monthly\";\n\nexport interface ConsumptionSeries {\n  /** Stable id, e.g. \"compute_unit_seconds\". Keys into each point's values. */\n  id: string;\n  /** Legend and tooltip label. */\n  label: string;\n  /** Any CSS color; defaults to the --chart-n token for its position. */\n  color?: string;\n  /**\n   * Unit of the values, e.g. \"CU-hrs\" or \"GB-mo\". Series that don't share\n   * a unit are never stacked or summed together — CU-hours plus GB-months\n   * is not a quantity.\n   */\n  unit?: string;\n}\n\nexport interface ConsumptionPoint {\n  /** Axis and tooltip label, e.g. \"Feb 4\" or \"14:00\". */\n  label: string;\n  /** Series id -> value, already converted to the display unit. */\n  values: Record<string, number>;\n}\n\nexport type ConsumptionChartProps = Omit<\n  ComponentProps<\"section\">,\n  \"children\" | \"onChange\"\n> & {\n  /** Buckets, oldest first. */\n  data: ConsumptionPoint[];\n  /** Series to plot, in stacking order (first sits at the bottom). */\n  series: ConsumptionSeries[];\n  /** \"area\" reads as a trend, \"bar\" as discrete buckets. */\n  variant?: \"area\" | \"bar\";\n  /**\n   * Stack series into a total. Ignored when the visible series carry more\n   * than one unit, where stacking would be arithmetic on unlike things.\n   */\n  stacked?: boolean;\n  /** Controlled visible series. */\n  activeSeriesIds?: string[];\n  /** Uncontrolled initial visible series; defaults to all of them. */\n  defaultActiveSeriesIds?: string[];\n  onActiveSeriesIdsChange?: (ids: string[]) => void;\n  /** Controlled granularity; pair with `granularities` to show the switch. */\n  granularity?: ConsumptionGranularityOption;\n  defaultGranularity?: ConsumptionGranularityOption;\n  onGranularityChange?: (granularity: ConsumptionGranularityOption) => void;\n  /** Granularities to offer. Omit to hide the switch. */\n  granularities?: ConsumptionGranularityOption[];\n  /** Panel heading. */\n  title?: string;\n  /** Right-side header slot, e.g. a DateRangePicker. Sits before the switch. */\n  action?: ReactNode;\n  /** Value formatter for the axis, readout, and legend totals. */\n  formatValue?: (value: number) => string;\n  /** Quiet footer, e.g. a metering-lag notice. */\n  meteredThrough?: string;\n  isLoading?: boolean;\n  error?: string | null;\n  /** Shown when `data` is empty. */\n  empty?: ReactNode;\n};\n\n/* ─────────────────────────────────────────────────────────\n * CHART STORYBOARD\n *\n *  rest      Recharts draws the plot; both axes are\n *            labelled, so the chart still answers questions\n *            in a screenshot\n *  color     one hue, five steps of lightness, and a\n *            hairline of --card between stacked bands.\n *            Bands separate by value and by that gap, which\n *            survives at any size — texture did not\n *  totals    the header sums per unit, never across them.\n *            CU-hours and GB-months don't add up, and one\n *            number pretending they do is worse than none\n *  stacking  only within a unit. Mixed units fall back to\n *            overlaid series and say so\n *  scrub     Recharts' accessibility layer gives the plot\n *            focus and arrow-key movement; the tooltip\n *            names the bucket and every visible series\n *  toggle    the legend is the filter. The last visible\n *            series won't turn itself off, because an empty\n *            chart answers nothing\n * ───────────────────────────────────────────────────────── */\n\nconst PALETTE = [\n  \"var(--chart-1)\",\n  \"var(--chart-2)\",\n  \"var(--chart-3)\",\n  \"var(--chart-4)\",\n  \"var(--chart-5)\",\n] as const;\n\n/**\n * Bands are tinted, not solid: a stack of fully saturated fills reads as\n * slabs of paint and the card stops looking like a chart. The series\n * color goes on the top edge instead, where it draws the boundary.\n */\nconst BAND_FILL_BOTTOM = 0.5;\nconst BAND_FILL_TOP = 0.24;\nconst OVERLAY_FILL = 0.16;\nconst BAND_STROKE = 2;\n\n/**\n * Bands fade as they stack. Depth is then encoded twice, by the ramp and\n * by weight, so two neighbouring greens can't collapse into one mass.\n */\nconst fillAt = (index: number, count: number) => {\n  if (count <= 1) {\n    return BAND_FILL_BOTTOM;\n  }\n\n  const ratio = index / (count - 1);\n\n  return BAND_FILL_BOTTOM - ratio * (BAND_FILL_BOTTOM - BAND_FILL_TOP);\n};\nconst AXIS_TICK_COUNT = 5;\nconst MAX_X_LABELS = 7;\n\nconst COMPACT = new Intl.NumberFormat(\"en-US\", {\n  maximumFractionDigits: 2,\n  notation: \"compact\",\n});\n\nconst defaultFormat = (value: number) => COMPACT.format(value);\n\nconst colorOf = (series: ConsumptionSeries, index: number): string =>\n  series.color ?? PALETTE[index % PALETTE.length] ?? PALETTE[0];\n\n/** Distinct units across the visible series; \"\" counts as its own unit. */\nconst unitsOf = (visible: ConsumptionSeries[]) => [\n  ...new Set(visible.map((item) => item.unit ?? \"\")),\n];\n\n/**\n * Sums per unit, never across. A chart of CU-hours and GB-months reads\n * \"102.8 CU-hrs · 77.5 GB-mo\" rather than one meaningless number.\n */\nconst totalsByUnit = (\n  visible: ConsumptionSeries[],\n  totals: Record<string, number>\n): [string, number][] => {\n  const byUnit = new Map<string, number>();\n\n  for (const item of visible) {\n    const unit = item.unit ?? \"\";\n\n    byUnit.set(unit, (byUnit.get(unit) ?? 0) + (totals[item.id] ?? 0));\n  }\n\n  return [...byUnit.entries()];\n};\n\nconst HeadlineTotals = ({\n  formatValue,\n  totals,\n}: {\n  formatValue: (value: number) => string;\n  totals: [string, number][];\n}) => (\n  <>\n    {totals.map(([unit, total], index) => (\n      <span className=\"flex items-baseline gap-1\" key={unit}>\n        {index > 0 ? (\n          <span className=\"text-muted-foreground/40 text-xs\">·</span>\n        ) : null}\n        <span className=\"font-medium font-mono text-foreground text-sm tabular-nums\">\n          {formatValue(total)}\n        </span>\n        {unit ? (\n          <span className=\"font-mono text-[10px] text-muted-foreground\">\n            {unit}\n          </span>\n        ) : null}\n      </span>\n    ))}\n  </>\n);\n\nconst LegendItem = ({\n  color,\n  formatValue,\n  isActive,\n  onToggle,\n  series,\n  total,\n}: {\n  color: string;\n  formatValue: (value: number) => string;\n  isActive: boolean;\n  onToggle: () => void;\n  series: ConsumptionSeries;\n  total: number;\n}) => (\n  <button\n    aria-pressed={isActive}\n    className={cn(\n      \"flex items-baseline gap-1.5 rounded-md px-1.5 py-1 text-left transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40\",\n      !isActive && \"opacity-45\"\n    )}\n    onClick={onToggle}\n    type=\"button\"\n  >\n    <span\n      aria-hidden=\"true\"\n      className={cn(\n        \"size-2.5 shrink-0 translate-y-[1px] rounded-[2px]\",\n        !isActive && \"ring-1 ring-muted-foreground/40\"\n      )}\n      style={{ background: isActive ? color : \"transparent\" }}\n    />\n    <span className=\"font-mono text-[11px] text-muted-foreground\">\n      {series.label}\n    </span>\n    <span className=\"font-medium font-mono text-[11px] text-foreground tabular-nums\">\n      {formatValue(total)}\n    </span>\n    {series.unit ? (\n      <span className=\"font-mono text-[10px] text-muted-foreground/60\">\n        {series.unit}\n      </span>\n    ) : null}\n  </button>\n);\n\n/* The pill slides between options over 220ms; the movement is the\n   explanation, so the reader sees which of two things they picked. */\nconst SWITCH_MS = 220;\nconst SWITCH_EASE = \"cubic-bezier(0.32, 0.72, 0, 1)\";\nconst SWITCH_PAD = 4;\n\nconst GranularitySwitch = ({\n  onChange,\n  options,\n  value,\n}: {\n  onChange: (next: ConsumptionGranularityOption) => void;\n  options: ConsumptionGranularityOption[];\n  value: ConsumptionGranularityOption;\n}) => {\n  const index = Math.max(0, options.indexOf(value));\n\n  return (\n    <fieldset\n      aria-label=\"Granularity\"\n      className=\"relative grid grid-flow-col items-center rounded-full border border-border/60 p-0.5\"\n      style={{ gridAutoColumns: \"1fr\" }}\n    >\n      <span\n        aria-hidden=\"true\"\n        className=\"absolute inset-y-0.5 left-0.5 rounded-full bg-primary/10 motion-reduce:transition-none\"\n        style={{\n          transform: `translateX(${index * 100}%)`,\n          transition: `transform ${SWITCH_MS}ms ${SWITCH_EASE}`,\n          width: `calc((100% - ${SWITCH_PAD}px) / ${options.length})`,\n        }}\n      />\n      {options.map((option) => (\n        <button\n          aria-pressed={option === value}\n          className={cn(\n            \"relative z-10 rounded-full px-2 py-0.5 text-center font-mono text-[10px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 motion-reduce:transition-none\",\n            option === value\n              ? \"text-primary\"\n              : \"text-muted-foreground hover:text-foreground\"\n          )}\n          key={option}\n          onClick={() => onChange(option)}\n          style={{ transition: `color ${SWITCH_MS}ms ${SWITCH_EASE}` }}\n          type=\"button\"\n        >\n          {option}\n        </button>\n      ))}\n    </fieldset>\n  );\n};\n\n/** Everything the plot and the header need, derived from props. */\nconst shapeChart = (\n  data: ConsumptionPoint[],\n  series: ConsumptionSeries[],\n  visible: ConsumptionSeries[]\n) => {\n  const rows = data.map((point) => ({ ...point.values, label: point.label }));\n  const totals: Record<string, number> = {};\n\n  for (const item of series) {\n    let sum = 0;\n\n    for (const point of data) {\n      sum += point.values[item.id] ?? 0;\n    }\n\n    totals[item.id] = sum;\n  }\n\n  const config: ChartConfig = Object.fromEntries(\n    series.map((item, index) => [\n      item.id,\n      { color: colorOf(item, index), label: item.label },\n    ])\n  );\n\n  return {\n    config,\n    headlineTotals: totalsByUnit(visible, totals),\n    rows,\n    totals,\n    xInterval: Math.max(0, Math.ceil(rows.length / MAX_X_LABELS) - 1),\n  };\n};\n\n/**\n * ChartTooltipContent's `formatter` replaces the entire row, swatch and\n * label included, so a formatter that returns just a number leaves the\n * reader with three bare values and no idea which series is which.\n */\nconst TooltipRow = ({\n  color,\n  label,\n  unit,\n  value,\n}: {\n  color: string;\n  label: string;\n  unit?: string;\n  value: string;\n}) => (\n  <>\n    <span\n      aria-hidden=\"true\"\n      className=\"size-2.5 shrink-0 translate-y-[1px] rounded-[2px]\"\n      style={{ background: color }}\n    />\n    <div className=\"flex flex-1 items-center justify-between gap-3 leading-none\">\n      <span className=\"text-muted-foreground\">{label}</span>\n      <span className=\"font-medium font-mono text-foreground tabular-nums\">\n        {value}\n        {unit ? (\n          <span className=\"ml-1 font-normal text-muted-foreground/70\">\n            {unit}\n          </span>\n        ) : null}\n      </span>\n    </div>\n  </>\n);\n\n/** Builds the tooltip row renderer for a given series list. */\nconst tooltipFormatter = (\n  series: ConsumptionSeries[],\n  formatValue: (value: number) => string\n) =>\n  function TooltipRowRenderer(\n    value: unknown,\n    name: unknown,\n    entry: { color?: string }\n  ) {\n    const item = series.find((candidate) => candidate.id === name);\n\n    return (\n      <TooltipRow\n        color={String(entry?.color ?? \"var(--chart-1)\")}\n        label={item?.label ?? String(name)}\n        unit={item?.unit}\n        value={formatValue(Number(value))}\n      />\n    );\n  };\n\n/** The plot itself: axes, marks, tooltip. */\nconst Plot = ({\n  colorFor,\n  config,\n  formatValue,\n  granularityKey,\n  isStacked,\n  rows,\n  series,\n  variant,\n  visible,\n  xInterval,\n}: {\n  colorFor: (item: ConsumptionSeries) => string;\n  config: ChartConfig;\n  formatValue: (value: number) => string;\n  granularityKey: string;\n  isStacked: boolean;\n  rows: Record<string, number | string>[];\n  series: ConsumptionSeries[];\n  variant: \"area\" | \"bar\";\n  visible: ConsumptionSeries[];\n  xInterval: number;\n}) => {\n  const Chart = variant === \"bar\" ? BarChart : AreaChart;\n\n  return (\n    <ChartContainer\n      className=\"aspect-auto h-[220px] w-full\"\n      config={config}\n      // Re-mounts on granularity change so the new dataset animates in\n      // rather than snapping between two unrelated shapes.\n      key={granularityKey}\n    >\n      <Chart accessibilityLayer data={rows} margin={{ left: 4, right: 8 }}>\n        <CartesianGrid\n          stroke=\"var(--border)\"\n          strokeOpacity={0.5}\n          vertical={false}\n        />\n        <XAxis\n          axisLine={false}\n          dataKey=\"label\"\n          interval={xInterval}\n          tickLine={false}\n          tickMargin={8}\n        />\n        <YAxis\n          axisLine={false}\n          tickCount={AXIS_TICK_COUNT}\n          tickFormatter={formatValue}\n          tickLine={false}\n          width={44}\n        />\n        <ChartTooltip\n          content={\n            <ChartTooltipContent\n              formatter={tooltipFormatter(series, formatValue)}\n            />\n          }\n          cursor={{ stroke: \"var(--border)\", strokeDasharray: \"2 3\" }}\n        />\n\n        {visible.map((item, index) =>\n          variant === \"bar\" ? (\n            <Bar\n              dataKey={item.id}\n              fill={colorFor(item)}\n              fillOpacity={isStacked ? fillAt(index, visible.length) : 0.4}\n              key={item.id}\n              radius={2}\n              stackId={isStacked ? \"a\" : undefined}\n              stroke={colorFor(item)}\n              strokeWidth={1}\n            />\n          ) : (\n            <Area\n              dataKey={item.id}\n              fill={colorFor(item)}\n              fillOpacity={\n                isStacked ? fillAt(index, visible.length) : OVERLAY_FILL\n              }\n              key={item.id}\n              stackId={isStacked ? \"a\" : undefined}\n              // The series color rides the top edge of its own band, so\n              // neighbours separate by line rather than by slab.\n              stroke={colorFor(item)}\n              strokeWidth={BAND_STROKE}\n              type=\"monotone\"\n            />\n          )\n        )}\n      </Chart>\n    </ChartContainer>\n  );\n};\n\nexport const ConsumptionChart = ({\n  action,\n  activeSeriesIds,\n  className,\n  data,\n  defaultActiveSeriesIds,\n  defaultGranularity = \"daily\",\n  empty,\n  error = null,\n  formatValue = defaultFormat,\n  granularities,\n  granularity,\n  isLoading = false,\n  meteredThrough,\n  onActiveSeriesIdsChange,\n  onGranularityChange,\n  series,\n  stacked = true,\n  title = \"consumption\",\n  variant = \"area\",\n  ...props\n}: ConsumptionChartProps) => {\n  const [activeIds, setActiveIds] = useControllableState<string[]>({\n    caller: \"ConsumptionChart\",\n    defaultProp: defaultActiveSeriesIds ?? series.map((item) => item.id),\n    onChange: onActiveSeriesIdsChange,\n    prop: activeSeriesIds,\n  });\n\n  const [activeGranularity, setActiveGranularity] =\n    useControllableState<ConsumptionGranularityOption>({\n      caller: \"ConsumptionChart\",\n      defaultProp: defaultGranularity,\n      onChange: onGranularityChange,\n      prop: granularity,\n    });\n\n  const visible = series.filter((item) => activeIds.includes(item.id));\n  const units = unitsOf(visible);\n  // Stacking adds values together. Across units that is not a quantity, so\n  // overlay instead and say why rather than drawing a meaningless height.\n  const isMixedUnits = units.length > 1;\n  const isStacked = stacked && !isMixedUnits;\n\n  // Recharts wants one flat row per bucket, keyed by series id.\n  const { config, headlineTotals, rows, totals, xInterval } = shapeChart(\n    data,\n    series,\n    visible\n  );\n\n  const toggleSeries = (id: string) => {\n    if (activeIds.length === 1 && activeIds[0] === id) {\n      return;\n    }\n\n    setActiveIds(\n      activeIds.includes(id)\n        ? activeIds.filter((item) => item !== id)\n        : series\n            .filter((item) => item.id === id || activeIds.includes(item.id))\n            .map((item) => item.id)\n    );\n  };\n\n  if (isLoading) {\n    return (\n      <section\n        className={cn(\n          \"rounded-lg border border-border/60 bg-card p-4\",\n          className\n        )}\n        data-slot=\"consumption-chart\"\n        {...props}\n      >\n        <Skeleton className=\"h-4 w-32\" />\n        <Skeleton className=\"mt-4 h-[220px] w-full\" />\n        <Skeleton className=\"mt-4 h-4 w-64\" />\n      </section>\n    );\n  }\n\n  return (\n    <section\n      className={cn(\n        \"rounded-lg border border-border/60 bg-card p-4\",\n        className\n      )}\n      data-slot=\"consumption-chart\"\n      {...props}\n    >\n      <header className=\"mb-3 flex flex-wrap items-center justify-between gap-3\">\n        <div className=\"flex flex-wrap items-baseline gap-x-2 gap-y-0.5\">\n          <h3 className=\"font-mono text-muted-foreground text-xs\">{title}</h3>\n          <HeadlineTotals formatValue={formatValue} totals={headlineTotals} />\n        </div>\n        <div className=\"flex items-center gap-2\">\n          {action}\n          {granularities?.length ? (\n            <GranularitySwitch\n              onChange={setActiveGranularity}\n              options={granularities}\n              value={activeGranularity}\n            />\n          ) : null}\n        </div>\n      </header>\n\n      {error ? (\n        <p\n          className=\"mb-3 flex items-baseline gap-2 text-sm\"\n          data-slot=\"consumption-chart-error\"\n          role=\"alert\"\n        >\n          <span className=\"font-mono text-destructive text-xs\">error</span>\n          <span className=\"text-foreground\">{error}</span>\n        </p>\n      ) : null}\n\n      {rows.length === 0 ? (\n        <div className=\"flex h-[220px] items-center justify-center rounded-md border border-border/50 border-dashed\">\n          {empty ?? (\n            <p className=\"font-mono text-[11px] text-muted-foreground\">\n              no consumption in this window\n            </p>\n          )}\n        </div>\n      ) : (\n        <Plot\n          colorFor={(item) => colorOf(item, series.indexOf(item))}\n          config={config}\n          formatValue={formatValue}\n          granularityKey={activeGranularity}\n          isStacked={isStacked}\n          rows={rows}\n          series={series}\n          variant={variant}\n          visible={visible}\n          xInterval={xInterval}\n        />\n      )}\n\n      <div className=\"mt-3 flex flex-wrap items-center gap-x-1 gap-y-0.5 border-border/50 border-t pt-3\">\n        {series.map((item, index) => (\n          <LegendItem\n            color={colorOf(item, index)}\n            formatValue={formatValue}\n            isActive={activeIds.includes(item.id)}\n            key={item.id}\n            onToggle={() => toggleSeries(item.id)}\n            series={item}\n            total={totals[item.id] ?? 0}\n          />\n        ))}\n      </div>\n\n      {isMixedUnits && stacked ? (\n        <p\n          className=\"mt-2 text-[10px] text-muted-foreground/70\"\n          data-slot=\"consumption-chart-units\"\n        >\n          series overlaid, not stacked · {units.filter(Boolean).join(\" and \")}{\" \"}\n          don&apos;t add up\n        </p>\n      ) : null}\n\n      {meteredThrough ? (\n        <p\n          className=\"mt-2 text-[10px] text-muted-foreground/70 tabular-nums\"\n          data-slot=\"consumption-chart-lag\"\n        >\n          {meteredThrough}\n        </p>\n      ) : null}\n    </section>\n  );\n};\n",
      "type": "registry:component"
    },
    {
      "path": "src/hooks/use-controllable-state.tsx",
      "content": "/* oxlint-disable func-style, no-empty-function, no-shadow, no-use-before-define, react/react-compiler, max-lines-per-function, max-lines, no-magic-numbers -- vendored from chanhdai.com/r/elastic-slider.json (MIT, @iamncdai); kept diffable against upstream */\nimport * as React from \"react\";\n\n// use-layout-effect.tsx\n// https://github.com/radix-ui/primitives/blob/main/packages/react/use-layout-effect/src/use-layout-effect.tsx\n\n/**\n * On the server, React emits a warning when calling `useLayoutEffect`.\n * This is because neither `useLayoutEffect` nor `useEffect` run on the server.\n * We use this safe version which suppresses the warning by replacing it with a noop on the server.\n *\n * See: https://reactjs.org/docs/hooks-reference.html#uselayouteffect\n */\nconst useLayoutEffect = globalThis?.document ? React.useLayoutEffect : () => {};\n\n// use-controllable-state.tsx\n// https://github.com/radix-ui/primitives/blob/main/packages/react/use-controllable-state/src/use-controllable-state.tsx\n\n// Prevent bundlers from trying to optimize the import\nconst useInsertionEffect: typeof useLayoutEffect =\n  (React as never)[\" useInsertionEffect \".trim().toString()] || useLayoutEffect;\n\ntype ChangeHandler<T> = (state: T) => void;\ntype SetStateFn<T> = React.Dispatch<React.SetStateAction<T>>;\n\ninterface UseControllableStateParams<T> {\n  prop?: T | undefined;\n  defaultProp: T;\n  onChange?: ChangeHandler<T>;\n  caller?: string;\n}\n\nexport function useControllableState<T>({\n  prop,\n  defaultProp,\n  onChange = () => {},\n  caller,\n}: UseControllableStateParams<T>): [T, SetStateFn<T>] {\n  const [uncontrolledProp, setUncontrolledProp, onChangeRef] =\n    useUncontrolledState({\n      defaultProp,\n      onChange,\n    });\n  const isControlled = prop !== undefined;\n  const value = isControlled ? prop : uncontrolledProp;\n\n  // Hooks run unconditionally so Hook order never changes between renders;\n  // only the dev-time warning itself is gated on the environment.\n  // (Neon UI patch on the vendored source.)\n  const isControlledRef = React.useRef(prop !== undefined);\n  React.useEffect(() => {\n    if (process.env.NODE_ENV !== \"production\") {\n      const wasControlled = isControlledRef.current;\n      if (wasControlled !== isControlled) {\n        const from = wasControlled ? \"controlled\" : \"uncontrolled\";\n        const to = isControlled ? \"controlled\" : \"uncontrolled\";\n        console.warn(\n          `${caller} is changing from ${from} to ${to}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`\n        );\n      }\n    }\n    isControlledRef.current = isControlled;\n  }, [isControlled, caller]);\n\n  const setValue = React.useCallback<SetStateFn<T>>(\n    (nextValue) => {\n      if (isControlled) {\n        const value = isFunction(nextValue) ? nextValue(prop) : nextValue;\n        if (value !== prop) {\n          onChangeRef.current?.(value);\n        }\n      } else {\n        // Notify the parent from the event handler instead of a useEffect,\n        // per https://react.dev/learn/you-might-not-need-an-effect — saves\n        // the extra render. (Neon UI patch on the vendored source.)\n        const value = isFunction(nextValue)\n          ? nextValue(uncontrolledProp)\n          : nextValue;\n        setUncontrolledProp(value);\n        if (value !== uncontrolledProp) {\n          onChangeRef.current?.(value);\n        }\n      }\n    },\n    [isControlled, prop, setUncontrolledProp, onChangeRef, uncontrolledProp]\n  );\n\n  return [value, setValue];\n}\n\nfunction useUncontrolledState<T>({\n  defaultProp,\n  onChange,\n}: Omit<UseControllableStateParams<T>, \"prop\">): [\n  Value: T,\n  setValue: React.Dispatch<React.SetStateAction<T>>,\n  OnChangeRef: React.RefObject<ChangeHandler<T> | undefined>,\n] {\n  const [value, setValue] = React.useState(defaultProp);\n\n  const onChangeRef = React.useRef(onChange);\n  useInsertionEffect(() => {\n    onChangeRef.current = onChange;\n  }, [onChange]);\n\n  // onChange is fired from setValue in useControllableState rather than from\n  // an effect here, so parents update in the same render pass.\n  // (Neon UI patch on the vendored source.)\n  return [value, setValue, onChangeRef];\n}\n\nfunction isFunction(value: unknown): value is (...args: never[]) => unknown {\n  return typeof value === \"function\";\n}\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:component"
}