{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "stat",
  "title": "stat",
  "description": "A metric read-out: label, figure in tabular numbers (formatter, optional count-up), unit, delta, hint, loading and unavailable states, as a tile, a row, or in a responsive StatGroup.",
  "dependencies": [
    "class-variance-authority@^0.7"
  ],
  "registryDependencies": [
    "@dwellir/dwellir-theme",
    "@dwellir/utils",
    "@dwellir/skeleton",
    "@dwellir/tooltip",
    "@dwellir/rune-icons"
  ],
  "files": [
    {
      "path": "components/ui/stat.tsx",
      "content": "'use client';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { type ComponentProps, type ReactNode, useEffect, useRef, useState } from 'react';\nimport { RuneArrowDown, RuneArrowUp, RuneInfo, RuneMinus } from '@/components/brand/rune-icons';\nimport { Skeleton } from '@/components/ui/skeleton';\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';\nimport { cn } from '@/lib/utils';\n\n/**\n * The colour of a figure. Neutral by default: colour marks a state, so set a\n * tone only when the value warrants it. The five status tiers paint with the\n * status scale's own inks, so an uptime figure matches its uptime bar (pass\n * the tier from uptimeTier in the uptime-bar part). Warning and critical are\n * for dashboard thresholds.\n */\ntype StatTone =\n  | 'neutral'\n  | 'good'\n  | 'warning'\n  | 'critical'\n  | 'muted'\n  | 'operational'\n  | 'minor'\n  | 'major'\n  | 'down'\n  | 'no-data';\n\nconst TONE_CLASS: Record<StatTone, string> = {\n  neutral: 'text-foreground',\n  good: 'text-status-operational-ink',\n  warning: 'text-warning-ink',\n  critical: 'text-destructive',\n  muted: 'text-muted-foreground',\n  operational: 'text-status-operational-ink',\n  minor: 'text-status-minor-ink',\n  major: 'text-status-major-ink',\n  down: 'text-status-down-ink',\n  'no-data': 'text-status-no-data-ink',\n};\n\ntype StatLayout = 'stacked' | 'inline' | 'auto';\n\nconst statVariants = cva('group/stat flex min-w-0', {\n  variants: {\n    layout: {\n      /* A tile: label over value, meta under it. */\n      stacked: 'flex-col gap-1',\n      /* A row in a list: label left, value right, meta under both. */\n      inline: 'flex-wrap items-baseline gap-x-4 gap-y-1',\n      /*\n       * Inline in a narrow container, stacked from the md container width.\n       * Needs a container ancestor: StatGroup is one.\n       */\n      auto: 'flex-wrap items-baseline gap-x-4 gap-y-1 @md:flex-col @md:flex-nowrap @md:items-stretch @md:gap-1',\n    },\n    align: {\n      start: '',\n      center: '',\n    },\n  },\n  compoundVariants: [\n    { layout: 'stacked', align: 'center', className: 'items-center text-center' },\n    { layout: 'auto', align: 'center', className: '@md:items-center @md:text-center' },\n  ],\n  defaultVariants: { layout: 'stacked', align: 'start' },\n});\n\n/* Where each piece sits per layout. Auto is inline below the md container width. */\nconst LABEL_LAYOUT: Record<StatLayout, string> = {\n  stacked: '',\n  inline: 'order-1',\n  auto: 'order-1 @md:order-0',\n};\nconst VALUE_LAYOUT: Record<StatLayout, string> = {\n  stacked: '',\n  inline: 'order-2 ml-auto',\n  auto: 'order-2 ml-auto @md:order-0 @md:ml-0',\n};\nconst META_LAYOUT: Record<StatLayout, string> = {\n  stacked: '',\n  inline: 'order-3 w-full',\n  auto: 'order-3 w-full @md:order-0 @md:w-auto',\n};\n\nconst statValueVariants = cva('wrap-anywhere font-bold tabular-nums', {\n  variants: {\n    size: {\n      sm: 'text-lg leading-tight',\n      md: 'text-xl leading-tight',\n      lg: 'font-display text-4xl leading-none tracking-tight',\n    },\n  },\n  defaultVariants: { size: 'md' },\n});\n\nconst statUnitVariants = cva('font-normal text-muted-foreground', {\n  variants: {\n    size: { sm: 'text-xs', md: 'text-sm', lg: 'text-base' },\n  },\n  defaultVariants: { size: 'md' },\n});\n\n/* A loading figure keeps the height of the figure it stands in for. */\nconst SKELETON_CLASS = { sm: 'h-6 w-16', md: 'h-7 w-20', lg: 'h-9 w-28' } as const;\n\nconst numberFormat = new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 });\n\nfunction prefersReducedMotion() {\n  return (\n    typeof window !== 'undefined' &&\n    typeof window.matchMedia === 'function' &&\n    window.matchMedia('(prefers-reduced-motion: reduce)').matches\n  );\n}\n\n/*\n * Counts from the figure on screen to the next one, ease-out over `duration`\n * seconds. The first paint already shows the final figure, so the count runs\n * only on a later change: it never jumps back to zero. Reduced motion and\n * animate=false jump straight to the value.\n */\nfunction useAnimatedValue(value: number, animate: boolean, duration: number) {\n  const [display, setDisplay] = useState(value);\n  const shown = useRef(value);\n  const first = useRef(true);\n  useEffect(() => {\n    const mount = first.current;\n    first.current = false;\n    const from = shown.current;\n    if (mount) return;\n    if (!animate || duration <= 0 || from === value || prefersReducedMotion()) {\n      shown.current = value;\n      setDisplay(value);\n      return;\n    }\n    let frame = 0;\n    const start = performance.now();\n    const tick = (now: number) => {\n      const step = Math.min(1, (now - start) / (duration * 1000));\n      const next = from + (value - from) * (1 - (1 - step) ** 3);\n      shown.current = next;\n      setDisplay(next);\n      if (step < 1) frame = requestAnimationFrame(tick);\n    };\n    frame = requestAnimationFrame(tick);\n    return () => cancelAnimationFrame(frame);\n  }, [value, animate, duration]);\n  return display;\n}\n\ntype StatProps = Omit<ComponentProps<'div'>, 'children'> &\n  VariantProps<typeof statVariants> &\n  VariantProps<typeof statValueVariants> & {\n    /** What the figure measures, such as \"Average RPS\". A string also names the hint button. */\n    label: ReactNode;\n    /** A number to format (and count up, with animate), or a ready-made string such as \"99.98%\". Null reads as unavailable. */\n    value?: number | string | null;\n    /** Formats a numeric value, also mid-count. Defaults to a grouped number with up to 2 decimals. */\n    format?: (value: number) => string;\n    /** A quiet suffix after the figure, such as \"req/s\" or \"%\". */\n    unit?: ReactNode;\n    /** Change since the last period. Use StatDelta, or any node. */\n    delta?: ReactNode;\n    /** A quiet line under the figure, such as \"of 5M included\". */\n    description?: ReactNode;\n    /** A short explanation behind an info button next to the label. Never the only copy of important text. */\n    hint?: ReactNode;\n    /** Colour marks a state: keep neutral unless the value warrants a tone. */\n    tone?: StatTone;\n    /** Shows a skeleton in the figure's place and marks the stat busy. */\n    loading?: boolean;\n    /** What screen readers hear instead of the dash when there is no value. */\n    unavailableLabel?: string;\n    /** Counts a numeric value up as it changes. Off by default; reduced motion always skips it. */\n    animate?: boolean;\n    /** The count-up's length in seconds. */\n    duration?: number;\n    /** Under the meta row: a progress bar, an uptime bar, and the like. */\n    children?: ReactNode;\n  };\n\nfunction StatHint({ label, hint }: { label: ReactNode; hint: ReactNode }) {\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>\n        <button\n          type=\"button\"\n          data-slot=\"stat-hint\"\n          aria-label={typeof label === 'string' ? `About ${label}` : 'More information'}\n          /* A 24px hit box (46px on a coarse pointer); vertical margins keep the label height.\n             It paints above the figure below it, which its coarse-pointer box overlaps. */\n          className=\"relative z-1 -my-1.25 rounded-full p-1.25 text-muted-foreground outline-none focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-solid pointer-coarse:-my-4 pointer-coarse:p-4\"\n        >\n          <RuneInfo data-slot=\"stat-hint-icon\" aria-hidden=\"true\" className=\"size-3.5\" />\n        </button>\n      </TooltipTrigger>\n      <TooltipContent>{hint}</TooltipContent>\n    </Tooltip>\n  );\n}\n\nfunction wrapStatValue(text: ReactNode) {\n  if (typeof text !== 'string') return text;\n  return text\n    .split(/(?<=[,.\\u00a0\\u202f])/u)\n    .flatMap((part, index) => [index > 0 && <wbr data-slot=\"stat-break\" key={index} />, part]);\n}\n\n/* The figure itself: a skeleton, a dash, or the (perhaps counting) value and its unit. */\nfunction StatFigure({\n  value,\n  format,\n  unit,\n  tone,\n  size,\n  loading,\n  unavailableLabel,\n  animate,\n  duration,\n}: Required<Pick<StatProps, 'duration'>> &\n  Pick<\n    StatProps,\n    'value' | 'format' | 'unit' | 'tone' | 'loading' | 'unavailableLabel' | 'animate'\n  > &\n  VariantProps<typeof statValueVariants>) {\n  const numeric = typeof value === 'number' && Number.isFinite(value) ? value : null;\n  const counting = Boolean(animate && numeric !== null && !loading);\n  const shown = useAnimatedValue(numeric ?? 0, counting, duration);\n  const formatValue = format ?? numberFormat.format.bind(numberFormat);\n  const classes = cn(statValueVariants({ size }), TONE_CLASS[tone ?? 'neutral']);\n  const unavailableColor = tone === 'no-data' ? undefined : 'text-muted-foreground';\n  if (loading) {\n    return <Skeleton data-slot=\"stat-skeleton\" className={SKELETON_CLASS[size ?? 'md']} />;\n  }\n  if (numeric === null && typeof value !== 'string') {\n    return (\n      <span data-slot=\"stat-value\" className={cn(classes, unavailableColor)}>\n        <span data-slot=\"stat-unavailable-mark\" aria-hidden=\"true\">\n          -\n        </span>\n        <span data-slot=\"stat-unavailable-label\" className=\"sr-only\">\n          {unavailableLabel}\n        </span>\n      </span>\n    );\n  }\n  const finalText = numeric !== null ? formatValue(numeric) : value;\n  const shownText = counting ? formatValue(shown) : finalText;\n  return (\n    <span data-slot=\"stat-value\" className={classes}>\n      {counting ? (\n        <>\n          {/* Screen readers hear the final figure once, not every frame of the count. */}\n          <span data-slot=\"stat-animated-value\" aria-hidden=\"true\">\n            {wrapStatValue(shownText)}\n          </span>\n          <span data-slot=\"stat-final-value\" className=\"sr-only\">\n            {finalText}\n          </span>\n        </>\n      ) : (\n        wrapStatValue(finalText)\n      )}\n      {unit ? (\n        <span data-slot=\"stat-unit\" className={cn(statUnitVariants({ size }), 'ml-1')}>\n          {unit}\n        </span>\n      ) : null}\n    </span>\n  );\n}\n\n/**\n * One metric read-out: a label, a figure in tabular numbers, and quiet meta\n * (a delta, a description, children such as a progress bar). The same part\n * reads as a tile, as a row in a list, and, with layout=\"auto\" inside a\n * StatGroup, as a row on a phone and a tile on a desktop. It carries no\n * surface: put it on a Card, as the dashboard's stat cards do.\n */\nfunction Stat({\n  className,\n  layout,\n  align,\n  size,\n  label,\n  value,\n  format,\n  unit,\n  delta,\n  description,\n  hint,\n  tone = 'neutral',\n  loading = false,\n  unavailableLabel = 'Not available',\n  animate = false,\n  duration = 1,\n  children,\n  ...props\n}: StatProps) {\n  const kind = layout ?? 'stacked';\n  return (\n    <div\n      data-slot=\"stat\"\n      data-layout={kind}\n      data-size={size ?? 'md'}\n      data-tone={tone}\n      aria-busy={loading || undefined}\n      className={cn(statVariants({ layout, align }), className)}\n      {...props}\n    >\n      <span\n        data-slot=\"stat-label\"\n        className={cn(\n          'flex items-center gap-0.5 text-sm text-muted-foreground',\n          LABEL_LAYOUT[kind],\n        )}\n      >\n        {label}\n        {hint ? <StatHint label={label} hint={hint} /> : null}\n      </span>\n      <span data-slot=\"stat-figure\" className={cn('min-w-0 max-w-full', VALUE_LAYOUT[kind])}>\n        <StatFigure\n          value={value}\n          format={format}\n          unit={unit}\n          tone={tone}\n          size={size}\n          loading={loading}\n          unavailableLabel={unavailableLabel}\n          animate={animate}\n          duration={duration}\n        />\n      </span>\n      {delta || description || children ? (\n        <div data-slot=\"stat-meta\" className={cn('flex flex-col gap-2', META_LAYOUT[kind])}>\n          {delta || description ? (\n            <span data-slot=\"stat-details\" className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n              {delta}\n              {description ? (\n                <span data-slot=\"stat-description\" className=\"text-sm text-muted-foreground\">\n                  {description}\n                </span>\n              ) : null}\n            </span>\n          ) : null}\n          {children}\n        </div>\n      ) : null}\n      {loading ? (\n        <span data-slot=\"stat-loading-label\" role=\"status\" className=\"sr-only\">\n          {typeof label === 'string' ? `Loading ${label}` : 'Loading'}\n        </span>\n      ) : null}\n    </div>\n  );\n}\n\nconst statDeltaVariants = cva('inline-flex items-center gap-0.5 text-xs font-bold tabular-nums', {\n  variants: {\n    tone: {\n      neutral: 'text-muted-foreground',\n      good: 'text-status-operational-ink',\n      warning: 'text-warning-ink',\n      critical: 'text-destructive',\n    },\n  },\n  defaultVariants: { tone: 'neutral' },\n});\n\nconst DELTA_ICON = { up: RuneArrowUp, down: RuneArrowDown, flat: RuneMinus } as const;\n\ntype StatDeltaProps = ComponentProps<'span'> &\n  VariantProps<typeof statDeltaVariants> & {\n    /** The arrow's way. Direction is not judgement: an up arrow can be bad news, so pick the tone apart from it. */\n    direction?: keyof typeof DELTA_ICON;\n  };\n\n/* A stat's change since the last period: an arrow and the change, such as \"+4.2%\". */\nfunction StatDelta({ className, tone, direction = 'flat', children, ...props }: StatDeltaProps) {\n  const Icon = DELTA_ICON[direction];\n  return (\n    <span\n      data-slot=\"stat-delta\"\n      data-direction={direction}\n      data-tone={tone ?? 'neutral'}\n      className={cn(statDeltaVariants({ tone }), className)}\n      {...props}\n    >\n      <Icon data-slot=\"stat-delta-icon\" aria-hidden=\"true\" className=\"size-3.5\" />\n      {children}\n    </span>\n  );\n}\n\n/* The grid inside the group: a size query matches an ancestor container, never its own element. */\nconst statGroupVariants = cva('w-full min-w-0', {\n  variants: {\n    layout: {\n      /* Tiles: a compact list in a narrow container, a row of columns in a wide one. */\n      grid: 'grid grid-cols-1 gap-x-6 gap-y-4',\n      /* Rows in a card, as in a usage summary: always one column. */\n      list: 'flex flex-col gap-4',\n    },\n    columns: {\n      auto: '',\n      '2': '',\n      '3': '',\n      '4': '',\n      '5': '',\n      '6': '',\n    },\n  },\n  compoundVariants: [\n    {\n      layout: 'grid',\n      columns: 'auto',\n      className: '@md:grid-cols-2 @3xl:grid-cols-3 @5xl:grid-cols-4',\n    },\n    { layout: 'grid', columns: '2', className: '@md:grid-cols-2' },\n    { layout: 'grid', columns: '3', className: '@md:grid-cols-2 @4xl:grid-cols-3' },\n    { layout: 'grid', columns: '4', className: '@md:grid-cols-2 @4xl:grid-cols-4' },\n    {\n      layout: 'grid',\n      columns: '5',\n      className: '@md:grid-cols-2 @3xl:grid-cols-3 @5xl:grid-cols-5',\n    },\n    {\n      layout: 'grid',\n      columns: '6',\n      className: '@md:grid-cols-2 @3xl:grid-cols-3 @5xl:grid-cols-6',\n    },\n  ],\n  defaultVariants: { layout: 'grid', columns: 'auto' },\n});\n\nconst GROUP_COLUMNS = { auto: 'auto', 2: '2', 3: '3', 4: '4', 5: '5', 6: '6' } as const;\n\ntype StatGroupProps = ComponentProps<'div'> &\n  Omit<VariantProps<typeof statGroupVariants>, 'columns'> & {\n    /** Columns in a wide container (layout=\"grid\"): a count, or auto for up to four. */\n    columns?: 2 | 3 | 4 | 5 | 6 | 'auto';\n  };\n\n/**\n * Several stats in one responsive group. The outer element is the container\n * and takes the caller's className, so the measured width is the group's own\n * (a card, a rail, a dialog), not the viewport's or an ancestor's. The grid\n * inside answers to it: a compact list in a narrow container, a row in a\n * wide one. Stats with layout=\"auto\" switch with it. Name the group with\n * aria-label, or head it with a heading outside the part.\n */\nfunction StatGroup({ className, layout, columns, children, ...props }: StatGroupProps) {\n  return (\n    // biome-ignore lint/a11y/useSemanticElements: readings form a named group, not a form fieldset.\n    <div\n      data-slot=\"stat-group\"\n      data-layout={layout ?? 'grid'}\n      role=\"group\"\n      className={cn('@container min-w-0', className)}\n      {...props}\n    >\n      <div\n        data-slot=\"stat-group-grid\"\n        className={statGroupVariants({\n          layout,\n          columns: columns === undefined ? undefined : GROUP_COLUMNS[columns],\n        })}\n      >\n        {children}\n      </div>\n    </div>\n  );\n}\n\nexport {\n  Stat,\n  StatDelta,\n  type StatDeltaProps,\n  StatGroup,\n  type StatGroupProps,\n  type StatProps,\n  type StatTone,\n  statDeltaVariants,\n  statGroupVariants,\n  statVariants,\n};\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}