{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "comment-pin",
  "type": "registry:component",
  "title": "Comment Pin",
  "description": "Anchored discussion pin rendered at canvas coordinates with author + unread badge.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/comment-pin/comment-pin.tsx",
      "content": "\"use client\";\n\nimport {\n  type ComponentPropsWithoutRef,\n  forwardRef,\n  type ReactNode,\n} from \"react\";\n\nimport { cn } from \"@vllnt/ui\";\n\n/**\n * Resolution state of a pinned comment.\n *\n * @public\n */\nexport type CommentPinState = \"open\" | \"resolved\";\n\n/**\n * Localizable strings.\n *\n * @public\n */\nexport type CommentPinLabels = {\n  /** Aria-label override. Defaults to `\"Comment\"`. */\n  region?: string;\n  /** Suffix appended after the unread count for screen readers. */\n  unreadSuffix?: string;\n};\n\nconst DEFAULT_LABELS = {\n  region: \"Comment\",\n  unreadSuffix: \"unread\",\n} as const satisfies Required<CommentPinLabels>;\n\n/**\n * Props for {@link CommentPin}.\n *\n * @public\n */\nexport type CommentPinProps = {\n  /** Optional author initial / glyph rendered inside the pin. */\n  authorInitial?: ReactNode;\n  /** Optional accent color for the pin. Defaults to the foreground. */\n  color?: string;\n  /** Localizable strings. */\n  labels?: CommentPinLabels;\n  /** Click handler — when provided, the pin becomes a button. */\n  onActivate?: () => void;\n  /** State of the underlying thread. Defaults to `\"open\"`. */\n  state?: CommentPinState;\n  /** Optional unread reply count. Renders as a small numeric badge. */\n  unread?: number;\n  /** Anchor X in canvas pixels. */\n  x: number;\n  /** Anchor Y in canvas pixels. */\n  y: number;\n} & ComponentPropsWithoutRef<\"div\">;\n\nconst STATE_FILL: Record<CommentPinState, string> = {\n  open: \"bg-foreground text-background\",\n  resolved: \"bg-muted text-muted-foreground\",\n};\n\ntype PinBodyInput = {\n  accent?: string;\n  authorInitial?: ReactNode;\n  state: CommentPinState;\n  unread?: number;\n};\n\nconst PinBody = (props: PinBodyInput): React.ReactElement => {\n  const showBadge = typeof props.unread === \"number\" && props.unread > 0;\n  const useAccent = props.accent && props.state === \"open\";\n  return (\n    <>\n      <span\n        aria-hidden=\"true\"\n        className={cn(\n          \"flex size-7 items-center justify-center rounded-full border border-background text-[11px] font-semibold shadow-sm\",\n          STATE_FILL[props.state],\n        )}\n        data-comment-pin-body\n        style={\n          useAccent\n            ? { backgroundColor: props.accent, color: \"white\" }\n            : undefined\n        }\n      >\n        {props.authorInitial ?? \"•\"}\n      </span>\n      {showBadge ? (\n        <span\n          aria-hidden=\"true\"\n          className=\"absolute -right-1 -top-1 inline-flex min-h-[14px] min-w-[14px] items-center justify-center rounded-full bg-red-500 px-1 text-[9px] font-medium text-white\"\n          data-comment-pin-unread\n        >\n          {props.unread}\n        </span>\n      ) : null}\n    </>\n  );\n};\n\n/**\n * Anchored discussion pin rendered at canvas coordinates. Use to mark\n * an object-anchored comment thread; click to expand into a\n * {@link \"../thread-bubble/thread-bubble\".ThreadBubble} (or whatever the host wires up).\n *\n * Pure presentation; the host owns the thread store + supplies the\n * unread count and resolution state.\n *\n * @example\n * ```tsx\n * <div className=\"relative h-screen w-screen\">\n *   <Canvas />\n *   <CommentPin\n *     x={420} y={180}\n *     authorInitial=\"B\"\n *     unread={3}\n *     onActivate={() => openThread(\"c-1\")}\n *   />\n * </div>\n * ```\n *\n * @public\n */\nexport const CommentPin = forwardRef<HTMLDivElement, CommentPinProps>(\n  (props, ref) => {\n    const {\n      authorInitial,\n      className,\n      color,\n      labels,\n      onActivate,\n      state = \"open\",\n      unread,\n      x,\n      y,\n      ...rest\n    } = props;\n    const resolvedLabels = { ...DEFAULT_LABELS, ...labels };\n    const showBadge = typeof unread === \"number\" && unread > 0;\n    const ariaLabel = showBadge\n      ? `${resolvedLabels.region}, ${unread} ${resolvedLabels.unreadSuffix}`\n      : resolvedLabels.region;\n    const handleClick = (): void => {\n      onActivate?.();\n    };\n    const body = (\n      <PinBody\n        accent={color}\n        authorInitial={authorInitial}\n        state={state}\n        unread={unread}\n      />\n    );\n    return (\n      <div\n        aria-label={ariaLabel}\n        className={cn(\n          \"absolute z-30 inline-flex -translate-x-1/2 -translate-y-1/2\",\n          className,\n        )}\n        data-comment-pin\n        data-comment-pin-state={state}\n        ref={ref}\n        role=\"img\"\n        style={{ left: x, top: y }}\n        {...rest}\n      >\n        {onActivate ? (\n          <button\n            aria-label={ariaLabel}\n            className=\"relative inline-flex rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n            data-comment-pin-trigger\n            onClick={handleClick}\n            type=\"button\"\n          >\n            {body}\n          </button>\n        ) : (\n          <span className=\"relative inline-flex\">{body}</span>\n        )}\n      </div>\n    );\n  },\n);\nCommentPin.displayName = \"CommentPin\";\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
