{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"indicator","type":"registry:ui","title":"Indicator","description":"A dot or a count on the corner of a button, an avatar or a tab.","dependencies":[],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-locale.json"],"files":[{"path":"registry/dashral/indicator.tsx","type":"registry:file","target":"components/dashral/indicator.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\n\nimport { useLocale } from \"./lib/locale\"\nimport { cn } from \"./lib/utils\"\n\ntype IndicatorColor = \"primary\" | \"neutral\" | \"info\" | \"success\" | \"warning\" | \"destructive\" | \"highlight\"\n\n// The dot or badge, and its ping. Every class is written out in full so Tailwind finds it.\nconst colors: Record<IndicatorColor, { fill: string; ping: string }> = {\n  primary: { fill: \"bg-primary text-primary-foreground\", ping: \"bg-primary\" },\n  neutral: { fill: \"bg-foreground text-background\", ping: \"bg-foreground\" },\n  info: { fill: \"bg-info text-info-foreground\", ping: \"bg-info\" },\n  success: { fill: \"bg-success text-success-foreground\", ping: \"bg-success\" },\n  warning: { fill: \"bg-warning text-warning-foreground\", ping: \"bg-warning\" },\n  destructive: { fill: \"bg-destructive text-destructive-foreground\", ping: \"bg-destructive\" },\n  highlight: { fill: \"bg-highlight text-highlight-foreground\", ping: \"bg-highlight\" },\n}\n\ntype IndicatorPosition = \"top-end\" | \"bottom-end\" | \"top-start\" | \"bottom-start\"\n\n// Where it sits: its centre on the corner of a square element, or on the edge of a round one (an avatar), where the\n// corner would leave it floating in the air.\nconst positions: Record<\"square\" | \"round\", Record<IndicatorPosition, string>> = {\n  square: {\n    \"top-end\": \"top-0 end-0 -translate-y-1/2 translate-x-1/2 rtl:-translate-x-1/2\",\n    \"bottom-end\": \"bottom-0 end-0 translate-y-1/2 translate-x-1/2 rtl:-translate-x-1/2\",\n    \"top-start\": \"top-0 start-0 -translate-y-1/2 -translate-x-1/2 rtl:translate-x-1/2\",\n    \"bottom-start\": \"bottom-0 start-0 translate-y-1/2 -translate-x-1/2 rtl:translate-x-1/2\",\n  },\n  round: {\n    \"top-end\": \"top-[14.6%] end-[14.6%] -translate-y-1/2 translate-x-1/2 rtl:-translate-x-1/2\",\n    \"bottom-end\": \"bottom-[14.6%] end-[14.6%] translate-y-1/2 translate-x-1/2 rtl:-translate-x-1/2\",\n    \"top-start\": \"top-[14.6%] start-[14.6%] -translate-y-1/2 -translate-x-1/2 rtl:translate-x-1/2\",\n    \"bottom-start\": \"bottom-[14.6%] start-[14.6%] translate-y-1/2 -translate-x-1/2 rtl:translate-x-1/2\",\n  },\n}\n\ntype IndicatorProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n  /** What it sits on: an icon button, an avatar, a tab, a nav link. */\n  children: React.ReactNode\n  /** A number in a small badge (\"3\", \"99+\"); without it, a dot. 0 hides it, unless showZero. */\n  count?: number\n  /** Above this, \"99+\". */\n  max?: number\n  showZero?: boolean\n  /** Whether it shows. It scales in and out on the spring, so toggling it never pops. */\n  show?: boolean\n  color?: IndicatorColor\n  position?: IndicatorPosition\n  /** round: for a round element (an avatar), so it sits on the edge rather than off the corner. */\n  shape?: \"square\" | \"round\"\n  /** A soft ring pulsing out from it, for something live or urgent. Still with reduced motion. */\n  pulse?: boolean\n  /**\n   * What it means, for screen readers: \"3 unread messages\", \"New\". Without it, it's decoration (hidden from them), so\n   * say it in the element's own label instead.\n   */\n  label?: string\n  classNames?: { indicator?: string }\n}\n\n/**\n * A dot or a count on the corner of something, in one tag: `<Indicator count={3} label=\"3 unread\"><Button …/></Indicator>`.\n * The numbers are written in the locale. A ring in the page's background color keeps it apart from what it sits on;\n * change it with classNames.indicator (\"ring-card\" on a card).\n */\nfunction Indicator({\n  children,\n  count,\n  max = 99,\n  showZero = false,\n  show = true,\n  color = \"destructive\",\n  position = \"top-end\",\n  shape = \"square\",\n  pulse = false,\n  label,\n  className,\n  classNames,\n  ...props\n}: IndicatorProps) {\n  const { locale } = useLocale()\n  const hasCount = count !== undefined\n  const visible = show && (!hasCount || count > 0 || showZero)\n  const number = new Intl.NumberFormat(locale)\n  const text = hasCount ? (count > max ? `${number.format(max)}+` : number.format(count)) : null\n  // While it shrinks away, it keeps the last number it showed (\"3\", not \"0\").\n  const [shownText, setShownText] = React.useState(text)\n  if (visible && text !== shownText) setShownText(text)\n\n  return (\n    <span data-slot=\"indicator\" className={cn(\"relative inline-flex w-fit shrink-0\", className)} {...props}>\n      {children}\n      <span\n        data-state={visible ? \"visible\" : \"hidden\"}\n        aria-hidden={!label || !visible}\n        className={cn(\n          \"pointer-events-none absolute z-10 grid place-items-center rounded-full ring-2 ring-background\",\n          positions[shape][position],\n          hasCount ? \"h-4.5 min-w-4.5 px-1 text-[10px] leading-none font-semibold tabular-nums\" : \"size-2.5\",\n          colors[color].fill,\n          // In on the spring, out on --ease-exit (a spring would be gone in a blink), from its own centre. The scale is\n          // its own property, so the translate above holds.\n          \"transition-[scale,opacity] motion-reduce:transition-none\",\n          visible\n            ? \"scale-100 opacity-100 duration-(--duration-standard) ease-spring\"\n            : \"scale-0 opacity-0 duration-(--duration-standard) ease-exit\",\n          classNames?.indicator\n        )}\n      >\n        {pulse && visible && (\n          <span aria-hidden className={cn(\"absolute inset-0 -z-10 animate-ping rounded-full opacity-60 motion-reduce:hidden\", colors[color].ping)} />\n        )}\n        {/* With a label, the label is what's read; the number is for the eyes. */}\n        {shownText !== null && <span aria-hidden={label ? true : undefined}>{shownText}</span>}\n        {label && <span className=\"sr-only\">{label}</span>}\n      </span>\n    </span>\n  )\n}\n\nexport { Indicator, type IndicatorProps, type IndicatorColor, type IndicatorPosition }\n"}],"cssVars":{"theme":{"color-destructive-foreground":"var(--destructive-foreground)","color-destructive-text":"var(--destructive-text)","color-info":"var(--info)","color-info-foreground":"var(--info-foreground)","color-info-text":"var(--info-text)","color-success":"var(--success)","color-success-foreground":"var(--success-foreground)","color-success-text":"var(--success-text)","color-warning":"var(--warning)","color-warning-foreground":"var(--warning-foreground)","color-warning-text":"var(--warning-text)","color-highlight":"var(--highlight)","color-highlight-foreground":"var(--highlight-foreground)","color-highlight-text":"var(--highlight-text)","ease-standard":"var(--ease-standard)","ease-enter":"var(--ease-enter)","ease-exit":"var(--ease-exit)","ease-spring":"var(--ease-spring)"},"light":{"destructive-foreground":"oklch(0.985 0 0)","destructive-text":"#bd2800","info":"oklch(0.5 0.134 242.749)","info-foreground":"oklch(0.985 0 0)","info-text":"oklch(0.443 0.11 240.79)","success":"oklch(0.508 0.118 165.612)","success-foreground":"oklch(0.985 0 0)","success-text":"oklch(0.432 0.095 166.913)","warning":"#f59200","warning-foreground":"oklch(0.3 0.07 60)","warning-text":"#964e00","highlight":"#b8008c","highlight-foreground":"oklch(0.985 0 0)","highlight-text":"#a3007b","duration-fast":"160ms","duration-standard":"240ms","duration-considered":"480ms","ease-standard":"cubic-bezier(0.2, 0, 0, 1)","ease-enter":"cubic-bezier(0, 0, 0, 1)","ease-exit":"cubic-bezier(0.3, 0, 1, 1)","ease-spring":"linear(0, 0.071 4%, 0.234 8%, 0.433 12%, 0.626 17%, 0.791 21%, 0.917 25%, 1.004 29%, 1.055 33%, 1.079 38%, 1.083 42%, 1.075 46%, 1.06 50%, 1.043 54%, 1.027 58%, 1.015 62%, 1.005 67%, 0.998 71%, 0.995 75%, 0.993 79%, 0.993 83%, 0.994 88%, 0.995 92%, 0.997 96%, 1)"},"dark":{"destructive-foreground":"oklch(0.2 0 0)","destructive-text":"#ff8a5c","info":"oklch(0.685 0.169 237.323)","info-foreground":"oklch(0.2 0 0)","info-text":"oklch(0.746 0.16 232.661)","success":"oklch(0.696 0.17 162.48)","success-foreground":"oklch(0.2 0.04 160)","success-text":"oklch(0.765 0.177 163.223)","warning":"#f59200","warning-foreground":"oklch(0.25 0.06 60)","warning-text":"#ffba42","highlight":"#ff2ecf","highlight-foreground":"oklch(0.2 0 0)","highlight-text":"#ff75df"}}}