{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "allocation-card",
  "title": "Allocation Card",
  "description": "A share-of-total card with a period switcher, a big percentage with delta, a tick bar, and a footer stat with an avatar stack.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "card",
    "tabs",
    "tooltip"
  ],
  "files": [
    {
      "path": "registry/dashboardcn/blocks/allocation-card.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Info } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { type NumberFormat } from \"@/registry/dashboardcn/lib/format\"\nimport {\n  Card,\n  CardAction,\n  CardContent,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport { DeltaBadge } from \"@/registry/dashboardcn/ui/delta-badge\"\nimport { PeriodTabs, type PeriodOption } from \"@/registry/dashboardcn/ui/period-tabs\"\nimport { TickBar } from \"@/registry/dashboardcn/ui/tick-bar\"\nimport { MetricValue } from \"@/registry/dashboardcn/ui/metric-value\"\n\nexport interface AllocationCardPerson {\n  name: string\n  /** Avatar image. Falls back to the first letter of the name. */\n  src?: string\n}\n\nexport interface AllocationCardProps extends React.ComponentProps<typeof Card> {\n  title: string\n  /** Shown in a tooltip behind an info icon next to the title. */\n  description?: string\n  /** Share of the whole, e.g. 0.46 for 46%. */\n  value: number\n  format?: NumberFormat\n  currency?: string\n  /** Bar fill from 0 to 100. Defaults to value when format is percent. */\n  progress?: number\n  /** Fractional change vs the prior period, e.g. 0.034 for +3.4%. */\n  delta?: number\n  /** Text after the delta, e.g. \"vs prior period\". */\n  deltaLabel?: string\n  /** Treat a decrease as good. */\n  invertDelta?: boolean\n  /** Any CSS color for the bar. */\n  color?: string\n  /** Number of ticks in the bar. */\n  segments?: number\n  /** Period options for the switcher. Omit to hide it. */\n  periods?: PeriodOption[]\n  period?: string\n  defaultPeriod?: string\n  onPeriodChange?: (period: string) => void\n  /** Footer stat, e.g. { label: \"Equities exposure\", value: \"$4.7M\" }. */\n  stat?: { label: string; value: React.ReactNode }\n  /** Avatars shown in the footer. */\n  people?: AllocationCardPerson[]\n  /** Text next to the avatars, e.g. \"6 members\". */\n  peopleLabel?: string\n}\n\n/** A share-of-total card with a period switcher, a big percentage with delta, a tick bar, and a footer stat with an avatar stack. */\nfunction AllocationCard({\n  title,\n  description,\n  value,\n  format = \"percent\",\n  currency,\n  progress,\n  delta,\n  deltaLabel,\n  invertDelta,\n  color = \"var(--color-emerald-500)\",\n  segments = 40,\n  periods,\n  period,\n  defaultPeriod,\n  onPeriodChange,\n  stat,\n  people = [],\n  peopleLabel,\n  className,\n  ...props\n}: AllocationCardProps) {\n  const fill = progress ?? (format === \"percent\" ? value * 100 : value)\n\n  return (\n    <Card data-slot=\"allocation-card\" className={cn(\"@container/card gap-4\", className)} {...props}>\n      <CardHeader className=\"@max-sm/card:[grid-template-columns:minmax(0,1fr)]!\">\n        <CardTitle className=\"flex items-center gap-1.5 text-sm\">\n          <span className=\"truncate\">{title}</span>\n          {description ? (\n            <TooltipProvider>\n              <Tooltip>\n                <TooltipTrigger asChild>\n                  <button\n                    type=\"button\"\n                    className=\"text-muted-foreground hover:text-foreground inline-flex shrink-0 rounded-full\"\n                  >\n                    <Info className=\"size-3.5\" />\n                    <span className=\"sr-only\">About {title}</span>\n                  </button>\n                </TooltipTrigger>\n                <TooltipContent className=\"max-w-56\">{description}</TooltipContent>\n              </Tooltip>\n            </TooltipProvider>\n          ) : null}\n        </CardTitle>\n        {periods ? (\n          <CardAction className=\"@max-sm/card:[grid-area:auto]! @max-sm/card:justify-self-start!\">\n            <PeriodTabs\n              options={periods}\n              value={period}\n              defaultValue={defaultPeriod ?? periods[0]?.value}\n              onValueChange={onPeriodChange}\n            />\n          </CardAction>\n        ) : null}\n      </CardHeader>\n      <CardContent className=\"flex flex-col gap-4\">\n        <div className=\"flex flex-wrap items-baseline gap-x-2 gap-y-1\">\n          <MetricValue\n            value={value}\n            format={format}\n            currency={currency}\n            maximumFractionDigits={0}\n            className=\"text-4xl font-semibold tracking-tight\"\n          />\n          {delta !== undefined ? (\n            <DeltaBadge delta={delta} invert={invertDelta} variant=\"text\" showIcon={false} className=\"text-sm\" />\n          ) : null}\n          {deltaLabel ? (\n            <span className=\"text-muted-foreground text-sm\">{deltaLabel}</span>\n          ) : null}\n        </div>\n        <TickBar value={fill} segments={segments} color={color} />\n      </CardContent>\n      {stat || people.length || peopleLabel ? (\n        <CardFooter className=\"justify-between gap-4 text-sm @max-sm/card:flex-col @max-sm/card:items-start @max-sm/card:gap-2\">\n          {stat ? (\n            <span className=\"text-muted-foreground min-w-0 truncate\">\n              {stat.label}:{\" \"}\n              <span className=\"text-foreground font-semibold tabular-nums\">{stat.value}</span>\n            </span>\n          ) : (\n            <span />\n          )}\n          {people.length || peopleLabel ? (\n            <div className=\"flex shrink-0 items-center gap-2\">\n              {people.length ? (\n                <div\n                  className=\"flex -space-x-2\"\n                  aria-label={people.map((p) => p.name).join(\", \")}\n                >\n                  {people.map((person) => (\n                    <span\n                      key={person.name}\n                      title={person.name}\n                      className=\"bg-muted ring-card text-muted-foreground flex size-7 items-center justify-center overflow-hidden rounded-full text-xs font-medium ring-2\"\n                    >\n                      {person.src ? (\n                        // eslint-disable-next-line @next/next/no-img-element\n                        <img\n                          src={person.src}\n                          alt={person.name}\n                          className=\"size-full object-cover\"\n                        />\n                      ) : (\n                        person.name.charAt(0).toUpperCase()\n                      )}\n                    </span>\n                  ))}\n                </div>\n              ) : null}\n              {peopleLabel ? (\n                <span className=\"text-muted-foreground whitespace-nowrap\">{peopleLabel}</span>\n              ) : null}\n            </div>\n          ) : null}\n        </CardFooter>\n      ) : null}\n    </Card>\n  )\n}\n\nexport { AllocationCard }\n",
      "type": "registry:component"
    },
    {
      "path": "registry/dashboardcn/ui/tick-bar.tsx",
      "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface TickBarProps extends React.ComponentProps<\"div\"> {\n  /** Current value, from 0 to max. */\n  value: number\n  max?: number\n  /** Number of ticks. */\n  segments?: number\n  /** Any CSS color for the filled ticks. Defaults to chart-1. */\n  color?: string\n  /** Any CSS color for the unfilled ticks. Defaults to muted. */\n  trackColor?: string\n  /** \"tick\" draws thin bars; \"pill\" draws wide rounded ones. */\n  shape?: \"tick\" | \"pill\"\n  /** Reveal filled ticks in sequence when the bar mounts. */\n  animate?: boolean\n  /** Total reveal time in milliseconds. */\n  animationDuration?: number\n}\n\n/** A progress bar drawn as a row of ticks, lit up to the current value. */\nfunction TickBar({\n  value,\n  max = 100,\n  segments = 40,\n  color = \"var(--chart-1)\",\n  trackColor,\n  shape = \"tick\",\n  animate = false,\n  animationDuration = 700,\n  className,\n  ...props\n}: TickBarProps) {\n  const fraction = Math.min(1, Math.max(0, value / (max || 1)))\n  // Any non-zero value lights at least one tick so small values still register.\n  const filled = fraction === 0 ? 0 : Math.max(1, Math.round(fraction * segments))\n\n  return (\n    <div\n      data-slot=\"tick-bar\"\n      data-shape={shape}\n      role=\"progressbar\"\n      aria-valuemin={0}\n      aria-valuemax={max}\n      aria-valuenow={value}\n      className={cn(\n        \"flex w-full\",\n        shape === \"tick\" ? \"h-6 gap-[3px]\" : \"h-4 gap-1.5\",\n        className\n      )}\n      {...props}\n    >\n      {Array.from({ length: segments }, (_, index) => {\n        const lit = index < filled\n        return (\n          <span\n            key={index}\n            data-filled={lit}\n            className={cn(\n              \"bg-muted relative h-full min-w-0 flex-1 overflow-hidden transition-colors\",\n              shape === \"tick\" ? \"rounded-[1.5px]\" : \"rounded-full\"\n            )}\n            style={{ backgroundColor: trackColor }}\n          >\n            {lit ? (\n              <span\n                className={cn(\n                  \"absolute inset-0\",\n                  shape === \"tick\" ? \"rounded-[1.5px]\" : \"rounded-full\",\n                  animate && \"motion-safe:animate-in motion-safe:fade-in\"\n                )}\n                style={{\n                  backgroundColor: color,\n                  ...(animate\n                    ? {\n                        animationDelay: `${(index / Math.max(1, filled)) * animationDuration * 0.8}ms`,\n                        animationDuration: `${Math.max(120, animationDuration * 0.25)}ms`,\n                        animationFillMode: \"both\",\n                      }\n                    : null),\n                }}\n              />\n            ) : null}\n          </span>\n        )\n      })}\n    </div>\n  )\n}\n\nexport { TickBar }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/dashboardcn/ui/delta-badge.tsx",
      "content": "import * as React from \"react\"\nimport { Minus, TrendingDown, TrendingUp } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { formatDelta } from \"@/registry/dashboardcn/lib/format\"\n\nexport type DeltaDirection = \"up\" | \"down\" | \"flat\"\n\nexport function getDeltaDirection(delta: number | undefined): DeltaDirection {\n  if (delta === undefined || delta === 0 || !Number.isFinite(delta)) return \"flat\"\n  return delta > 0 ? \"up\" : \"down\"\n}\n\nconst directionIcon: Record<DeltaDirection, React.ElementType> = {\n  up: TrendingUp,\n  down: TrendingDown,\n  flat: Minus,\n}\n\nexport interface DeltaBadgeProps extends React.ComponentProps<\"span\"> {\n  /** Fractional change, e.g. 0.124 for +12.4%. */\n  delta: number\n  /** Treat a decrease as good and an increase as bad (churn, latency, errors). */\n  invert?: boolean\n  variant?: \"outline\" | \"soft\" | \"text\"\n  showIcon?: boolean\n}\n\nfunction DeltaBadge({\n  delta,\n  invert = false,\n  variant = \"outline\",\n  showIcon = true,\n  className,\n  children,\n  ...props\n}: DeltaBadgeProps) {\n  const direction = getDeltaDirection(delta)\n  const positive = direction === \"flat\" ? null : (direction === \"up\") !== invert\n  const Icon = directionIcon[direction]\n\n  return (\n    <span\n      data-slot=\"delta-badge\"\n      data-direction={direction}\n      data-positive={positive}\n      className={cn(\n        \"inline-flex items-center gap-1 text-xs font-medium tabular-nums\",\n        variant === \"outline\" && \"rounded-md border px-1.5 py-0.5\",\n        variant === \"soft\" && \"rounded-md px-1.5 py-0.5\",\n        variant === \"soft\" && positive === true && \"bg-emerald-500/10\",\n        variant === \"soft\" && positive === false && \"bg-red-500/10\",\n        variant === \"soft\" && positive === null && \"bg-muted\",\n        positive === true && \"text-emerald-600 dark:text-emerald-400\",\n        positive === false && \"text-red-600 dark:text-red-400\",\n        positive === null && \"text-muted-foreground\",\n        className\n      )}\n      {...props}\n    >\n      {showIcon ? <Icon className=\"size-3\" aria-hidden=\"true\" /> : null}\n      {children ?? formatDelta(delta)}\n    </span>\n  )\n}\n\nexport { DeltaBadge }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/dashboardcn/ui/period-tabs.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Tabs, TabsList, TabsTrigger } from \"@/components/ui/tabs\"\n\nexport interface PeriodOption {\n  value: string\n  label: string\n}\n\nexport const defaultPeriods: PeriodOption[] = [\n  { value: \"week\", label: \"Week\" },\n  { value: \"month\", label: \"Month\" },\n  { value: \"year\", label: \"Year\" },\n]\n\nexport interface PeriodTabsProps\n  extends Omit<React.ComponentProps<typeof Tabs>, \"children\"> {\n  options?: PeriodOption[]\n  size?: \"sm\" | \"default\"\n  /** Slide a single pill between tabs instead of swapping backgrounds. */\n  animated?: boolean\n}\n\ninterface IndicatorRect {\n  left: number\n  top: number\n  width: number\n  height: number\n}\n\n/** Small segmented control for switching a chart's time range. */\nfunction PeriodTabs({\n  options = defaultPeriods,\n  size = \"sm\",\n  animated = false,\n  className,\n  value,\n  defaultValue,\n  onValueChange,\n  ...props\n}: PeriodTabsProps) {\n  const listRef = React.useRef<HTMLDivElement>(null)\n  const [internalValue, setInternalValue] = React.useState(\n    defaultValue ?? options[0]?.value\n  )\n  const active = value ?? internalValue\n  const [rect, setRect] = React.useState<IndicatorRect | null>(null)\n\n  const measure = React.useCallback(() => {\n    const list = listRef.current\n    if (!list) return\n    const trigger = list.querySelector<HTMLElement>('[data-state=\"active\"]')\n    if (!trigger) return\n    setRect({\n      left: trigger.offsetLeft,\n      top: trigger.offsetTop,\n      width: trigger.offsetWidth,\n      height: trigger.offsetHeight,\n    })\n  }, [])\n\n  React.useLayoutEffect(() => {\n    if (!animated) return\n    measure()\n  }, [animated, active, options, size, measure])\n\n  React.useEffect(() => {\n    if (!animated || !listRef.current) return\n    const observer = new ResizeObserver(measure)\n    observer.observe(listRef.current)\n    return () => observer.disconnect()\n  }, [animated, measure])\n\n  return (\n    <Tabs\n      data-slot=\"period-tabs\"\n      className={cn(\"w-fit\", className)}\n      value={value}\n      defaultValue={defaultValue}\n      onValueChange={(next) => {\n        setInternalValue(next)\n        onValueChange?.(next)\n      }}\n      {...props}\n    >\n      <TabsList\n        ref={listRef}\n        className={cn(\n          \"relative\",\n          size === \"sm\" && \"group-data-[orientation=horizontal]/tabs:h-8 p-1\"\n        )}\n      >\n        {animated && rect ? (\n          <span\n            aria-hidden\n            data-slot=\"period-tabs-indicator\"\n            className=\"pointer-events-none absolute rounded-md bg-background shadow-sm transition-[left,top,width,height] duration-300 ease-[cubic-bezier(0.25,1,0.5,1)] motion-reduce:transition-none dark:border dark:border-input dark:bg-input/30\"\n            style={rect}\n          />\n        ) : null}\n        {options.map((option) => (\n          <TabsTrigger\n            key={option.value}\n            value={option.value}\n            className={cn(\n              size === \"sm\" && \"h-6 px-2.5 text-xs\",\n              animated &&\n                \"relative z-10 data-[state=active]:bg-transparent group-data-[variant=default]/tabs-list:data-[state=active]:shadow-none dark:data-[state=active]:border-transparent dark:data-[state=active]:bg-transparent\"\n            )}\n          >\n            {option.label}\n          </TabsTrigger>\n        ))}\n      </TabsList>\n    </Tabs>\n  )\n}\n\nexport { PeriodTabs }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/dashboardcn/ui/metric-value.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { formatNumber, type NumberFormat } from \"@/registry/dashboardcn/lib/format\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\n\n/** Values at or above this are abbreviated by default. */\nexport const DEFAULT_COMPACT_FROM = 100_000\n\nexport interface MetricValueProps extends Omit<React.ComponentProps<\"span\">, \"children\"> {\n  /** Strings render as-is. */\n  value: number | string\n  format?: NumberFormat\n  currency?: string\n  maximumFractionDigits?: number\n  /** Values at or above this are abbreviated (e.g. $158K) with the full value in a tooltip. Set to Infinity to always show the full value. */\n  compactFrom?: number\n  /** Text after the number, e.g. a unit. */\n  suffix?: React.ReactNode\n}\n\n/** A formatted number. Large values are abbreviated and reveal the full value on hover. */\nfunction MetricValue({\n  value,\n  format,\n  currency,\n  maximumFractionDigits,\n  compactFrom = DEFAULT_COMPACT_FROM,\n  suffix,\n  className,\n  ...props\n}: MetricValueProps) {\n  const classes = cn(\"tabular-nums\", className)\n\n  if (typeof value !== \"number\") {\n    return (\n      <span data-slot=\"metric-value\" className={classes} {...props}>\n        {value}\n        {suffix}\n      </span>\n    )\n  }\n\n  const full = formatNumber(value, { format, currency, maximumFractionDigits })\n  const abbreviate = format !== \"percent\" && Math.abs(value) >= compactFrom\n  if (!abbreviate) {\n    return (\n      <span data-slot=\"metric-value\" className={classes} {...props}>\n        {full}\n        {suffix}\n      </span>\n    )\n  }\n\n  return (\n    <TooltipProvider>\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <span\n            data-slot=\"metric-value\"\n            data-compact=\"\"\n            className={cn(classes, \"cursor-default\")}\n            {...props}\n          >\n            {formatNumber(value, { format, currency, compact: true })}\n            {suffix}\n          </span>\n        </TooltipTrigger>\n        <TooltipContent className=\"tabular-nums\">\n          {full}\n          {suffix}\n        </TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  )\n}\n\nexport { MetricValue }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/dashboardcn/lib/format.ts",
      "content": "export type NumberFormat = \"number\" | \"compact\" | \"currency\" | \"percent\"\n\nexport interface FormatNumberOptions {\n  format?: NumberFormat\n  /** ISO 4217 code, used when format is \"currency\". Defaults to USD. */\n  currency?: string\n  locale?: string\n  maximumFractionDigits?: number\n  /** Abbreviate large values, e.g. \"$158K\" instead of \"$158,143\". */\n  compact?: boolean\n}\n\nconst formatterCache = new Map<string, Intl.NumberFormat>()\n\nfunction getFormatter(locale: string, options: Intl.NumberFormatOptions) {\n  const key = locale + JSON.stringify(options)\n  let formatter = formatterCache.get(key)\n  if (!formatter) {\n    formatter = new Intl.NumberFormat(locale, options)\n    formatterCache.set(key, formatter)\n  }\n  return formatter\n}\n\n/**\n * Format a metric value for display.\n *\n * formatNumber(1234567)                          -> \"1,234,567\"\n * formatNumber(1234567, { format: \"compact\" })   -> \"1.2M\"\n * formatNumber(48.2, { format: \"currency\" })     -> \"$48.20\"\n * formatNumber(158143, { format: \"currency\", compact: true }) -> \"$158K\"\n * formatNumber(0.124, { format: \"percent\" })     -> \"12.4%\"\n */\nexport function formatNumber(\n  value: number,\n  {\n    format = \"number\",\n    currency = \"USD\",\n    locale = \"en-US\",\n    maximumFractionDigits,\n    compact = false,\n  }: FormatNumberOptions = {}\n): string {\n  if (!Number.isFinite(value)) return \"—\"\n\n  // Compact values keep three significant digits: $158K, $1.23M, 41.2K.\n  const compactOptions: Intl.NumberFormatOptions = compact\n    ? maximumFractionDigits === undefined\n      ? { notation: \"compact\", maximumSignificantDigits: 3 }\n      : { notation: \"compact\", maximumFractionDigits }\n    : {}\n  switch (format) {\n    case \"compact\":\n      return getFormatter(locale, {\n        notation: \"compact\",\n        maximumFractionDigits: maximumFractionDigits ?? 1,\n      }).format(value)\n    case \"currency\":\n      return getFormatter(locale, {\n        style: \"currency\",\n        currency,\n        maximumFractionDigits: maximumFractionDigits ?? 2,\n        ...compactOptions,\n      }).format(value)\n    case \"percent\":\n      return getFormatter(locale, {\n        style: \"percent\",\n        maximumFractionDigits: maximumFractionDigits ?? 1,\n      }).format(value)\n    default:\n      return getFormatter(locale, {\n        maximumFractionDigits: maximumFractionDigits ?? 0,\n        ...compactOptions,\n      }).format(value)\n  }\n}\n\n/**\n * Format a fractional change as a signed percentage.\n *\n * formatDelta(0.124)  -> \"+12.4%\"\n * formatDelta(-0.03)  -> \"-3.0%\"\n * formatDelta(0)      -> \"0.0%\"\n */\nexport function formatDelta(delta: number, locale = \"en-US\"): string {\n  if (!Number.isFinite(delta)) return \"—\"\n  return getFormatter(locale, {\n    style: \"percent\",\n    signDisplay: \"exceptZero\",\n    maximumFractionDigits: 1,\n    minimumFractionDigits: 1,\n  }).format(delta)\n}\n\n/**\n * Fractional change between two values. Returns 0 when there is no baseline.\n */\nexport function computeDelta(current: number, previous: number): number {\n  if (!previous) return 0\n  return (current - previous) / Math.abs(previous)\n}\n",
      "type": "registry:lib"
    }
  ],
  "categories": [
    "dashboard",
    "finance"
  ],
  "type": "registry:block"
}