{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"ticker","type":"registry:ui","title":"ticker","description":"A composable finance ticker for displaying symbols, prices and changes.","author":"Hayden Bleasel <hello@haydenbleasel.com>","dependencies":[],"devDependencies":[],"registryDependencies":["avatar"],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { createContext, memo, useContext, useMemo } from \"react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport { cn } from \"@/lib/utils\";\n\ntype TickerContextValue = {\n  formatter: Intl.NumberFormat;\n};\n\nconst DEFAULT_CURRENCY = \"USD\";\nconst DEFAULT_LOCALE = \"en-US\";\n\nconst defaultFormatter = new Intl.NumberFormat(DEFAULT_LOCALE, {\n  style: \"currency\",\n  currency: DEFAULT_CURRENCY,\n  minimumFractionDigits: 2,\n  maximumFractionDigits: 2,\n});\n\nconst TickerContext = createContext<TickerContextValue>({\n  formatter: defaultFormatter,\n});\n\nexport const useTickerContext = () => useContext(TickerContext);\n\nexport type TickerProps = HTMLAttributes<HTMLButtonElement> & {\n  currency?: string;\n  locale?: string;\n};\n\nexport const Ticker = memo(\n  ({\n    children,\n    className,\n    currency = DEFAULT_CURRENCY,\n    locale = DEFAULT_LOCALE,\n    ...props\n  }: TickerProps & { children: ReactNode }) => {\n    const formatter = useMemo(() => {\n      try {\n        return new Intl.NumberFormat(locale, {\n          style: \"currency\",\n          currency: currency.toUpperCase(),\n          minimumFractionDigits: 2,\n          maximumFractionDigits: 2,\n        });\n      } catch {\n        return defaultFormatter;\n      }\n    }, [currency, locale]);\n\n    return (\n      <TickerContext.Provider value={{ formatter }}>\n        <button\n          className={cn(\n            \"inline-flex items-center gap-1.5 whitespace-nowrap align-middle\",\n            className\n          )}\n          type=\"button\"\n          {...props}\n        >\n          {children}\n        </button>\n      </TickerContext.Provider>\n    );\n  }\n);\nTicker.displayName = \"Ticker\";\n\nexport type TickerIconProps = HTMLAttributes<HTMLImageElement> & {\n  src?: string;\n  symbol?: string;\n  asChild?: boolean;\n};\n\nexport const TickerIcon = memo(\n  ({\n    src,\n    symbol,\n    className,\n    asChild,\n    children,\n    ...props\n  }: TickerIconProps) => {\n    if (asChild) {\n      return (\n        <div\n          className={cn(\n            \"overflow-hidden rounded-full border border-border bg-muted\",\n            className\n          )}\n        >\n          {children}\n        </div>\n      );\n    }\n\n    return (\n      <Avatar className={cn(\"size-7 border border-border bg-muted\", className)}>\n        <AvatarImage src={src} {...props} />\n        <AvatarFallback className=\"font-semibold text-muted-foreground text-sm\">\n          {symbol?.slice(0, 2).toUpperCase()}\n        </AvatarFallback>\n      </Avatar>\n    );\n  }\n);\nTickerIcon.displayName = \"TickerIcon\";\n\nexport type TickerSymbolProps = HTMLAttributes<HTMLSpanElement> & {\n  symbol: string;\n};\n\nexport const TickerSymbol = memo(\n  ({ symbol, className, ...props }: TickerSymbolProps) => (\n    <span className={cn(\"font-medium\", className)} {...props}>\n      {symbol.toUpperCase()}\n    </span>\n  )\n);\nTickerSymbol.displayName = \"TickerSymbol\";\n\nexport type TickerPriceProps = HTMLAttributes<HTMLSpanElement> & {\n  price: number;\n};\n\nexport const TickerPrice = memo(\n  ({ price, className, ...props }: TickerPriceProps) => {\n    const context = useTickerContext();\n\n    const formattedPrice = useMemo(\n      () => context.formatter.format(price),\n      [price, context]\n    );\n\n    return (\n      <span className={cn(\"text-muted-foreground\", className)} {...props}>\n        {formattedPrice}\n      </span>\n    );\n  }\n);\nTickerPrice.displayName = \"TickerPrice\";\n\nexport type TickerPriceChangeProps = HTMLAttributes<HTMLSpanElement> & {\n  change: number;\n  isPercent?: boolean;\n};\n\nexport const TickerPriceChange = memo(\n  ({ change, isPercent, className, ...props }: TickerPriceChangeProps) => {\n    const isPositiveChange = useMemo(() => change >= 0, [change]);\n    const context = useTickerContext();\n\n    const changeFormatted = useMemo(() => {\n      if (isPercent) {\n        return `${change.toFixed(2)}%`;\n      }\n      return context.formatter.format(change);\n    }, [change, isPercent, context]);\n\n    return (\n      <span\n        className={cn(\n          \"flex items-center gap-0.5\",\n          isPositiveChange\n            ? \"text-green-600 dark:text-green-500\"\n            : \"text-red-600 dark:text-red-500\",\n          className\n        )}\n        {...props}\n      >\n        <svg\n          aria-labelledby=\"ticker-change-icon-title\"\n          className={isPositiveChange ? \"\" : \"rotate-180\"}\n          fill=\"currentColor\"\n          height=\"12\"\n          role=\"img\"\n          viewBox=\"0 0 24 24\"\n          width=\"12\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n        >\n          <title id=\"ticker-change-icon-title\">\n            {isPositiveChange ? \"Up icon\" : \"Down icon\"}\n          </title>\n          <path d=\"M24 22h-24l12-20z\" />\n        </svg>\n        {changeFormatted}\n      </span>\n    );\n  }\n);\nTickerPriceChange.displayName = \"TickerPriceChange\";\n","target":"components/kibo-ui/ticker/index.tsx"}],"css":{}}