{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "engagement-bar",
  "title": "Engagement Bar",
  "author": "ilinxa",
  "description": "Social action row — like, comment, share, bookmark, custom actions, and a multi-reaction picker with realtime counts and burst animation.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "avatar",
    "input",
    "popover"
  ],
  "files": [
    {
      "path": "src/registry/components/data/engagement-bar/engagement-bar.tsx",
      "content": "\"use client\";\n\nimport { memo, useEffect, useImperativeHandle, useMemo, useRef } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  DEFAULT_ENGAGEMENT_BAR_LABELS,\n  type EngagementAction,\n  type EngagementActionAlign,\n  type EngagementBarHandle,\n  type EngagementBarProps,\n  type EngagementBarLabels,\n  type EngagementState,\n} from \"./types\";\nimport { ActionButton } from \"./parts/action-button\";\nimport {\n  deriveStateFromActions,\n  useEngagementState,\n} from \"./hooks/use-engagement-state\";\nimport { formatEngagementCount } from \"./lib/format-count\";\n\ninterface EngagementBarInnerProps extends EngagementBarProps {\n  ref?: React.Ref<EngagementBarHandle>;\n}\n\nfunction defaultAlignFor(kind: EngagementAction[\"kind\"]): EngagementActionAlign {\n  return kind === \"bookmark\" || kind === \"view-count\" ? \"right\" : \"left\";\n}\n\nfunction actionKey(action: EngagementAction, index: number): string {\n  if (action.kind === \"custom\") return `custom-${action.id}`;\n  return `${action.kind}-${index}`;\n}\n\nfunction EngagementBarInner({\n  actions,\n  variant = \"default\",\n  subscribe,\n  onSubscribeDelta,\n  likersPreview,\n  reactionsPreview,\n  labels: labelsProp,\n  className,\n  actionClassName,\n  ref,\n}: EngagementBarInnerProps) {\n  const { state, dispatch, controlled } = useEngagementState({\n    actions,\n    subscribe,\n    onSubscribeDelta,\n  });\n\n  const labels = useMemo<Required<Omit<EngagementBarLabels, \"formatCount\">>>(\n    () => ({ ...DEFAULT_ENGAGEMENT_BAR_LABELS, ...labelsProp }),\n    [labelsProp],\n  );\n\n  const format = useMemo<(n: number) => string>(\n    () => labelsProp?.formatCount ?? formatEngagementCount,\n    [labelsProp?.formatCount],\n  );\n\n  // Stable handle identity — refs mirror state + actions via passive effect\n  // (refs must not be written during render).\n  const stateRef = useRef<EngagementState>(state);\n  const actionsRef = useRef<EngagementAction[]>(actions);\n  useEffect(() => {\n    stateRef.current = state;\n    actionsRef.current = actions;\n  });\n\n  useImperativeHandle(\n    ref,\n    () => ({\n      triggerLike: () => {\n        const likeAction = actionsRef.current.find((a) => a.kind === \"like\");\n        if (!likeAction || likeAction.kind !== \"like\") return;\n        const next = !stateRef.current.liked;\n        if (likeAction.liked === undefined) {\n          dispatch({ kind: \"like-toggle\" });\n        }\n        likeAction.onToggle?.(next);\n      },\n      triggerBookmark: () => {\n        const bookmarkAction = actionsRef.current.find(\n          (a) => a.kind === \"bookmark\",\n        );\n        if (!bookmarkAction || bookmarkAction.kind !== \"bookmark\") return;\n        const next = !stateRef.current.bookmarked;\n        if (bookmarkAction.bookmarked === undefined) {\n          dispatch({ kind: \"bookmark-toggle\" });\n        }\n        bookmarkAction.onToggle?.(next);\n      },\n      triggerReaction: (kind: string | null) => {\n        const reactionAction = actionsRef.current.find(\n          (a) => a.kind === \"reaction\",\n        );\n        if (!reactionAction || reactionAction.kind !== \"reaction\") return;\n        // Per the handle's contract: no-op if `kind` is not in the catalog.\n        if (\n          kind !== null &&\n          !reactionAction.kinds.some((k) => k.key === kind)\n        ) {\n          return;\n        }\n        // Optimistic dispatch only when uncontrolled (otherwise host owns the value\n        // and the dispatch would be overwritten by the next render's overlay).\n        if (reactionAction.viewerReaction === undefined) {\n          dispatch({ kind: \"reaction-select\", reactionKind: kind });\n        }\n        // Defense 1 per Q-PP-3 — microtask-deferred consumer notify so the local\n        // mirror commits before the host hears about the change.\n        queueMicrotask(() => {\n          reactionAction.onSelect?.(kind);\n        });\n      },\n      getCurrentState: () => stateRef.current,\n      getCurrentReaction: () => stateRef.current.viewerReaction,\n      reset: () => {\n        dispatch({\n          kind: \"reset\",\n          next: deriveStateFromActions(actionsRef.current),\n        });\n      },\n    }),\n    // dispatch is stable; refs handle the rest. Empty dep array → handle identity is stable.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [],\n  );\n\n  if (actions.length === 0) return null;\n\n  // Stacked variant: vertical list, no left/right split, align ignored.\n  if (variant === \"stacked\") {\n    return (\n      <div\n        className={cn(\"flex flex-col items-center gap-3\", className)}\n      >\n        {actions.map((action, index) => (\n          <ActionButton\n            key={actionKey(action, index)}\n            action={action}\n            variant={variant}\n            state={state}\n            controlled={controlled}\n            dispatch={dispatch}\n            format={format}\n            labels={labels}\n            actionClassName={actionClassName}\n          />\n        ))}\n        {likersPreview ? <div className=\"mt-1\">{likersPreview}</div> : null}\n        {reactionsPreview ? (\n          <div className=\"mt-1\">{reactionsPreview}</div>\n        ) : null}\n      </div>\n    );\n  }\n\n  // Default + compact: horizontal row split by align.\n  const leftActions: EngagementAction[] = [];\n  const rightActions: EngagementAction[] = [];\n  for (const action of actions) {\n    const align = action.align ?? defaultAlignFor(action.kind);\n    if (align === \"right\") rightActions.push(action);\n    else leftActions.push(action);\n  }\n\n  const outerGapClass = variant === \"compact\" ? \"gap-1\" : \"gap-2\";\n\n  return (\n    <div className={cn(\"flex flex-col\", outerGapClass, className)}>\n      <div className=\"flex items-center justify-between\">\n        <div className=\"flex items-center gap-1\">\n          {leftActions.map((action, index) => (\n            <ActionButton\n              key={actionKey(action, index)}\n              action={action}\n              variant={variant}\n              state={state}\n              controlled={controlled}\n              dispatch={dispatch}\n              format={format}\n              labels={labels}\n              actionClassName={actionClassName}\n            />\n          ))}\n        </div>\n        {rightActions.length > 0 ? (\n          <div className=\"flex items-center gap-1\">\n            {rightActions.map((action, index) => (\n              <ActionButton\n                key={actionKey(action, index)}\n                action={action}\n                variant={variant}\n                state={state}\n                controlled={controlled}\n                dispatch={dispatch}\n                format={format}\n                labels={labels}\n                actionClassName={actionClassName}\n              />\n            ))}\n          </div>\n        ) : null}\n      </div>\n      {likersPreview ? <div className=\"mt-1\">{likersPreview}</div> : null}\n      {reactionsPreview ? (\n        <div className=\"mt-1\">{reactionsPreview}</div>\n      ) : null}\n    </div>\n  );\n}\n\nconst EngagementBar = memo(EngagementBarInner);\nEngagementBar.displayName = \"EngagementBar\";\n\nexport { EngagementBar };\n",
      "type": "registry:component",
      "target": "components/engagement-bar/engagement-bar.tsx"
    },
    {
      "path": "src/registry/components/data/engagement-bar/index.ts",
      "content": "export { EngagementBar } from \"./engagement-bar\";\nexport { EngagementHeartBurst } from \"./parts/engagement-heart-burst\";\nexport type { EngagementHeartBurstProps } from \"./parts/engagement-heart-burst\";\nexport { LikersStrip } from \"./parts/likers-strip\";\nexport type { LikersStripProps } from \"./parts/likers-strip\";\nexport { ShareMenu } from \"./parts/share-menu\";\nexport type { ShareMenuProps } from \"./parts/share-menu\";\nexport {\n  engagementReducer,\n  useEngagementState,\n  deriveStateFromActions,\n} from \"./hooks/use-engagement-state\";\nexport type {\n  UseEngagementStateOptions,\n  UseEngagementStateResult,\n} from \"./hooks/use-engagement-state\";\nexport { formatEngagementCount } from \"./lib/format-count\";\n\nexport type {\n  EngagementBarProps,\n  EngagementBarHandle,\n  EngagementBarVariant,\n  EngagementBarLabels,\n  EngagementAction,\n  EngagementActionAlign,\n  EngagementDelta,\n  EngagementLikeUser,\n  EngagementLikerProfile,\n  EngagementReactionKind,\n  EngagementState,\n  EngagementLocalAction,\n  Subscribe,\n  Unsubscribe,\n} from \"./types\";\n\nexport { DEFAULT_ENGAGEMENT_BAR_LABELS } from \"./types\";\n\n",
      "type": "registry:component",
      "target": "components/engagement-bar/index.ts"
    },
    {
      "path": "src/registry/components/data/engagement-bar/types.ts",
      "content": "import type { ReactNode } from \"react\";\n\nexport type EngagementBarVariant = \"default\" | \"compact\" | \"stacked\";\n\nexport type EngagementActionAlign = \"left\" | \"right\" | \"auto\";\n\n/**\n * Single reaction kind in the host-supplied catalog for the `reaction` action.\n *\n * One source of truth for icon + label + count — no parallel `counts` /\n * `availableKinds` maps that can drift. Backends with their own reaction codes\n * map their rows to this shape at the host boundary.\n */\nexport interface EngagementReactionKind {\n  /** Stable identifier matching backend payloads (e.g. `\"love\"`, `\"laugh\"`). */\n  key: string;\n  /** Host-supplied icon node (lucide / emoji / image). Library does not ship reaction icons. */\n  icon: ReactNode;\n  /** Localized human label — used in picker tooltip + aria-label. */\n  label: string;\n  /** Seed tally for this kind. After the bar mounts, live tally lives in `EngagementState.reactionCounts[key]`. */\n  count: number;\n  /** Optional tint (any CSS color) applied to the icon when this is the viewer's current reaction. */\n  color?: string;\n}\n\n/** Strict discriminated union — no extra fields. */\nexport type EngagementAction =\n  | {\n      kind: \"like\";\n      count: number;\n      liked?: boolean;\n      onToggle?: (next: boolean) => void;\n      /**\n       * Optional separate click target for the count number. When provided, the\n       * like action splits into two clickable elements:\n       *   - the heart icon (fires onToggle)\n       *   - the count text (fires onCountClick)\n       * Use this for kasder-style \"tap heart to like, tap count to open likers panel\".\n       * If omitted, the heart + count behave as a single button (backwards-compatible).\n       */\n      onCountClick?: () => void;\n      align?: EngagementActionAlign;\n    }\n  | {\n      kind: \"comment\";\n      count: number;\n      onClick?: () => void;\n      align?: EngagementActionAlign;\n    }\n  | {\n      kind: \"share\";\n      count?: number;\n      onClick?: () => void;\n      align?: EngagementActionAlign;\n    }\n  | {\n      kind: \"bookmark\";\n      bookmarked?: boolean;\n      onToggle?: (next: boolean) => void;\n      align?: EngagementActionAlign;\n    }\n  | {\n      kind: \"view-count\";\n      count: number;\n      align?: EngagementActionAlign;\n    }\n  | {\n      kind: \"custom\";\n      id: string;\n      label: string;\n      icon: ReactNode;\n      count?: number;\n      active?: boolean;\n      onClick?: () => void;\n      align?: EngagementActionAlign;\n    }\n  | {\n      /**\n       * Multi-kind reaction action (FB / LinkedIn style). One per-content reaction\n       * per viewer; choose from `kinds`. Picker opens on tap-when-null, on\n       * tap-when-set if `clearOnTap === false`, or on long-press (350ms) always.\n       *\n       * Coexists freely with `kind: \"like\"` per Q-P3 lock — a single content item\n       * MAY have both action types in the bar's `actions` array (hybrid UIs are\n       * a supported pattern).\n       */\n      kind: \"reaction\";\n      /** Ordered kind catalog. Single source of truth for icons + labels + seed counts. */\n      kinds: EngagementReactionKind[];\n      /** Pre-summed total across all kinds. Drives the action's count label. */\n      totalCount: number;\n      /** Viewer's currently-selected kind key (must match one of `kinds[].key`), or null. */\n      viewerReaction?: string | null;\n      /** Fires when viewer picks a kind, or `null` to clear. */\n      onSelect?: (kind: string | null) => void;\n      /**\n       * Optional separate click target for the count number — mirrors `like.onCountClick`.\n       * Hosts use this to open a reactors panel inline. When unset, count is non-interactive text.\n       */\n      onCountClick?: () => void;\n      /**\n       * Tap-with-current-reaction behavior. Default `true` (tap-clears, Twitter-heart style).\n       * Set `false` for FB-Reactions parity — tap opens the picker; the picker's `Remove`\n       * button is then the clear escape. Long-press always opens the picker regardless.\n       */\n      clearOnTap?: boolean;\n      align?: EngagementActionAlign;\n    };\n\nexport interface EngagementLikeUser {\n  id: string;\n  name: string;\n  username: string;\n  avatar: string;\n}\n\n/**\n * UI-display shape for likers strips and share-menu user pickers.\n * Looser than {@link EngagementLikeUser} (the realtime-delta payload shape) —\n * `username` and `avatar` are optional because UI sources may not always have them.\n */\nexport interface EngagementLikerProfile {\n  id: string;\n  name: string;\n  username?: string;\n  avatar?: string;\n}\n\n/** Realtime delta union. Same shape conventions as comment-thread will use. */\nexport type EngagementDelta =\n  | { kind: \"like-changed\"; count: number; liked?: boolean; userId?: string }\n  | { kind: \"comment-count-changed\"; count: number }\n  | { kind: \"share-count-changed\"; count: number }\n  | { kind: \"view-count-changed\"; count: number }\n  | { kind: \"bookmark-changed\"; bookmarked: boolean }\n  | { kind: \"liker-added\"; user: EngagementLikeUser }\n  | { kind: \"liker-removed\"; userId: string }\n  /** Server-authoritative replace of all 3 reaction state fields. Wins over local optimistic ops. */\n  | {\n      kind: \"reaction-changed\";\n      counts: Record<string, number>;\n      totalCount: number;\n      viewerReaction?: string | null;\n    }\n  /** Pass-through to host (bar does not maintain reactor lists; that's the host's `reactionsPreview` slot). */\n  | { kind: \"reactor-added\"; user: EngagementLikeUser; reactionKind: string }\n  | { kind: \"reactor-removed\"; userId: string; reactionKind: string };\n\nexport type Unsubscribe = () => void;\nexport type Subscribe<T> = (handler: (delta: T) => void) => Unsubscribe;\n\nexport interface EngagementBarLabels {\n  /** Default: \"Like\". aria-label when not liked. */\n  like?: string;\n  /** Default: \"Unlike\". aria-label when liked. */\n  unlike?: string;\n  /** Default: \"Show likers\". aria-label for the count-as-button when split via onCountClick. */\n  openLikersPanel?: string;\n  /** Default: \"Comment\". aria-label on comment action. */\n  comment?: string;\n  /** Default: \"Share\". aria-label on share action. */\n  share?: string;\n  /** Default: \"Bookmark\". aria-label when not bookmarked. */\n  bookmark?: string;\n  /** Default: \"Remove bookmark\". aria-label when bookmarked. */\n  unbookmark?: string;\n  /** Default: \"Views\". aria-label on view-count display. */\n  viewCount?: string;\n  /** Default: \"React\". aria-label on the reaction action trigger when `viewerReaction` is null. */\n  react?: string;\n  /** Default: \"Remove reaction\". aria-label / button text for the picker's clear-current-reaction control. */\n  removeReaction?: string;\n  /** Default: \"Show reactions\". aria-label for the count-as-button on the reaction action when split via onCountClick. */\n  openReactionsPanel?: string;\n  /** Default: \"Pick a reaction\". aria-label on the picker popover (group label). */\n  reactionPickerLabel?: string;\n  /** Optional locale-aware count formatter. Defaults to formatEngagementCount. */\n  formatCount?: (n: number) => string;\n}\n\nexport interface EngagementBarProps {\n  /** The actions to render. Required. Order = render order (within each align group). */\n  actions: EngagementAction[];\n  /** Variant. Default: \"default\". */\n  variant?: EngagementBarVariant;\n  /** Realtime subscription. Optional. Identity must be stable across renders (memoize via useCallback). */\n  subscribe?: Subscribe<EngagementDelta>;\n  /** Fires for every delta the subscription emits, regardless of controlled/uncontrolled mode. */\n  onSubscribeDelta?: (delta: EngagementDelta) => void;\n  /** Slot rendered below the action row. Hosts use for likers preview / \"X liked this\" / etc. */\n  likersPreview?: ReactNode;\n  /**\n   * Slot rendered below the action row — parallel to `likersPreview`. Hosts use for\n   * mixed-kind reactor preview / \"Alice, Bob and 42 others reacted\" / etc. Rendered\n   * unconditionally when provided (does not gate on reaction action presence).\n   * When BOTH slots are passed, both render in order: `likersPreview` first, then\n   * `reactionsPreview`. Host's call to deduplicate if both would be redundant.\n   */\n  reactionsPreview?: ReactNode;\n  /** Localized labels. Defaults are English. */\n  labels?: EngagementBarLabels;\n  /** Override classes for the wrapping <div>. */\n  className?: string;\n  /** Override classes for each action button. */\n  actionClassName?: string;\n}\n\nexport interface EngagementBarHandle {\n  /** Programmatically toggle the like action (flips state + fires onToggle). No-op if no like action present. */\n  triggerLike: () => void;\n  /** Programmatically toggle the bookmark action. No-op if no bookmark action present. */\n  triggerBookmark: () => void;\n  /**\n   * Programmatically set the viewer's reaction (or `null` to clear). Flips local\n   * mirror + fires `action.onSelect` (microtask-deferred per Defense 1). No-op if\n   * no reaction action present. No-op if `kind` is not in the action's `kinds[].key` catalog.\n   */\n  triggerReaction: (kind: string | null) => void;\n  /** Read the current optimistic state of all toggleable actions. Includes reaction fields (null when no reaction action). */\n  getCurrentState: () => EngagementState;\n  /** Convenience read — returns `state.viewerReaction`. Mirrors the `triggerLike`/`getCurrentState` parity. */\n  getCurrentReaction: () => string | null;\n  /** Reset internal optimistic state to actions' values (no-op for fully-controlled fields). */\n  reset: () => void;\n}\n\n/** Internal optimistic state shape. Public-readable via getCurrentState() / engagementReducer. */\nexport interface EngagementState {\n  liked: boolean;\n  likeCount: number;\n  commentCount: number;\n  shareCount: number | null;\n  viewCount: number | null;\n  bookmarked: boolean;\n  /**\n   * Live per-kind tallies. `null` when no `kind: \"reaction\"` entry is present in\n   * the actions array. After init, `reactionCounts[key]` is the source of truth\n   * for display — `action.kinds[i].count` is the seed only. Renderers MUST read\n   * `state.reactionCounts[k.key] ?? k.count` per kind.\n   */\n  reactionCounts: Record<string, number> | null;\n  /** Live total across all kinds. `null` when no reaction action present. */\n  reactionTotalCount: number | null;\n  /** Viewer's current reaction key, or `null` if none / no reaction action present. */\n  viewerReaction: string | null;\n}\n\n/** Reducer action union. Public for hosts driving their own state machines. */\nexport type EngagementLocalAction =\n  | { kind: \"like-toggle\" }\n  | { kind: \"bookmark-toggle\" }\n  /** Pick a kind (string) or clear (`null`). Updates `viewerReaction` + per-kind counts + total. */\n  | { kind: \"reaction-select\"; reactionKind: string | null }\n  | { kind: \"subscribe-delta\"; delta: EngagementDelta }\n  | { kind: \"reset\"; next: EngagementState };\n\nexport const DEFAULT_ENGAGEMENT_BAR_LABELS: Required<\n  Omit<EngagementBarLabels, \"formatCount\">\n> = {\n  like: \"Like\",\n  unlike: \"Unlike\",\n  openLikersPanel: \"Show likers\",\n  comment: \"Comment\",\n  share: \"Share\",\n  bookmark: \"Bookmark\",\n  unbookmark: \"Remove bookmark\",\n  viewCount: \"Views\",\n  react: \"React\",\n  removeReaction: \"Remove reaction\",\n  openReactionsPanel: \"Show reactions\",\n  reactionPickerLabel: \"Pick a reaction\",\n};\n",
      "type": "registry:component",
      "target": "components/engagement-bar/types.ts"
    },
    {
      "path": "src/registry/components/data/engagement-bar/parts/action-button.tsx",
      "content": "\"use client\";\n\nimport { memo } from \"react\";\nimport type {\n  EngagementAction,\n  EngagementBarVariant,\n  EngagementBarLabels,\n  EngagementLocalAction,\n  EngagementState,\n} from \"../types\";\nimport { LikeAction } from \"./like-action\";\nimport { CommentAction } from \"./comment-action\";\nimport { ShareAction } from \"./share-action\";\nimport { BookmarkAction } from \"./bookmark-action\";\nimport { ViewCountAction } from \"./view-count-action\";\nimport { CustomAction } from \"./custom-action\";\nimport { ReactionAction } from \"./reaction-action\";\n\ninterface ControlledFlags {\n  liked: boolean;\n  bookmarked: boolean;\n}\n\ninterface ActionButtonProps {\n  action: EngagementAction;\n  variant: EngagementBarVariant;\n  state: EngagementState;\n  controlled: ControlledFlags;\n  dispatch: React.Dispatch<EngagementLocalAction>;\n  format: (n: number) => string;\n  labels: Required<Omit<EngagementBarLabels, \"formatCount\">>;\n  actionClassName?: string;\n}\n\n/**\n * Dispatches to the per-kind action component. Click handlers live INSIDE\n * each per-kind component (not memoized upstream — that would be a hooks-rules\n * violation inside `.map()`). React.memo on each per-kind keeps re-renders cheap.\n */\nfunction ActionButtonInner({\n  action,\n  variant,\n  state,\n  controlled,\n  dispatch,\n  format,\n  labels,\n  actionClassName,\n}: ActionButtonProps) {\n  switch (action.kind) {\n    case \"like\":\n      return (\n        <LikeAction\n          variant={variant}\n          liked={state.liked}\n          count={state.likeCount}\n          controlled={controlled.liked}\n          onToggle={action.onToggle}\n          onCountClick={action.onCountClick}\n          format={format}\n          labels={labels}\n          dispatch={dispatch}\n          actionClassName={actionClassName}\n        />\n      );\n    case \"comment\":\n      return (\n        <CommentAction\n          variant={variant}\n          count={state.commentCount}\n          onClick={action.onClick}\n          format={format}\n          labels={labels}\n          actionClassName={actionClassName}\n        />\n      );\n    case \"share\":\n      return (\n        <ShareAction\n          variant={variant}\n          count={state.shareCount ?? undefined}\n          onClick={action.onClick}\n          format={format}\n          labels={labels}\n          actionClassName={actionClassName}\n        />\n      );\n    case \"bookmark\":\n      return (\n        <BookmarkAction\n          variant={variant}\n          bookmarked={state.bookmarked}\n          controlled={controlled.bookmarked}\n          onToggle={action.onToggle}\n          labels={labels}\n          dispatch={dispatch}\n          actionClassName={actionClassName}\n        />\n      );\n    case \"view-count\":\n      return (\n        <ViewCountAction\n          variant={variant}\n          count={state.viewCount ?? action.count}\n          format={format}\n          labels={labels}\n          actionClassName={actionClassName}\n        />\n      );\n    case \"custom\":\n      return (\n        <CustomAction\n          variant={variant}\n          id={action.id}\n          label={action.label}\n          icon={action.icon}\n          count={action.count}\n          active={action.active}\n          onClick={action.onClick}\n          format={format}\n          actionClassName={actionClassName}\n        />\n      );\n    case \"reaction\":\n      return (\n        <ReactionAction\n          action={action}\n          variant={variant}\n          state={state}\n          dispatch={dispatch}\n          format={format}\n          labels={labels}\n          actionClassName={actionClassName}\n        />\n      );\n  }\n}\n\nexport const ActionButton = memo(ActionButtonInner);\nActionButton.displayName = \"ActionButton\";\n",
      "type": "registry:component",
      "target": "components/engagement-bar/parts/action-button.tsx"
    },
    {
      "path": "src/registry/components/data/engagement-bar/parts/like-action.tsx",
      "content": "\"use client\";\n\nimport { memo } from \"react\";\nimport { Heart } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport type {\n  EngagementBarVariant,\n  EngagementBarLabels,\n  EngagementLocalAction,\n} from \"../types\";\n\ninterface LikeActionProps {\n  variant: EngagementBarVariant;\n  liked: boolean;\n  count: number;\n  controlled: boolean;\n  onToggle?: (next: boolean) => void;\n  /** When provided, count becomes a separate clickable target. */\n  onCountClick?: () => void;\n  format: (n: number) => string;\n  labels: Required<Omit<EngagementBarLabels, \"formatCount\">>;\n  dispatch: React.Dispatch<EngagementLocalAction>;\n  actionClassName?: string;\n}\n\nfunction LikeActionInner({\n  variant,\n  liked,\n  count,\n  controlled,\n  onToggle,\n  onCountClick,\n  format,\n  labels,\n  dispatch,\n  actionClassName,\n}: LikeActionProps) {\n  const handleHeartClick = () => {\n    const next = !liked;\n    if (!controlled) {\n      dispatch({ kind: \"like-toggle\" });\n    }\n    onToggle?.(next);\n  };\n\n  const heartAriaLabel = liked ? labels.unlike : labels.like;\n  const iconSizeClass = variant === \"compact\" ? \"h-4 w-4\" : \"h-5 w-5\";\n  const splitCount = !!onCountClick;\n\n  if (variant === \"stacked\") {\n    // Stacked layout: heart icon + count vertically. Split mode wraps each in\n    // its own button.\n    if (splitCount) {\n      return (\n        <div className={cn(\"flex flex-col items-center gap-0.5\", actionClassName)}>\n          <Button\n            type=\"button\"\n            variant=\"ghost\"\n            size=\"sm\"\n            aria-pressed={liked}\n            aria-label={heartAriaLabel}\n            onClick={handleHeartClick}\n            className={cn(\"h-auto px-2 py-1\", liked && \"text-destructive\")}\n          >\n            <Heart\n              className={cn(\n                \"h-6 w-6 transition-transform\",\n                liked && \"scale-110 fill-current\",\n              )}\n            />\n          </Button>\n          <button\n            type=\"button\"\n            onClick={onCountClick}\n            aria-label={labels.openLikersPanel ?? \"Show likers\"}\n            className=\"rounded text-xs font-medium tabular-nums text-foreground hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n          >\n            {format(count)}\n          </button>\n        </div>\n      );\n    }\n    return (\n      <Button\n        type=\"button\"\n        variant=\"ghost\"\n        size=\"sm\"\n        aria-pressed={liked}\n        aria-label={heartAriaLabel}\n        onClick={handleHeartClick}\n        className={cn(\n          \"flex h-auto flex-col items-center gap-0.5 px-2 py-1\",\n          liked && \"text-destructive\",\n          actionClassName,\n        )}\n      >\n        <Heart\n          className={cn(\n            \"h-6 w-6 transition-transform\",\n            liked && \"scale-110 fill-current\",\n          )}\n        />\n        <span className=\"text-xs font-medium tabular-nums\" aria-live=\"polite\">\n          {format(count)}\n        </span>\n      </Button>\n    );\n  }\n\n  // Default + compact horizontal layout. Split mode: heart Button + count button\n  // separated by gap-2 to visually match the bundled `gap-2 px-2` of single-button\n  // actions (comment / share / bookmark / view-count).\n  if (splitCount) {\n    return (\n      <div className={cn(\"flex items-center gap-2 pr-2\", actionClassName)}>\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"sm\"\n          aria-pressed={liked}\n          aria-label={heartAriaLabel}\n          onClick={handleHeartClick}\n          className={cn(\n            \"h-9 px-2 transition-colors\",\n            liked && \"text-destructive\",\n          )}\n        >\n          <Heart\n            className={cn(\n              iconSizeClass,\n              \"transition-transform\",\n              liked && \"scale-110 fill-current\",\n            )}\n          />\n        </Button>\n        <button\n          type=\"button\"\n          onClick={onCountClick}\n          aria-label={labels.openLikersPanel ?? \"Show likers\"}\n          className={cn(\n            \"rounded text-sm font-medium tabular-nums transition-colors hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n            liked ? \"text-destructive\" : \"text-foreground\",\n          )}\n        >\n          {format(count)}\n        </button>\n      </div>\n    );\n  }\n\n  return (\n    <Button\n      type=\"button\"\n      variant=\"ghost\"\n      size=\"sm\"\n      aria-pressed={liked}\n      aria-label={heartAriaLabel}\n      onClick={handleHeartClick}\n      className={cn(\n        \"gap-2 px-2 transition-colors\",\n        liked && \"text-destructive\",\n        actionClassName,\n      )}\n    >\n      <Heart\n        className={cn(\n          iconSizeClass,\n          \"transition-transform\",\n          liked && \"scale-110 fill-current\",\n        )}\n      />\n      <span className=\"text-sm font-medium tabular-nums\" aria-live=\"polite\">\n        {format(count)}\n      </span>\n    </Button>\n  );\n}\n\nexport const LikeAction = memo(LikeActionInner);\nLikeAction.displayName = \"LikeAction\";\n",
      "type": "registry:component",
      "target": "components/engagement-bar/parts/like-action.tsx"
    },
    {
      "path": "src/registry/components/data/engagement-bar/parts/comment-action.tsx",
      "content": "\"use client\";\n\nimport { memo } from \"react\";\nimport { MessageCircle } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport type { EngagementBarVariant, EngagementBarLabels } from \"../types\";\n\ninterface CommentActionProps {\n  variant: EngagementBarVariant;\n  count: number;\n  onClick?: () => void;\n  format: (n: number) => string;\n  labels: Required<Omit<EngagementBarLabels, \"formatCount\">>;\n  actionClassName?: string;\n}\n\nfunction CommentActionInner({\n  variant,\n  count,\n  onClick,\n  format,\n  labels,\n  actionClassName,\n}: CommentActionProps) {\n  const iconSizeClass = variant === \"compact\" ? \"h-4 w-4\" : \"h-5 w-5\";\n\n  if (variant === \"stacked\") {\n    return (\n      <Button\n        type=\"button\"\n        variant=\"ghost\"\n        size=\"sm\"\n        aria-label={labels.comment}\n        onClick={onClick}\n        className={cn(\n          \"flex h-auto flex-col items-center gap-0.5 px-2 py-1\",\n          actionClassName,\n        )}\n      >\n        <MessageCircle className=\"h-6 w-6\" />\n        <span className=\"text-xs font-medium tabular-nums\">\n          {format(count)}\n        </span>\n      </Button>\n    );\n  }\n\n  return (\n    <Button\n      type=\"button\"\n      variant=\"ghost\"\n      size=\"sm\"\n      aria-label={labels.comment}\n      onClick={onClick}\n      className={cn(\"gap-2 px-2\", actionClassName)}\n    >\n      <MessageCircle className={iconSizeClass} />\n      <span className=\"text-sm font-medium tabular-nums\">{format(count)}</span>\n    </Button>\n  );\n}\n\nexport const CommentAction = memo(CommentActionInner);\nCommentAction.displayName = \"CommentAction\";\n",
      "type": "registry:component",
      "target": "components/engagement-bar/parts/comment-action.tsx"
    },
    {
      "path": "src/registry/components/data/engagement-bar/parts/share-action.tsx",
      "content": "\"use client\";\n\nimport { memo } from \"react\";\nimport { Share2 } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport type { EngagementBarVariant, EngagementBarLabels } from \"../types\";\n\ninterface ShareActionProps {\n  variant: EngagementBarVariant;\n  count?: number;\n  onClick?: () => void;\n  format: (n: number) => string;\n  labels: Required<Omit<EngagementBarLabels, \"formatCount\">>;\n  actionClassName?: string;\n}\n\nfunction ShareActionInner({\n  variant,\n  count,\n  onClick,\n  format,\n  labels,\n  actionClassName,\n}: ShareActionProps) {\n  const iconSizeClass = variant === \"compact\" ? \"h-4 w-4\" : \"h-5 w-5\";\n  const showCount = count !== undefined;\n\n  if (variant === \"stacked\") {\n    return (\n      <Button\n        type=\"button\"\n        variant=\"ghost\"\n        size=\"sm\"\n        aria-label={labels.share}\n        onClick={onClick}\n        className={cn(\n          \"flex h-auto flex-col items-center gap-0.5 px-2 py-1\",\n          actionClassName,\n        )}\n      >\n        <Share2 className=\"h-6 w-6\" />\n        {showCount ? (\n          <span className=\"text-xs font-medium tabular-nums\">\n            {format(count)}\n          </span>\n        ) : null}\n      </Button>\n    );\n  }\n\n  return (\n    <Button\n      type=\"button\"\n      variant=\"ghost\"\n      size=\"sm\"\n      aria-label={labels.share}\n      onClick={onClick}\n      className={cn(showCount ? \"gap-2 px-2\" : \"px-2\", actionClassName)}\n    >\n      <Share2 className={iconSizeClass} />\n      {showCount ? (\n        <span className=\"text-sm font-medium tabular-nums\">\n          {format(count)}\n        </span>\n      ) : null}\n    </Button>\n  );\n}\n\nexport const ShareAction = memo(ShareActionInner);\nShareAction.displayName = \"ShareAction\";\n",
      "type": "registry:component",
      "target": "components/engagement-bar/parts/share-action.tsx"
    },
    {
      "path": "src/registry/components/data/engagement-bar/parts/bookmark-action.tsx",
      "content": "\"use client\";\n\nimport { memo } from \"react\";\nimport { Bookmark } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport type {\n  EngagementBarVariant,\n  EngagementBarLabels,\n  EngagementLocalAction,\n} from \"../types\";\n\ninterface BookmarkActionProps {\n  variant: EngagementBarVariant;\n  bookmarked: boolean;\n  controlled: boolean;\n  onToggle?: (next: boolean) => void;\n  labels: Required<Omit<EngagementBarLabels, \"formatCount\">>;\n  dispatch: React.Dispatch<EngagementLocalAction>;\n  actionClassName?: string;\n}\n\nfunction BookmarkActionInner({\n  variant,\n  bookmarked,\n  controlled,\n  onToggle,\n  labels,\n  dispatch,\n  actionClassName,\n}: BookmarkActionProps) {\n  const handleClick = () => {\n    const next = !bookmarked;\n    if (!controlled) {\n      dispatch({ kind: \"bookmark-toggle\" });\n    }\n    onToggle?.(next);\n  };\n\n  const ariaLabel = bookmarked ? labels.unbookmark : labels.bookmark;\n  const iconSizeClass = variant === \"compact\" ? \"h-4 w-4\" : \"h-5 w-5\";\n\n  if (variant === \"stacked\") {\n    return (\n      <Button\n        type=\"button\"\n        variant=\"ghost\"\n        size=\"sm\"\n        aria-pressed={bookmarked}\n        aria-label={ariaLabel}\n        onClick={handleClick}\n        className={cn(\n          \"flex h-auto flex-col items-center gap-0.5 px-2 py-1\",\n          actionClassName,\n        )}\n      >\n        <Bookmark\n          className={cn(\"h-6 w-6 transition-all\", bookmarked && \"fill-current\")}\n        />\n      </Button>\n    );\n  }\n\n  return (\n    <Button\n      type=\"button\"\n      variant=\"ghost\"\n      size=\"sm\"\n      aria-pressed={bookmarked}\n      aria-label={ariaLabel}\n      onClick={handleClick}\n      className={cn(\"px-2 transition-colors\", actionClassName)}\n    >\n      <Bookmark\n        className={cn(\n          iconSizeClass,\n          \"transition-all\",\n          bookmarked && \"fill-current\",\n        )}\n      />\n    </Button>\n  );\n}\n\nexport const BookmarkAction = memo(BookmarkActionInner);\nBookmarkAction.displayName = \"BookmarkAction\";\n",
      "type": "registry:component",
      "target": "components/engagement-bar/parts/bookmark-action.tsx"
    },
    {
      "path": "src/registry/components/data/engagement-bar/parts/view-count-action.tsx",
      "content": "\"use client\";\n\nimport { memo } from \"react\";\nimport { Eye } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport type { EngagementBarVariant, EngagementBarLabels } from \"../types\";\n\ninterface ViewCountActionProps {\n  variant: EngagementBarVariant;\n  count: number;\n  format: (n: number) => string;\n  labels: Required<Omit<EngagementBarLabels, \"formatCount\">>;\n  actionClassName?: string;\n}\n\nfunction ViewCountActionInner({\n  variant,\n  count,\n  format,\n  labels,\n  actionClassName,\n}: ViewCountActionProps) {\n  const iconSizeClass = variant === \"compact\" ? \"h-4 w-4\" : \"h-5 w-5\";\n\n  if (variant === \"stacked\") {\n    return (\n      <div\n        role=\"group\"\n        aria-label={labels.viewCount}\n        className={cn(\n          \"flex flex-col items-center gap-0.5 px-2 py-1 text-muted-foreground\",\n          actionClassName,\n        )}\n      >\n        <Eye className=\"h-6 w-6\" />\n        <span\n          className=\"text-xs font-medium tabular-nums\"\n          aria-live=\"polite\"\n        >\n          {format(count)}\n        </span>\n      </div>\n    );\n  }\n\n  return (\n    <div\n      role=\"group\"\n      aria-label={labels.viewCount}\n      className={cn(\n        \"flex items-center gap-2 px-2 text-sm text-muted-foreground\",\n        actionClassName,\n      )}\n    >\n      <Eye className={iconSizeClass} />\n      <span className=\"font-medium tabular-nums\" aria-live=\"polite\">\n        {format(count)}\n      </span>\n    </div>\n  );\n}\n\nexport const ViewCountAction = memo(ViewCountActionInner);\nViewCountAction.displayName = \"ViewCountAction\";\n",
      "type": "registry:component",
      "target": "components/engagement-bar/parts/view-count-action.tsx"
    },
    {
      "path": "src/registry/components/data/engagement-bar/parts/custom-action.tsx",
      "content": "\"use client\";\n\nimport { memo, type ReactNode } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport type { EngagementBarVariant } from \"../types\";\n\ninterface CustomActionProps {\n  variant: EngagementBarVariant;\n  id: string;\n  label: string;\n  icon: ReactNode;\n  count?: number;\n  active?: boolean;\n  onClick?: () => void;\n  format: (n: number) => string;\n  actionClassName?: string;\n}\n\nfunction CustomActionInner({\n  variant,\n  label,\n  icon,\n  count,\n  active,\n  onClick,\n  format,\n  actionClassName,\n}: CustomActionProps) {\n  const showCount = count !== undefined;\n\n  if (variant === \"stacked\") {\n    return (\n      <Button\n        type=\"button\"\n        variant=\"ghost\"\n        size=\"sm\"\n        aria-label={label}\n        aria-pressed={active ? true : undefined}\n        onClick={onClick}\n        className={cn(\n          \"flex h-auto flex-col items-center gap-0.5 px-2 py-1\",\n          active && \"text-primary\",\n          actionClassName,\n        )}\n      >\n        {icon}\n        {showCount ? (\n          <span className=\"text-xs font-medium tabular-nums\">\n            {format(count)}\n          </span>\n        ) : null}\n      </Button>\n    );\n  }\n\n  return (\n    <Button\n      type=\"button\"\n      variant=\"ghost\"\n      size=\"sm\"\n      aria-label={label}\n      aria-pressed={active ? true : undefined}\n      onClick={onClick}\n      className={cn(\n        showCount ? \"gap-2 px-2\" : \"px-2\",\n        active && \"text-primary\",\n        actionClassName,\n      )}\n    >\n      {icon}\n      {showCount ? (\n        <span className=\"text-sm font-medium tabular-nums\">\n          {format(count)}\n        </span>\n      ) : null}\n    </Button>\n  );\n}\n\nexport const CustomAction = memo(CustomActionInner);\nCustomAction.displayName = \"CustomAction\";\n",
      "type": "registry:component",
      "target": "components/engagement-bar/parts/custom-action.tsx"
    },
    {
      "path": "src/registry/components/data/engagement-bar/parts/engagement-heart-burst.tsx",
      "content": "import { Heart } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport \"./engagement-heart-burst.css\";\n\nexport interface EngagementHeartBurstProps {\n  /** Increment to trigger the burst animation. 0 = never burst (initial). */\n  trigger: number;\n  /** Override classes for the burst container (typically positions the overlay). */\n  className?: string;\n}\n\n/**\n * RSC-compatible CSS-keyframe heart burst. No \"use client\" — purely declarative.\n * The `key={trigger}` pattern remounts the inner div each time the trigger\n * counter changes, restarting the keyframe from the beginning.\n *\n * Host typical wiring:\n *\n *   const [burstKey, setBurstKey] = useState(0);\n *   <MediaCarousel\n *     onDoubleTap={() => setBurstKey((k) => k + 1)}\n *   />\n *   <EngagementHeartBurst\n *     trigger={burstKey}\n *     className=\"absolute inset-0 flex items-center justify-center pointer-events-none\"\n *   />\n */\nexport function EngagementHeartBurst({\n  trigger,\n  className,\n}: EngagementHeartBurstProps) {\n  if (trigger === 0) return null;\n\n  return (\n    <div\n      key={trigger}\n      aria-hidden=\"true\"\n      className={cn(\n        \"pointer-events-none flex items-center justify-center\",\n        className,\n      )}\n    >\n      <Heart className=\"engagement-heart-burst-icon h-24 w-24 fill-current text-destructive\" />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/engagement-bar/parts/engagement-heart-burst.tsx"
    },
    {
      "path": "src/registry/components/data/engagement-bar/parts/engagement-heart-burst.css",
      "content": "@keyframes engagement-heart-burst {\n  0% {\n    transform: scale(0);\n    opacity: 0;\n  }\n  35% {\n    transform: scale(1.4);\n    opacity: 1;\n  }\n  100% {\n    transform: scale(1);\n    opacity: 0;\n  }\n}\n\n.engagement-heart-burst-icon {\n  animation: engagement-heart-burst 600ms cubic-bezier(0.18, 0.89, 0.32, 1.28)\n    forwards;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .engagement-heart-burst-icon {\n    animation: engagement-heart-burst 200ms linear forwards;\n  }\n}\n",
      "type": "registry:file",
      "target": "components/engagement-bar/parts/engagement-heart-burst.css"
    },
    {
      "path": "src/registry/components/data/engagement-bar/parts/likers-strip.tsx",
      "content": "\"use client\";\n\nimport { memo, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport { Button } from \"@/components/ui/button\";\nimport type { EngagementLikerProfile } from \"../types\";\n\n/** Pointer-event drag-to-scroll for the likers strip (desktop). Touch devices get\n * native swipe via `touch-action: pan-x`. */\nfunction useDragScroll() {\n  const scrollRef = useRef<HTMLDivElement | null>(null);\n  const stateRef = useRef<{\n    down: boolean;\n    startX: number;\n    startScroll: number;\n    pointerId: number | null;\n  }>({\n    down: false,\n    startX: 0,\n    startScroll: 0,\n    pointerId: null,\n  });\n\n  const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (e.pointerType !== \"mouse\") return; // touch handled natively\n    const el = scrollRef.current;\n    if (!el) return;\n    stateRef.current = {\n      down: true,\n      startX: e.clientX,\n      startScroll: el.scrollLeft,\n      pointerId: e.pointerId,\n    };\n    el.setPointerCapture(e.pointerId);\n    el.style.cursor = \"grabbing\";\n  };\n\n  const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n    const s = stateRef.current;\n    if (!s.down || s.pointerId !== e.pointerId) return;\n    const el = scrollRef.current;\n    if (!el) return;\n    const dx = e.clientX - s.startX;\n    el.scrollLeft = s.startScroll - dx;\n  };\n\n  const endDrag = (e: React.PointerEvent<HTMLDivElement>) => {\n    const s = stateRef.current;\n    if (!s.down) return;\n    s.down = false;\n    s.pointerId = null;\n    const el = scrollRef.current;\n    if (el) {\n      el.style.cursor = \"\";\n      try {\n        el.releasePointerCapture(e.pointerId);\n      } catch {\n        // pointer may already be released\n      }\n    }\n  };\n\n  return {\n    scrollRef,\n    onPointerDown,\n    onPointerMove,\n    onPointerUp: endDrag,\n    onPointerCancel: endDrag,\n  };\n}\n\nexport interface LikersStripProps {\n  /** Total likes on the post (drives the \"+N\" pill). */\n  totalCount: number;\n  /** Already-loaded likers. */\n  likers: EngagementLikerProfile[];\n  /** Heading label (e.g. \"Beğenenler\" / \"Likes\"). */\n  heading: string;\n  /** Fetch more likers. Component appends results to local state. */\n  onLoadMore?: () => Promise<EngagementLikerProfile[]>;\n  /** \"+N\" pill aria-label template — `{count}` is replaced. Default \"+{count} more\". */\n  moreAriaLabelTemplate?: string;\n  /** Hide-button label (kasder's \"Gizle\"). When provided, renders a Hide button next to the heading. */\n  onClose?: () => void;\n  /** Hide-button label text. Default \"Hide\". */\n  closeLabel?: string;\n  className?: string;\n}\n\nfunction initials(name: string): string {\n  return (\n    name\n      .trim()\n      .split(/\\s+/)\n      .map((p) => p[0]?.toUpperCase() ?? \"\")\n      .slice(0, 2)\n      .join(\"\") || \"?\"\n  );\n}\n\nfunction LikersStripInner({\n  totalCount,\n  likers: initialLikers,\n  heading,\n  onLoadMore,\n  moreAriaLabelTemplate = \"+{count} more\",\n  onClose,\n  closeLabel = \"Hide\",\n  className,\n}: LikersStripProps) {\n  const [likers, setLikers] = useState(initialLikers);\n  const [isLoading, setIsLoading] = useState(false);\n  const remaining = Math.max(0, totalCount - likers.length);\n  const {\n    scrollRef,\n    onPointerDown,\n    onPointerMove,\n    onPointerUp,\n    onPointerCancel,\n  } = useDragScroll();\n\n  const handleLoadMore = async () => {\n    if (!onLoadMore || isLoading) return;\n    setIsLoading(true);\n    try {\n      const next = await onLoadMore();\n      setLikers((prev) => [...prev, ...next]);\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  return (\n    <div className={cn(\"flex flex-col gap-2\", className)}>\n      <div className=\"flex items-center justify-between gap-2\">\n        <span className=\"text-sm font-semibold\">{heading}</span>\n        {onClose ? (\n          <Button\n            type=\"button\"\n            variant=\"ghost\"\n            size=\"sm\"\n            onClick={onClose}\n            className=\"h-7 px-2 text-xs\"\n          >\n            {closeLabel}\n          </Button>\n        ) : null}\n      </div>\n      <div\n        ref={scrollRef}\n        onPointerDown={onPointerDown}\n        onPointerMove={onPointerMove}\n        onPointerUp={onPointerUp}\n        onPointerCancel={onPointerCancel}\n        style={{ touchAction: \"pan-x\" }}\n        className=\"flex cursor-grab select-none items-stretch gap-3 overflow-x-auto pb-1 [scrollbar-width:thin]\"\n        role=\"list\"\n      >\n        {likers.map((user) => (\n          <div\n            key={user.id}\n            role=\"listitem\"\n            className=\"flex w-16 shrink-0 flex-col items-center gap-1 sm:w-20\"\n          >\n            <Avatar className=\"h-10 w-10 sm:h-12 sm:w-12\">\n              {user.avatar ? <AvatarImage src={user.avatar} alt=\"\" /> : null}\n              <AvatarFallback>{initials(user.name)}</AvatarFallback>\n            </Avatar>\n            {/* Fixed-height name+username slot — keeps every column the same total height\n                so the strip looks symmetric regardless of which users have a username. */}\n            <div className=\"flex h-8 w-full flex-col justify-start text-center\">\n              <span className=\"truncate text-xs font-medium leading-tight\">\n                {user.name.split(\" \")[0]}\n              </span>\n              <span className=\"truncate text-[10px] leading-tight text-muted-foreground\">\n                {user.username ? `@${user.username}` : \" \"}\n              </span>\n            </div>\n          </div>\n        ))}\n        {remaining > 0 && onLoadMore ? (\n          <div className=\"flex w-16 shrink-0 flex-col items-center gap-1 sm:w-20\">\n            <Button\n              type=\"button\"\n              variant=\"outline\"\n              onClick={() => {\n                void handleLoadMore();\n              }}\n              disabled={isLoading}\n              aria-label={moreAriaLabelTemplate.replace(\n                \"{count}\",\n                String(remaining),\n              )}\n              className=\"h-11 w-11 shrink-0 rounded-full p-0 text-xs font-semibold sm:h-12 sm:w-12\"\n            >\n              {isLoading ? \"…\" : `+${remaining > 99 ? \"99\" : remaining}`}\n            </Button>\n            <div className=\"h-8\" aria-hidden=\"true\" />\n          </div>\n        ) : null}\n      </div>\n    </div>\n  );\n}\n\nexport const LikersStrip = memo(LikersStripInner);\nLikersStrip.displayName = \"LikersStrip\";\n",
      "type": "registry:component",
      "target": "components/engagement-bar/parts/likers-strip.tsx"
    },
    {
      "path": "src/registry/components/data/engagement-bar/parts/share-menu.tsx",
      "content": "\"use client\";\n\nimport { memo, useMemo, useState } from \"react\";\nimport { Search, Send } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport type { EngagementLikerProfile } from \"../types\";\n\nexport interface ShareMenuProps {\n  /** Pre-loaded recent / suggested users for sharing. */\n  users: EngagementLikerProfile[];\n  /** Optional async search — called on every input change with the trimmed query.\n   * If omitted, the panel filters `users` locally by name/username. */\n  onSearch?: (query: string) => Promise<EngagementLikerProfile[]>;\n  /** Fired when the user selects someone to share with. */\n  onShareTo: (user: EngagementLikerProfile) => void;\n  /** Heading label. Default \"Share with…\". */\n  heading?: string;\n  /** Search input placeholder. Default \"Search people…\". */\n  searchPlaceholder?: string;\n  /** Empty state when search has no results. Default \"No matches.\" */\n  emptyLabel?: string;\n  /** Hide-button label (kasder's \"Gizle\"). */\n  onClose?: () => void;\n  /** Hide-button label text. Default \"Hide\". */\n  closeLabel?: string;\n  className?: string;\n}\n\nfunction initials(name: string): string {\n  return (\n    name\n      .trim()\n      .split(/\\s+/)\n      .map((p) => p[0]?.toUpperCase() ?? \"\")\n      .slice(0, 2)\n      .join(\"\") || \"?\"\n  );\n}\n\nfunction ShareMenuInner({\n  users,\n  onSearch,\n  onShareTo,\n  heading = \"Share with…\",\n  searchPlaceholder = \"Search people…\",\n  emptyLabel = \"No matches.\",\n  onClose,\n  closeLabel = \"Hide\",\n  className,\n}: ShareMenuProps) {\n  const [query, setQuery] = useState(\"\");\n  const [searchResults, setSearchResults] = useState<EngagementLikerProfile[] | null>(\n    null,\n  );\n\n  const visibleUsers = useMemo<EngagementLikerProfile[]>(() => {\n    if (searchResults !== null) return searchResults;\n    const q = query.trim().toLowerCase();\n    if (!q) return users;\n    return users.filter(\n      (u) =>\n        u.name.toLowerCase().includes(q) ||\n        (u.username ? u.username.toLowerCase().includes(q) : false),\n    );\n  }, [searchResults, query, users]);\n\n  const handleQueryChange = async (next: string) => {\n    setQuery(next);\n    if (!onSearch) {\n      setSearchResults(null);\n      return;\n    }\n    if (!next.trim()) {\n      setSearchResults(null);\n      return;\n    }\n    const results = await onSearch(next);\n    setSearchResults(results);\n  };\n\n  return (\n    <div className={cn(\"flex flex-col gap-2\", className)}>\n      <div className=\"flex items-center justify-between gap-2\">\n        <span className=\"text-sm font-semibold\">{heading}</span>\n        {onClose ? (\n          <Button\n            type=\"button\"\n            variant=\"ghost\"\n            size=\"sm\"\n            onClick={onClose}\n            className=\"h-7 px-2 text-xs\"\n          >\n            {closeLabel}\n          </Button>\n        ) : null}\n      </div>\n      <div className=\"relative\">\n        <Search\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground\"\n        />\n        <Input\n          value={query}\n          onChange={(e) => {\n            void handleQueryChange(e.target.value);\n          }}\n          placeholder={searchPlaceholder}\n          className=\"h-9 pl-8\"\n        />\n      </div>\n      <ul className=\"-mx-1 max-h-64 overflow-y-auto\" role=\"list\">\n        {visibleUsers.length === 0 ? (\n          <li\n            className=\"px-3 py-6 text-center text-xs text-muted-foreground\"\n            role=\"status\"\n          >\n            {emptyLabel}\n          </li>\n        ) : (\n          visibleUsers.map((user) => (\n            <li key={user.id}>\n              <button\n                type=\"button\"\n                onClick={() => onShareTo(user)}\n                className=\"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors hover:bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n              >\n                <Avatar className=\"h-8 w-8 shrink-0\">\n                  {user.avatar ? (\n                    <AvatarImage src={user.avatar} alt=\"\" />\n                  ) : null}\n                  <AvatarFallback className=\"text-[10px]\">\n                    {initials(user.name)}\n                  </AvatarFallback>\n                </Avatar>\n                <div className=\"min-w-0 flex-1\">\n                  <div className=\"truncate text-sm font-medium\">\n                    {user.name}\n                  </div>\n                  {user.username ? (\n                    <div className=\"truncate text-xs text-muted-foreground\">\n                      @{user.username}\n                    </div>\n                  ) : null}\n                </div>\n                <Send\n                  aria-hidden=\"true\"\n                  className=\"h-4 w-4 shrink-0 text-muted-foreground\"\n                />\n              </button>\n            </li>\n          ))\n        )}\n      </ul>\n    </div>\n  );\n}\n\nexport const ShareMenu = memo(ShareMenuInner);\nShareMenu.displayName = \"ShareMenu\";\n",
      "type": "registry:component",
      "target": "components/engagement-bar/parts/share-menu.tsx"
    },
    {
      "path": "src/registry/components/data/engagement-bar/parts/reaction-picker.tsx",
      "content": "\"use client\";\n\nimport { memo, useCallback, useRef } from \"react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport type {\n  EngagementBarLabels,\n  EngagementReactionKind,\n} from \"../types\";\n\nexport interface ReactionPickerProps {\n  /** Ordered kind catalog. Picker renders one button per kind in order. */\n  kinds: EngagementReactionKind[];\n  /**\n   * Live merged counts per Q-PP-4 source-of-truth rule. Parent computes\n   * `state.reactionCounts[k.key] ?? k.count` for each kind and passes the\n   * resolved map here. Picker reads this directly — does not consult\n   * `kinds[i].count` for display (that's seed-only).\n   */\n  mergedCounts: Record<string, number>;\n  /** Viewer's currently-selected kind key, or null. Highlights matching kind. */\n  viewerReaction: string | null;\n  /** Fires when viewer picks a kind, or null via the Remove button. */\n  onSelect: (kind: string | null) => void;\n  /** Required, fully-resolved labels (no undefined). */\n  labels: Required<Omit<EngagementBarLabels, \"formatCount\">>;\n  className?: string;\n}\n\nfunction ReactionPickerInner({\n  kinds,\n  mergedCounts,\n  viewerReaction,\n  onSelect,\n  labels,\n  className,\n}: ReactionPickerProps) {\n  const buttonsRef = useRef<Array<HTMLButtonElement | null>>([]);\n  const showRemove = viewerReaction !== null;\n  const buttonCount = kinds.length + (showRemove ? 1 : 0);\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent<HTMLButtonElement>, index: number) => {\n      if (e.key === \"ArrowRight\") {\n        e.preventDefault();\n        const next = (index + 1) % buttonCount;\n        buttonsRef.current[next]?.focus();\n      } else if (e.key === \"ArrowLeft\") {\n        e.preventDefault();\n        const prev = (index - 1 + buttonCount) % buttonCount;\n        buttonsRef.current[prev]?.focus();\n      }\n    },\n    [buttonCount],\n  );\n\n  return (\n    <div\n      role=\"group\"\n      aria-label={labels.reactionPickerLabel}\n      className={cn(\"flex items-center gap-1 p-1\", className)}\n    >\n      {kinds.map((kind, index) => {\n        const isSelected = kind.key === viewerReaction;\n        const count = mergedCounts[kind.key] ?? 0;\n        return (\n          <button\n            key={kind.key}\n            ref={(el) => {\n              buttonsRef.current[index] = el;\n            }}\n            type=\"button\"\n            aria-label={kind.label}\n            aria-pressed={isSelected}\n            onClick={() => onSelect(kind.key)}\n            onKeyDown={(e) => handleKeyDown(e, index)}\n            className={cn(\n              \"flex flex-col items-center gap-0.5 rounded-md px-2 py-1.5\",\n              \"transition-transform hover:scale-125\",\n              \"focus:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n              isSelected && \"bg-accent\",\n            )}\n            style={kind.color ? { color: kind.color } : undefined}\n          >\n            <span className=\"flex h-6 w-6 items-center justify-center\">\n              {kind.icon}\n            </span>\n            {count > 0 ? (\n              <span className=\"text-[10px] font-medium tabular-nums leading-none text-muted-foreground\">\n                {count}\n              </span>\n            ) : null}\n          </button>\n        );\n      })}\n      {showRemove ? (\n        <button\n          ref={(el) => {\n            buttonsRef.current[kinds.length] = el;\n          }}\n          type=\"button\"\n          aria-label={labels.removeReaction}\n          onClick={() => onSelect(null)}\n          onKeyDown={(e) => handleKeyDown(e, kinds.length)}\n          className={cn(\n            \"flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground\",\n            \"transition-colors hover:bg-destructive/10 hover:text-destructive\",\n            \"focus:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n          )}\n        >\n          <X className=\"h-4 w-4\" />\n        </button>\n      ) : null}\n    </div>\n  );\n}\n\nexport const ReactionPicker = memo(ReactionPickerInner);\nReactionPicker.displayName = \"ReactionPicker\";\n",
      "type": "registry:component",
      "target": "components/engagement-bar/parts/reaction-picker.tsx"
    },
    {
      "path": "src/registry/components/data/engagement-bar/parts/reaction-action.tsx",
      "content": "\"use client\";\n\nimport { memo, useCallback, useMemo, useRef, useState } from \"react\";\nimport { Smile } from \"lucide-react\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { cn } from \"@/lib/utils\";\nimport type {\n  EngagementAction,\n  EngagementBarVariant,\n  EngagementBarLabels,\n  EngagementLocalAction,\n  EngagementState,\n} from \"../types\";\nimport { ReactionPicker } from \"./reaction-picker\";\n\nconst LONG_PRESS_MS = 350;\nconst POINTER_MOVE_TOLERANCE_SQ = 100; // (10px)^2\n\ntype ReactionActionData = Extract<EngagementAction, { kind: \"reaction\" }>;\n\ninterface ReactionActionProps {\n  action: ReactionActionData;\n  variant: EngagementBarVariant;\n  state: EngagementState;\n  dispatch: React.Dispatch<EngagementLocalAction>;\n  format: (n: number) => string;\n  labels: Required<Omit<EngagementBarLabels, \"formatCount\">>;\n  actionClassName?: string;\n}\n\nfunction ReactionActionInner({\n  action,\n  variant,\n  state,\n  dispatch,\n  format,\n  labels,\n  actionClassName,\n}: ReactionActionProps) {\n  const [pickerOpen, setPickerOpen] = useState(false);\n\n  // Long-press detection refs.\n  const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const longPressFired = useRef(false);\n  const downPos = useRef({ x: 0, y: 0 });\n\n  const cancelLongPress = useCallback(() => {\n    if (longPressTimer.current !== null) {\n      clearTimeout(longPressTimer.current);\n      longPressTimer.current = null;\n    }\n  }, []);\n\n  const handlePointerDown = useCallback(\n    (e: React.PointerEvent<HTMLButtonElement>) => {\n      // Capture so subsequent pointermove / pointerup events keep firing on\n      // this button even if the user drags off. Without this, long-press would\n      // continue running on a button the user has already left (per E7).\n      try {\n        e.currentTarget.setPointerCapture(e.pointerId);\n      } catch {\n        // Older browsers / non-pointer-capable inputs — silently degrade.\n      }\n      longPressFired.current = false;\n      downPos.current = { x: e.clientX, y: e.clientY };\n      longPressTimer.current = setTimeout(() => {\n        longPressFired.current = true;\n        setPickerOpen(true);\n      }, LONG_PRESS_MS);\n    },\n    [],\n  );\n\n  const handlePointerMove = useCallback(\n    (e: React.PointerEvent<HTMLButtonElement>) => {\n      if (longPressTimer.current === null) return;\n      const dx = e.clientX - downPos.current.x;\n      const dy = e.clientY - downPos.current.y;\n      if (dx * dx + dy * dy > POINTER_MOVE_TOLERANCE_SQ) {\n        cancelLongPress();\n      }\n    },\n    [cancelLongPress],\n  );\n\n  const isControlled = action.viewerReaction !== undefined;\n\n  const handleIconClick = useCallback(() => {\n    // PopoverTrigger's built-in onClick fires alongside ours (Slot mergeProps\n    // composes both handlers; Radix's `setOpen(!open)` toggle runs after our\n    // handler returns). We override Radix's toggle via `queueMicrotask` when\n    // our tap-matrix doesn't want it to fire — the microtask runs after Radix's\n    // state update commits, so our `setPickerOpen(...)` is the final value.\n    // This avoids the F-cross-13 mismatch (Base UI doesn't export PopoverAnchor)\n    // while still suppressing Radix's auto-toggle when needed.\n\n    // Long-press suppression: the timer already opened the picker. Radix's\n    // trailing click would close it; force it back open.\n    if (longPressFired.current) {\n      longPressFired.current = false;\n      queueMicrotask(() => setPickerOpen(true));\n      return;\n    }\n    cancelLongPress();\n\n    const viewer = state.viewerReaction;\n    const clearOnTap = action.clearOnTap ?? true;\n\n    // F-01 lock matrix:\n    //   viewer = null  →  open picker (any clearOnTap) — Radix toggles for us\n    //   viewer = set, clearOnTap = true  →  clear (dispatch null + microtask\n    //     onSelect(null)) — Radix toggles open, we override to closed\n    //   viewer = set, clearOnTap = false →  open picker — Radix toggles for us\n    if (viewer !== null && clearOnTap) {\n      if (!isControlled) {\n        dispatch({ kind: \"reaction-select\", reactionKind: null });\n      }\n      // Defense 1 — microtask-deferred consumer notify + override Radix's\n      // auto-toggle. Both queued in the same microtask for consistency.\n      queueMicrotask(() => {\n        action.onSelect?.(null);\n        setPickerOpen(false);\n      });\n    }\n    // Else: Radix's PopoverTrigger toggle handles opening the picker. We\n    // don't call setPickerOpen here.\n  }, [cancelLongPress, state.viewerReaction, action, isControlled, dispatch]);\n\n  const handlePick = useCallback(\n    (kind: string | null) => {\n      setPickerOpen(false);\n      // No-op if same kind (matches reducer's same-kind guard at line 187).\n      if (kind === state.viewerReaction) return;\n      if (!isControlled) {\n        dispatch({ kind: \"reaction-select\", reactionKind: kind });\n      }\n      // Defense 1 — microtask-deferred consumer notify.\n      queueMicrotask(() => {\n        action.onSelect?.(kind);\n      });\n    },\n    [state.viewerReaction, isControlled, dispatch, action],\n  );\n\n  // Resolve display values per Q-PP-5.\n  const currentKind = action.kinds.find((k) => k.key === state.viewerReaction);\n  const displayColor = currentKind?.color;\n  const triggerAriaLabel = currentKind?.label ?? labels.react;\n\n  // Q-PP-4 source-of-truth — merged counts for the picker.\n  const mergedCounts = useMemo<Record<string, number>>(() => {\n    const live = state.reactionCounts ?? {};\n    return action.kinds.reduce<Record<string, number>>((acc, k) => {\n      acc[k.key] = live[k.key] ?? k.count;\n      return acc;\n    }, {});\n  }, [state.reactionCounts, action.kinds]);\n\n  // Q-PP-5 visibility rule — count hides only when totalCount===0 AND viewer===null.\n  const totalCount = state.reactionTotalCount ?? 0;\n  const showCount = totalCount > 0 || state.viewerReaction !== null;\n  const splitCount = !!action.onCountClick;\n\n  const iconSizeClass = variant === \"compact\" ? \"h-4 w-4\" : \"h-5 w-5\";\n\n  // Icon node — viewer's current kind icon when set, neutral Smile otherwise.\n  const iconNode = currentKind ? (\n    <span\n      className={cn(\"flex items-center justify-center\", iconSizeClass)}\n      style={displayColor ? { color: displayColor } : undefined}\n    >\n      {currentKind.icon}\n    </span>\n  ) : (\n    <Smile className={cn(iconSizeClass, \"transition-transform\")} />\n  );\n\n  const countButton =\n    splitCount && showCount ? (\n      <button\n        type=\"button\"\n        onClick={action.onCountClick}\n        aria-label={labels.openReactionsPanel}\n        className={cn(\n          \"rounded text-sm font-medium tabular-nums text-foreground transition-colors\",\n          \"hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n        )}\n      >\n        {format(totalCount)}\n      </button>\n    ) : null;\n\n  // F-cross-13 deeper: Base UI's PopoverTrigger doesn't accept `asChild` (it\n  // uses a `render` prop pattern instead). Radix DOES accept asChild. Cross-\n  // compatible solution: render PopoverTrigger DIRECTLY as the button (both\n  // libraries render it as a `<button>` by default + pass through props). We\n  // drop our `<button>` wrapper + spread the button props onto PopoverTrigger.\n  // `triggerButton` is inlined here as PopoverTrigger's children.\n  const popover = (\n    <Popover open={pickerOpen} onOpenChange={setPickerOpen}>\n      <PopoverTrigger\n        type=\"button\"\n        aria-pressed={state.viewerReaction !== null}\n        aria-label={triggerAriaLabel}\n        onPointerDown={handlePointerDown}\n        onPointerMove={handlePointerMove}\n        onPointerUp={cancelLongPress}\n        onPointerCancel={cancelLongPress}\n        onClick={handleIconClick}\n        className={cn(\n          \"inline-flex h-9 items-center gap-2 rounded-md px-2 text-sm font-medium transition-colors\",\n          \"hover:bg-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n          \"select-none touch-none\",\n          state.viewerReaction !== null && \"text-foreground\",\n          // E10 — non-split mode merges actionClassName into the trigger button.\n          !splitCount && actionClassName,\n        )}\n      >\n        {iconNode}\n        {showCount && !splitCount ? (\n          <span className=\"text-sm font-medium tabular-nums\" aria-live=\"polite\">\n            {format(totalCount)}\n          </span>\n        ) : null}\n      </PopoverTrigger>\n      <PopoverContent className=\"w-auto\" align=\"start\">\n        <ReactionPicker\n          kinds={action.kinds}\n          mergedCounts={mergedCounts}\n          viewerReaction={state.viewerReaction}\n          onSelect={handlePick}\n          labels={labels}\n        />\n      </PopoverContent>\n    </Popover>\n  );\n\n  // Variant layouts mirror like-action's structure.\n  // E10 — non-split returns the popover bare (button carries actionClassName);\n  // split wraps in an outer div with actionClassName to host both popover + countButton.\n  if (variant === \"stacked\") {\n    if (splitCount) {\n      return (\n        <div\n          className={cn(\n            \"flex flex-col items-center gap-0.5\",\n            actionClassName,\n          )}\n        >\n          {popover}\n          {countButton}\n        </div>\n      );\n    }\n    return popover;\n  }\n\n  // default + compact horizontal.\n  if (splitCount) {\n    return (\n      <div className={cn(\"flex items-center gap-2 pr-2\", actionClassName)}>\n        {popover}\n        {countButton}\n      </div>\n    );\n  }\n\n  return popover;\n}\n\nexport const ReactionAction = memo(ReactionActionInner);\nReactionAction.displayName = \"ReactionAction\";\n",
      "type": "registry:component",
      "target": "components/engagement-bar/parts/reaction-action.tsx"
    },
    {
      "path": "src/registry/components/data/engagement-bar/hooks/use-engagement-state.ts",
      "content": "\"use client\";\n\nimport { useEffect, useMemo, useReducer, useRef } from \"react\";\nimport type {\n  EngagementAction,\n  EngagementDelta,\n  EngagementLocalAction,\n  EngagementState,\n  Subscribe,\n} from \"../types\";\n\ninterface ControlledFlags {\n  liked: boolean;\n  likeCount: boolean;\n  bookmarked: boolean;\n  commentCount: boolean;\n  shareCount: boolean;\n  viewCount: boolean;\n  /**\n   * Controlled if `action.viewerReaction !== undefined` (host explicitly passes the\n   * value, either `string` or `null`). Reaction `kinds[i].count` is NEVER controlled\n   * per Q-PP-4 source-of-truth rule — state wins after init.\n   */\n  viewerReaction: boolean;\n}\n\nconst INITIAL_EMPTY_STATE: EngagementState = {\n  liked: false,\n  likeCount: 0,\n  commentCount: 0,\n  shareCount: null,\n  viewCount: null,\n  bookmarked: false,\n  reactionCounts: null,\n  reactionTotalCount: null,\n  viewerReaction: null,\n};\n\n/**\n * Pure derivation of the initial state shape from the actions array.\n * Each action contributes its initial values to the corresponding field;\n * absent actions use the default (0 / null / false).\n */\nexport function deriveStateFromActions(\n  actions: EngagementAction[],\n): EngagementState {\n  const next: EngagementState = { ...INITIAL_EMPTY_STATE };\n  for (const action of actions) {\n    switch (action.kind) {\n      case \"like\":\n        next.liked = action.liked ?? false;\n        next.likeCount = action.count;\n        break;\n      case \"comment\":\n        next.commentCount = action.count;\n        break;\n      case \"share\":\n        next.shareCount = action.count ?? null;\n        break;\n      case \"bookmark\":\n        next.bookmarked = action.bookmarked ?? false;\n        break;\n      case \"view-count\":\n        next.viewCount = action.count;\n        break;\n      case \"reaction\":\n        // Per Q-PP-4 source-of-truth rule — kinds[i].count is the SEED only.\n        next.reactionCounts = action.kinds.reduce<Record<string, number>>(\n          (acc, k) => {\n            acc[k.key] = k.count;\n            return acc;\n          },\n          {},\n        );\n        next.reactionTotalCount = action.totalCount;\n        next.viewerReaction = action.viewerReaction ?? null;\n        break;\n      // \"custom\" doesn't contribute to internal state — host owns active/count\n    }\n  }\n  return next;\n}\n\nfunction deriveControlledFlags(actions: EngagementAction[]): ControlledFlags {\n  const flags: ControlledFlags = {\n    liked: false,\n    likeCount: false,\n    bookmarked: false,\n    commentCount: false,\n    shareCount: false,\n    viewCount: false,\n    viewerReaction: false,\n  };\n  for (const action of actions) {\n    switch (action.kind) {\n      case \"like\":\n        if (action.liked !== undefined) flags.liked = true;\n        // count is always present for like — hybrid: count is \"controlled\"\n        // only if the host updates it on each render. We treat count as\n        // controlled iff liked is controlled (paired contract).\n        flags.likeCount = flags.liked;\n        break;\n      case \"bookmark\":\n        if (action.bookmarked !== undefined) flags.bookmarked = true;\n        break;\n      case \"reaction\":\n        // `viewerReaction` is optional-controlled per the like pattern.\n        // `kinds[i].count` is NEVER controlled — state owns counts after init.\n        if (action.viewerReaction !== undefined) flags.viewerReaction = true;\n        break;\n      // other action kinds don't have separate controlled flags here —\n      // their counts always come from props directly via the resolved state.\n    }\n  }\n  return flags;\n}\n\n/** Pure reducer — exported for hosts driving their own state. */\nexport function engagementReducer(\n  state: EngagementState,\n  action: EngagementLocalAction,\n): EngagementState {\n  switch (action.kind) {\n    case \"like-toggle\": {\n      const nextLiked = !state.liked;\n      return {\n        ...state,\n        liked: nextLiked,\n        likeCount: Math.max(0, state.likeCount + (nextLiked ? 1 : -1)),\n      };\n    }\n    case \"bookmark-toggle\":\n      return { ...state, bookmarked: !state.bookmarked };\n    case \"subscribe-delta\": {\n      const d = action.delta;\n      switch (d.kind) {\n        case \"like-changed\":\n          return {\n            ...state,\n            likeCount: d.count,\n            liked: d.liked ?? state.liked,\n          };\n        case \"comment-count-changed\":\n          return { ...state, commentCount: d.count };\n        case \"share-count-changed\":\n          return { ...state, shareCount: d.count };\n        case \"view-count-changed\":\n          return { ...state, viewCount: d.count };\n        case \"bookmark-changed\":\n          return { ...state, bookmarked: d.bookmarked };\n        case \"liker-added\":\n        case \"liker-removed\":\n          // These deltas inform the likersPreview slot host, not the bar's\n          // internal state. Bar state unchanged.\n          return state;\n        case \"reaction-changed\":\n          // Server-authoritative replace. Counts + total + viewer all swap to\n          // the delta payload. Viewer is optional in the delta — fall back to\n          // current state when absent (so the server can update just counts).\n          return {\n            ...state,\n            reactionCounts: d.counts,\n            reactionTotalCount: d.totalCount,\n            viewerReaction:\n              d.viewerReaction !== undefined\n                ? d.viewerReaction\n                : state.viewerReaction,\n          };\n        case \"reactor-added\":\n        case \"reactor-removed\":\n          // Pass-through per Q-PP-4 — the bar does not maintain a reactor list.\n          // Hosts that want a live reactors strip consume the delta via\n          // onSubscribeDelta and render into the `reactionsPreview` slot.\n          return state;\n      }\n      return state;\n    }\n    case \"reaction-select\": {\n      // Optimistic per-kind tally update. State holds the live count map; a\n      // null reactionKind clears the viewer + decrements the old kind.\n      if (state.reactionCounts === null || state.reactionTotalCount === null) {\n        // No reaction action present — dispatch is a no-op.\n        return state;\n      }\n      const current = state.viewerReaction;\n      const next = action.reactionKind;\n      if (current === next) return state; // same kind tap = no-op\n\n      const counts = { ...state.reactionCounts };\n      let total = state.reactionTotalCount;\n\n      if (current !== null) {\n        // Decrement old kind. Counts can never go below 0 (defensive — host\n        // backend should guarantee this, but optimistic ops shouldn't crash on drift).\n        counts[current] = Math.max(0, (counts[current] ?? 0) - 1);\n        total = Math.max(0, total - 1);\n      }\n      if (next !== null) {\n        counts[next] = (counts[next] ?? 0) + 1;\n        total = total + 1;\n      }\n      return {\n        ...state,\n        reactionCounts: counts,\n        reactionTotalCount: total,\n        viewerReaction: next,\n      };\n    }\n    case \"reset\":\n      return action.next;\n  }\n}\n\nfunction isControlledForDelta(\n  delta: EngagementDelta,\n  controlled: ControlledFlags,\n): boolean {\n  switch (delta.kind) {\n    case \"like-changed\":\n      return controlled.liked || controlled.likeCount;\n    case \"bookmark-changed\":\n      return controlled.bookmarked;\n    case \"comment-count-changed\":\n      return controlled.commentCount;\n    case \"share-count-changed\":\n      return controlled.shareCount;\n    case \"view-count-changed\":\n      return controlled.viewCount;\n    case \"liker-added\":\n    case \"liker-removed\":\n      return false; // these never patch internal state anyway\n    case \"reaction-changed\":\n      // Counts + total are NEVER controlled per Q-PP-4 — always patch from server.\n      // Viewer field also patches; effective-state useMemo overlays the host's\n      // controlled `viewerReaction` if `controlled.viewerReaction === true`.\n      return false;\n    case \"reactor-added\":\n    case \"reactor-removed\":\n      // Never patch internal state (pass-through to host's reactionsPreview slot).\n      // Dispatch is harmless (reducer returns state unchanged) — matches the\n      // existing `liker-added` / `liker-removed` convention.\n      return false;\n  }\n}\n\nexport interface UseEngagementStateOptions {\n  actions: EngagementAction[];\n  subscribe?: Subscribe<EngagementDelta>;\n  onSubscribeDelta?: (delta: EngagementDelta) => void;\n}\n\nexport interface UseEngagementStateResult {\n  state: EngagementState;\n  dispatch: React.Dispatch<EngagementLocalAction>;\n  controlled: ControlledFlags;\n}\n\n/**\n * Internal-leaning hook (not re-exported) that wires:\n *  - useReducer over engagementReducer\n *  - per-render controlled-vs-uncontrolled flag computation\n *  - effective-state merge (controlled props win per-field)\n *  - subscription effect that fires onSubscribeDelta and patches uncontrolled fields\n */\nexport function useEngagementState(\n  opts: UseEngagementStateOptions,\n): UseEngagementStateResult {\n  const initial = useMemo(\n    () => deriveStateFromActions(opts.actions),\n    // intentionally only on mount — useReducer ignores initial changes after mount,\n    // but we still memoize so the closure is stable.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [],\n  );\n  const [internalState, dispatch] = useReducer(engagementReducer, initial);\n\n  const controlled = useMemo(\n    () => deriveControlledFlags(opts.actions),\n    [opts.actions],\n  );\n\n  // Effective state: controlled props win per-field.\n  const state = useMemo<EngagementState>(() => {\n    const likeAction = opts.actions.find((a) => a.kind === \"like\");\n    const bookmarkAction = opts.actions.find((a) => a.kind === \"bookmark\");\n    const commentAction = opts.actions.find((a) => a.kind === \"comment\");\n    const shareAction = opts.actions.find((a) => a.kind === \"share\");\n    const viewCountAction = opts.actions.find((a) => a.kind === \"view-count\");\n    const reactionAction = opts.actions.find((a) => a.kind === \"reaction\");\n\n    return {\n      liked:\n        controlled.liked && likeAction?.kind === \"like\"\n          ? (likeAction.liked ?? false)\n          : internalState.liked,\n      likeCount:\n        controlled.likeCount && likeAction?.kind === \"like\"\n          ? likeAction.count\n          : internalState.likeCount,\n      commentCount:\n        commentAction?.kind === \"comment\"\n          ? commentAction.count\n          : internalState.commentCount,\n      shareCount:\n        shareAction?.kind === \"share\"\n          ? (shareAction.count ?? null)\n          : internalState.shareCount,\n      viewCount:\n        viewCountAction?.kind === \"view-count\"\n          ? viewCountAction.count\n          : internalState.viewCount,\n      bookmarked:\n        controlled.bookmarked && bookmarkAction?.kind === \"bookmark\"\n          ? (bookmarkAction.bookmarked ?? false)\n          : internalState.bookmarked,\n      // Per Q-PP-4 source-of-truth rule: `reactionCounts` and `reactionTotalCount`\n      // are NEVER controlled — state owns them. Renderers read\n      // `state.reactionCounts[k.key] ?? k.count` per kind (action.kinds is the seed).\n      reactionCounts: internalState.reactionCounts,\n      reactionTotalCount: internalState.reactionTotalCount,\n      // `viewerReaction` follows the like-pattern: optional-controlled. When the\n      // host passes `action.viewerReaction !== undefined` (string OR null), host\n      // wins. Otherwise internal state.\n      viewerReaction:\n        controlled.viewerReaction && reactionAction?.kind === \"reaction\"\n          ? (reactionAction.viewerReaction ?? null)\n          : internalState.viewerReaction,\n    };\n  }, [internalState, opts.actions, controlled]);\n\n  // Refs keep the subscription effect stable on `subscribe` identity only.\n  // Re-running it on `controlled` or `onSubscribeDelta` change would drop deltas\n  // in flight between cleanup + re-call.\n  const controlledRef = useRef(controlled);\n  useEffect(() => {\n    controlledRef.current = controlled;\n  });\n  const onSubscribeDeltaRef = useRef(opts.onSubscribeDelta);\n  useEffect(() => {\n    onSubscribeDeltaRef.current = opts.onSubscribeDelta;\n  });\n\n  const subscribe = opts.subscribe;\n  useEffect(() => {\n    if (!subscribe) return;\n    const unsub = subscribe((delta) => {\n      onSubscribeDeltaRef.current?.(delta);\n      if (!isControlledForDelta(delta, controlledRef.current)) {\n        dispatch({ kind: \"subscribe-delta\", delta });\n      }\n    });\n    return unsub;\n  }, [subscribe]);\n\n  // Defense 2 (structural resync guard) per Q-PP-3 — when the host transitions\n  // `viewerReaction` from uncontrolled → controlled (or changes the controlled\n  // value), the internal mirror can be stale relative to the effective overlay.\n  // The next `reaction-select` dispatch would read stale internal state and\n  // decrement the wrong kind. This effect syncs the internal `viewerReaction`\n  // to the controlled value without touching counts (counts stay server / state\n  // owned per Q-PP-4 source-of-truth).\n  const reactionAction = opts.actions.find((a) => a.kind === \"reaction\");\n  const controlledViewerReaction =\n    controlled.viewerReaction && reactionAction?.kind === \"reaction\"\n      ? (reactionAction.viewerReaction ?? null)\n      : undefined;\n  const internalStateRef = useRef(internalState);\n  useEffect(() => {\n    internalStateRef.current = internalState;\n  });\n  useEffect(() => {\n    if (controlledViewerReaction === undefined) return;\n    const current = internalStateRef.current;\n    if (controlledViewerReaction === current.viewerReaction) return;\n    dispatch({\n      kind: \"reset\",\n      next: { ...current, viewerReaction: controlledViewerReaction },\n    });\n  }, [controlledViewerReaction]);\n\n  return { state, dispatch, controlled };\n}\n",
      "type": "registry:component",
      "target": "components/engagement-bar/hooks/use-engagement-state.ts"
    },
    {
      "path": "src/registry/components/data/engagement-bar/lib/format-count.ts",
      "content": "/**\n * Humanizes engagement counts:\n *   0           → \"0\"\n *   999         → \"999\"\n *   1_000       → \"1k\"\n *   1_234       → \"1.2k\"\n *   12_345      → \"12k\"      (truncated, no decimal once past 10k)\n *   1_234_567   → \"1.2m\"\n *   1_234_567_890 → \"1.2b\"\n *\n * Locale-agnostic ('.' decimal separator). Hosts wanting locale-specific\n * formatting pass `labels.formatCount` to override entirely.\n */\nexport function formatEngagementCount(n: number): string {\n  if (n < 0) return \"0\";\n  if (n < 1_000) return String(n);\n  if (n < 10_000)\n    return (n / 1_000).toFixed(1).replace(/\\.0$/, \"\") + \"k\";\n  if (n < 1_000_000) return Math.floor(n / 1_000) + \"k\";\n  if (n < 1_000_000_000)\n    return (n / 1_000_000).toFixed(1).replace(/\\.0$/, \"\") + \"m\";\n  return (n / 1_000_000_000).toFixed(1).replace(/\\.0$/, \"\") + \"b\";\n}\n",
      "type": "registry:component",
      "target": "components/engagement-bar/lib/format-count.ts"
    }
  ],
  "categories": [
    "data"
  ],
  "type": "registry:block"
}