{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "indicator",
  "title": "Indicator",
  "description": "A small count, dot, ring or word pinned to the corner of what it describes, gone at zero and spelled out for screen readers.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/indicator.tsx",
      "content": "import type * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { SPRING_DEFAULT } from \"@/registry/miseui/lib/motion\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype IndicatorVariant = \"dot\" | \"count\" | \"ring\" | \"label\"\ntype IndicatorEffect = \"pop\"\ntype IndicatorTone = \"accent\" | \"neutral\" | \"success\" | \"warning\" | \"error\"\ntype IndicatorCorner = \"top-right\" | \"top-left\" | \"bottom-right\" | \"bottom-left\"\ntype IndicatorShape = \"square\" | \"circle\"\n\nconst POP_FROM_SCALE = 0.5\n\n/** Status hues are fixed, because the accent may itself be amber or red. Only `accent` follows the theme. */\nconst TONE_CLASS: Record<IndicatorTone, string> = {\n  accent: \"bg-mise text-mise-foreground\",\n  neutral: \"bg-foreground text-background\",\n  success: \"bg-emerald-600 text-white dark:bg-emerald-500 dark:text-emerald-950\",\n  warning: \"bg-amber-400 text-amber-950\",\n  // The dark theme lightens the error colour past what white text can sit on.\n  error: \"bg-destructive text-white dark:text-background\",\n}\n\nconst RING_TONE_CLASS: Record<IndicatorTone, string> = {\n  accent: \"ring-mise\",\n  neutral: \"ring-foreground\",\n  success: \"ring-emerald-500\",\n  warning: \"ring-amber-400\",\n  error: \"ring-destructive\",\n}\n\n/** The centre of the mark sits on the corner of a square child. */\nconst SQUARE_CORNER_CLASS: Record<IndicatorCorner, string> = {\n  \"top-right\": \"top-0 right-0 translate-x-1/2 -translate-y-1/2\",\n  \"top-left\": \"top-0 left-0 -translate-x-1/2 -translate-y-1/2\",\n  \"bottom-right\": \"right-0 bottom-0 translate-x-1/2 translate-y-1/2\",\n  \"bottom-left\": \"bottom-0 left-0 -translate-x-1/2 translate-y-1/2\",\n}\n\n/** A circle has nothing at its bounding corner, so the mark moves in by 14.6% to land on the rim itself. */\nconst CIRCLE_CORNER_CLASS: Record<IndicatorCorner, string> = {\n  \"top-right\": \"top-[14.6%] right-[14.6%] translate-x-1/2 -translate-y-1/2\",\n  \"top-left\": \"top-[14.6%] left-[14.6%] -translate-x-1/2 -translate-y-1/2\",\n  \"bottom-right\": \"right-[14.6%] bottom-[14.6%] translate-x-1/2 translate-y-1/2\",\n  \"bottom-left\": \"bottom-[14.6%] left-[14.6%] -translate-x-1/2 translate-y-1/2\",\n}\n\n/** The cut-out takes the surface colour, so a mark reads as sitting on its child and not as a smudge touching it. */\nconst CUT_OUT_CLASS = \"ring-2 ring-[var(--indicator-surface,var(--background))]\"\n\nconst MARK_CLASS: Record<Exclude<IndicatorVariant, \"ring\">, string> = {\n  dot: \"block size-2.5 rounded-full\",\n  count: \"flex h-5 min-w-5 items-center justify-center rounded-full px-1 text-xs leading-none font-medium tabular-nums\",\n  label: \"flex h-5 items-center rounded-sm px-1 text-xs leading-none font-medium whitespace-nowrap\",\n}\n\n/** Anything that is not a positive whole number is treated as nothing to report. */\nfunction normaliseCount(count: number | undefined) {\n  if (count === undefined || !Number.isFinite(count)) return 0\n  return Math.max(0, Math.floor(count))\n}\n\nfunction formatIndicatorCount(count: number | undefined, max: number) {\n  const whole = normaliseCount(count)\n  return whole > max ? `${max}+` : String(whole)\n}\n\ninterface VisibilityOptions {\n  count: number | undefined\n  showZero: boolean\n  text: React.ReactNode\n}\n\n/** No count at all means a plain \"something here\" mark; a count of zero means nothing here, unless asked. */\nfunction isIndicatorVisible({ count, showZero, text }: VisibilityOptions) {\n  if (text !== undefined && text !== null) return true\n  if (count === undefined) return true\n  return normaliseCount(count) > 0 || showZero\n}\n\ntype IndicatorProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n  /** What the mark sits on. Keep it small and interactive things inside it, as the mark itself ignores the pointer. */\n  children: React.ReactNode\n  /** `dot` says something is new, `count` says how much, `ring` circles the whole child, `label` carries a short word. */\n  variant?: IndicatorVariant\n  /** `pop` scales the mark in when its value changes, never on first paint. */\n  effect?: IndicatorEffect\n  count?: number\n  /** Above this the mark reads 99+. */\n  max?: number\n  /** Keeps the mark at a count of zero. */\n  showZero?: boolean\n  /** The word in `label`. */\n  text?: React.ReactNode\n  /** Required: a number floating over an icon means nothing to a screen reader without its noun. */\n  label: string\n  tone?: IndicatorTone\n  corner?: IndicatorCorner\n  /** Set `circle` for avatars, so the mark lands on the rim instead of floating off the bounding box. */\n  shape?: IndicatorShape\n}\n\nfunction Indicator({\n  children,\n  variant = \"count\",\n  effect,\n  count,\n  max = 99,\n  showZero = false,\n  text,\n  label,\n  tone = \"accent\",\n  corner = \"top-right\",\n  shape = \"square\",\n  className,\n  ...props\n}: IndicatorProps) {\n  const reduceMotion = useReducedMotion() ?? false\n  const visible = isIndicatorVisible({ count, showZero, text })\n  const shown = variant === \"label\" ? text : formatIndicatorCount(count, max)\n  const popKey = variant === \"dot\" ? String(normaliseCount(count)) : String(shown)\n  const corners = shape === \"circle\" ? CIRCLE_CORNER_CLASS : SQUARE_CORNER_CLASS\n  return (\n    <span data-slot=\"indicator\" data-variant={variant} data-effect={effect} className={cn(\"relative inline-flex w-fit\", className)} {...props}>\n      {children}\n      {visible && variant === \"ring\" ? (\n        <span\n          aria-hidden=\"true\"\n          data-slot=\"indicator-ring\"\n          className={cn(\"pointer-events-none absolute -inset-1 ring-2\", shape === \"circle\" ? \"rounded-full\" : \"rounded-lg\", RING_TONE_CLASS[tone])}\n        />\n      ) : null}\n      {visible && variant !== \"ring\" ? (\n        <span aria-hidden=\"true\" data-slot=\"indicator-anchor\" className={cn(\"pointer-events-none absolute z-10\", corners[corner])}>\n          <AnimatePresence initial={false}>\n            <motion.span\n              key={effect === \"pop\" && !reduceMotion ? popKey : \"still\"}\n              data-slot=\"indicator-mark\"\n              className={cn(MARK_CLASS[variant], TONE_CLASS[tone], CUT_OUT_CLASS)}\n              initial={effect === \"pop\" && !reduceMotion ? { scale: POP_FROM_SCALE } : false}\n              animate={{ scale: 1 }}\n              transition={SPRING_DEFAULT}\n            >\n              {variant === \"dot\" ? null : shown}\n            </motion.span>\n          </AnimatePresence>\n        </span>\n      ) : null}\n      {visible ? <span className=\"sr-only\">{label}</span> : null}\n    </span>\n  )\n}\n\nexport {\n  Indicator,\n  formatIndicatorCount,\n  isIndicatorVisible,\n  type IndicatorProps,\n  type IndicatorVariant,\n  type IndicatorEffect,\n  type IndicatorTone,\n  type IndicatorCorner,\n  type IndicatorShape,\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "display"
  ],
  "type": "registry:ui"
}