{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "comment-pin",
  "title": "Comment Pin",
  "description": "Anchored discussion pin rendered at canvas coordinates with author + unread badge.",
  "dependencies": [
    "@vllnt/ui@^0.3.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/comment-pin/comment-pin.tsx",
      "content": "\"use client\";\n\nimport type { ComponentPropsWithoutRef, ReactNode } 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 = ({\n  ref,\n  ...props\n}: CommentPinProps & { ref?: React.Ref<HTMLDivElement> }) => {\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 handleActivateComment = (): 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={handleActivateComment}\n          type=\"button\"\n        >\n          {body}\n        </button>\n      ) : (\n        <span className=\"relative inline-flex\">{body}</span>\n      )}\n    </div>\n  );\n};\nCommentPin.displayName = \"CommentPin\";\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component",
  "version": "0.3.0",
  "stability": "stable"
}
