{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "feature-voting",
  "type": "registry:ui",
  "description": "List of features with up-vote support, optional sorting by vote count, and controlled or uncontrolled state",
  "dependencies": [
    "@radix-ui/react-use-controllable-state"
  ],
  "files": [
    {
      "path": "registry/default/ui/feature-voting.tsx",
      "content": "\"use client\"\n\nimport {\n  Children,\n  createContext,\n  isValidElement,\n  useCallback,\n  useContext,\n  useMemo,\n  type ComponentProps,\n  type MouseEvent,\n} from \"react\"\nimport { useControllableState } from \"@radix-ui/react-use-controllable-state\"\n\n/* -----------------------------------------------------------------------------\n * Types\n * -------------------------------------------------------------------------- */\n\nexport type FeatureVotingValue = Record<string, number>\n\nexport interface FeatureVotingRootProps\n  extends Omit<ComponentProps<\"ul\">, \"defaultValue\"> {\n  /** Current vote counts (controlled) */\n  value?: FeatureVotingValue\n  /** Initial vote counts (uncontrolled) */\n  defaultValue?: FeatureVotingValue\n  /** Callback when votes change */\n  onValueChange?: (value: FeatureVotingValue) => void\n  /** Set of feature IDs the current user has voted for */\n  votedFeatures?: Set<string>\n  /** Default voted features (uncontrolled) */\n  defaultVotedFeatures?: Set<string>\n  /** Callback when user votes/unvotes */\n  onVotedFeaturesChange?: (votedFeatures: Set<string>) => void\n  /** Whether voting is disabled */\n  disabled?: boolean\n}\n\nexport interface FeatureVotingItemProps extends ComponentProps<\"li\"> {\n  /** Unique identifier for this feature */\n  value: string\n  /** Whether this specific item is disabled */\n  disabled?: boolean\n}\n\nexport type FeatureVotingTriggerProps = ComponentProps<\"button\">\n\nexport type FeatureVotingCountProps = ComponentProps<\"span\">\n\nexport type FeatureVotingTitleProps = ComponentProps<\"span\">\n\nexport type FeatureVotingDescriptionProps = ComponentProps<\"span\">\n\nexport interface FeatureVotingGroupProps extends ComponentProps<\"div\"> {\n  /** Sort items by vote count */\n  sortBy?: \"votes-asc\" | \"votes-desc\" | \"none\"\n}\n\n/* -----------------------------------------------------------------------------\n * Context\n * -------------------------------------------------------------------------- */\n\ninterface FeatureVotingContextValue {\n  votes: FeatureVotingValue\n  votedFeatures: Set<string>\n  disabled: boolean\n  vote: (featureId: string) => void\n  unvote: (featureId: string) => void\n  toggleVote: (featureId: string) => void\n  getVoteCount: (featureId: string) => number\n  hasVoted: (featureId: string) => boolean\n}\n\nconst FeatureVotingContext = createContext<FeatureVotingContextValue | null>(\n  null\n)\n\nfunction useFeatureVotingContext() {\n  const context = useContext(FeatureVotingContext)\n  if (!context) {\n    throw new Error(\n      \"FeatureVoting components must be used within FeatureVoting.Root\"\n    )\n  }\n  return context\n}\n\ninterface FeatureVotingItemContextValue {\n  featureId: string\n  disabled: boolean\n}\n\nconst FeatureVotingItemContext =\n  createContext<FeatureVotingItemContextValue | null>(null)\n\nfunction useFeatureVotingItemContext() {\n  const context = useContext(FeatureVotingItemContext)\n  if (!context) {\n    throw new Error(\n      \"FeatureVoting.Item sub-components must be used within FeatureVoting.Item\"\n    )\n  }\n  return context\n}\n\n/* -----------------------------------------------------------------------------\n * Root\n * -------------------------------------------------------------------------- */\n\nfunction FeatureVotingRoot({\n  value: controlledValue,\n  defaultValue = {},\n  onValueChange,\n  votedFeatures: controlledVotedFeatures,\n  defaultVotedFeatures,\n  onVotedFeaturesChange,\n  disabled = false,\n  children,\n  ...props\n}: FeatureVotingRootProps) {\n  const [votes, setVotes] = useControllableState<FeatureVotingValue>({\n    prop: controlledValue,\n    defaultProp: defaultValue,\n    onChange: onValueChange,\n  })\n\n  const [votedFeaturesArray, setVotedFeaturesArray] = useControllableState({\n    prop: controlledVotedFeatures\n      ? Array.from(controlledVotedFeatures)\n      : undefined,\n    defaultProp: defaultVotedFeatures ? Array.from(defaultVotedFeatures) : [],\n    onChange: (arr) => onVotedFeaturesChange?.(new Set(arr)),\n  })\n\n  const votedFeatures = useMemo(\n    () => new Set(votedFeaturesArray),\n    [votedFeaturesArray]\n  )\n\n  const vote = useCallback(\n    (featureId: string) => {\n      if (disabled || votedFeatures.has(featureId)) {\n        return\n      }\n\n      setVotes((prev) => ({\n        ...prev,\n        [featureId]: (prev?.[featureId] ?? 0) + 1,\n      }))\n      setVotedFeaturesArray((prev) => [...(prev ?? []), featureId])\n    },\n    [disabled, votedFeatures, setVotes, setVotedFeaturesArray]\n  )\n\n  const unvote = useCallback(\n    (featureId: string) => {\n      if (disabled || !votedFeatures.has(featureId)) {\n        return\n      }\n\n      setVotes((prev) => ({\n        ...prev,\n        [featureId]: Math.max((prev?.[featureId] ?? 0) - 1, 0),\n      }))\n      setVotedFeaturesArray((prev) =>\n        (prev ?? []).filter((id) => id !== featureId)\n      )\n    },\n    [disabled, votedFeatures, setVotes, setVotedFeaturesArray]\n  )\n\n  const toggleVote = useCallback(\n    (featureId: string) => {\n      if (votedFeatures.has(featureId)) {\n        unvote(featureId)\n      } else {\n        vote(featureId)\n      }\n    },\n    [votedFeatures, vote, unvote]\n  )\n\n  const getVoteCount = useCallback(\n    (featureId: string) => votes?.[featureId] ?? 0,\n    [votes]\n  )\n\n  const hasVoted = useCallback(\n    (featureId: string) => votedFeatures.has(featureId),\n    [votedFeatures]\n  )\n\n  const contextValue = useMemo(\n    () => ({\n      votes: votes ?? {},\n      votedFeatures,\n      disabled,\n      vote,\n      unvote,\n      toggleVote,\n      getVoteCount,\n      hasVoted,\n    }),\n    [\n      votes,\n      votedFeatures,\n      disabled,\n      vote,\n      unvote,\n      toggleVote,\n      getVoteCount,\n      hasVoted,\n    ]\n  )\n\n  return (\n    <FeatureVotingContext.Provider value={contextValue}>\n      <ul\n        aria-label=\"Feature voting list\"\n        data-disabled={disabled ? true : undefined}\n        {...props}\n      >\n        {children}\n      </ul>\n    </FeatureVotingContext.Provider>\n  )\n}\n\n/* -----------------------------------------------------------------------------\n * Group (optional sorting wrapper)\n * -------------------------------------------------------------------------- */\n\nfunction FeatureVotingGroup({\n  sortBy = \"none\",\n  children,\n  ...props\n}: FeatureVotingGroupProps) {\n  const { votes } = useFeatureVotingContext()\n\n  const sortedChildren = useMemo(() => {\n    if (sortBy === \"none\") {\n      return children\n    }\n\n    const childArray = Children.toArray(children)\n\n    return childArray.sort((a, b) => {\n      if (!(isValidElement(a) && isValidElement(b))) {\n        return 0\n      }\n\n      const aValue = (a.props as FeatureVotingItemProps).value\n      const bValue = (b.props as FeatureVotingItemProps).value\n      const aVotes = votes[aValue] ?? 0\n      const bVotes = votes[bValue] ?? 0\n\n      return sortBy === \"votes-desc\" ? bVotes - aVotes : aVotes - bVotes\n    })\n  }, [children, sortBy, votes])\n\n  return <div {...props}>{sortedChildren}</div>\n}\n\n/* -----------------------------------------------------------------------------\n * Item\n * -------------------------------------------------------------------------- */\n\nfunction FeatureVotingItem({\n  value,\n  disabled: itemDisabled = false,\n  children,\n  ...props\n}: FeatureVotingItemProps) {\n  const {\n    disabled: rootDisabled,\n    hasVoted,\n    getVoteCount,\n  } = useFeatureVotingContext()\n  const disabled = rootDisabled || itemDisabled\n  const voted = hasVoted(value)\n  const voteCount = getVoteCount(value)\n\n  const itemContextValue = useMemo(\n    () => ({ featureId: value, disabled }),\n    [value, disabled]\n  )\n\n  return (\n    <FeatureVotingItemContext.Provider value={itemContextValue}>\n      <li\n        data-disabled={disabled ? true : undefined}\n        data-feature={value}\n        data-slot=\"feature-voting-item\"\n        data-vote-count={voteCount}\n        data-voted={voted ? true : undefined}\n        {...props}\n      >\n        {children}\n      </li>\n    </FeatureVotingItemContext.Provider>\n  )\n}\n\n/* -----------------------------------------------------------------------------\n * Trigger\n * -------------------------------------------------------------------------- */\n\nfunction FeatureVotingTrigger({\n  children,\n  onClick,\n  ...props\n}: FeatureVotingTriggerProps) {\n  const {\n    toggleVote,\n    hasVoted,\n    disabled: rootDisabled,\n  } = useFeatureVotingContext()\n  const { featureId, disabled: itemDisabled } = useFeatureVotingItemContext()\n\n  const disabled = rootDisabled || itemDisabled\n  const voted = hasVoted(featureId)\n\n  const handleClick = useCallback(\n    (event: MouseEvent<HTMLButtonElement>) => {\n      onClick?.(event)\n      if (!(event.defaultPrevented || disabled)) {\n        toggleVote(featureId)\n      }\n    },\n    [onClick, disabled, toggleVote, featureId]\n  )\n\n  return (\n    <button\n      aria-label={voted ? \"Remove vote for feature\" : \"Vote for feature\"}\n      aria-pressed={voted}\n      data-slot=\"feature-voting-trigger\"\n      data-state={voted ? \"voted\" : \"idle\"}\n      disabled={disabled}\n      onClick={handleClick}\n      type=\"button\"\n      {...props}\n    >\n      {children}\n    </button>\n  )\n}\n\n/* -----------------------------------------------------------------------------\n * Count\n * -------------------------------------------------------------------------- */\n\nfunction FeatureVotingCount({ children, ...props }: FeatureVotingCountProps) {\n  const { getVoteCount } = useFeatureVotingContext()\n  const { featureId } = useFeatureVotingItemContext()\n\n  const count = getVoteCount(featureId)\n\n  return (\n    <span data-slot=\"feature-voting-count\" {...props}>\n      {children ?? count}\n    </span>\n  )\n}\n\n/* -----------------------------------------------------------------------------\n * Title\n * -------------------------------------------------------------------------- */\n\nfunction FeatureVotingTitle({ children, ...props }: FeatureVotingTitleProps) {\n  return (\n    <span data-slot=\"feature-voting-title\" {...props}>\n      {children}\n    </span>\n  )\n}\n\n/* -----------------------------------------------------------------------------\n * Description\n * -------------------------------------------------------------------------- */\n\nfunction FeatureVotingDescription({\n  children,\n  ...props\n}: FeatureVotingDescriptionProps) {\n  return (\n    <span data-slot=\"feature-voting-description\" {...props}>\n      {children}\n    </span>\n  )\n}\n\n/* -----------------------------------------------------------------------------\n * Hook for external access\n * -------------------------------------------------------------------------- */\n\nexport function useFeatureVoting() {\n  return useFeatureVotingContext()\n}\n\n/* -----------------------------------------------------------------------------\n * Export\n * -------------------------------------------------------------------------- */\n\nexport const FeatureVoting = {\n  Root: FeatureVotingRoot,\n  Group: FeatureVotingGroup,\n  Item: FeatureVotingItem,\n  Trigger: FeatureVotingTrigger,\n  Count: FeatureVotingCount,\n  Title: FeatureVotingTitle,\n  Description: FeatureVotingDescription,\n}\n\nexport {\n  FeatureVotingRoot,\n  FeatureVotingGroup,\n  FeatureVotingItem,\n  FeatureVotingTrigger,\n  FeatureVotingCount,\n  FeatureVotingTitle,\n  FeatureVotingDescription,\n}\n",
      "type": "registry:ui"
    }
  ]
}