{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "trade-ticket",
  "title": "Trade ticket",
  "description": "Buy or sell one of two outcomes, with amount chips and the payout worked out as you type.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/manniche/trade-ticket/trade-ticket.tsx",
      "content": "import { LayoutGroup, MotionConfig, motion } from 'motion/react'\nimport { useId, useState, type SubmitEvent } from 'react'\nimport { cn } from '@/lib/utils'\n\nexport type Outcome = { id: string; label: string; /** Price per share from 0 to 1. A share pays 1 if the outcome happens. */ price: number }\n\nexport type TradeOrder = { side: 'buy' | 'sell'; outcome: string; amount: number; shares: number }\n\nexport type TradeTicketProps = {\n  /** Two outcomes, usually yes and no. */\n  outcomes: [Outcome, Outcome]\n  /** Most someone can spend; the Max chip uses it. */\n  balance: number\n  currency?: string\n  locale?: string\n  onTrade: (order: TradeOrder) => void\n  labels?: Partial<Record<'buy' | 'sell' | 'amount' | 'toWin' | 'toGet' | 'avg' | 'trade' | 'max', string>>\n  className?: string\n}\n\nconst EN = { buy: 'Buy', sell: 'Sell', amount: 'Amount', toWin: 'To win', toGet: 'You get', avg: 'Avg. price', trade: 'Trade', max: 'Max' }\nconst CHIPS = [1, 5, 10, 100]\n\n/** A ticket for buying or selling one of two outcomes, with the payout worked out as you type. */\nexport function TradeTicket({ outcomes, balance, currency = 'USD', locale = 'en-US', onTrade, labels = {}, className }: TradeTicketProps) {\n  const t = { ...EN, ...labels }\n  const [side, setSide] = useState<'buy' | 'sell'>('buy')\n  const [pick, setPick] = useState(outcomes[0].id)\n  const [raw, setRaw] = useState('')\n  const ids = useId()\n\n  const money = new Intl.NumberFormat(locale, { style: 'currency', currency })\n  const symbol = money.formatToParts(0).find((p) => p.type === 'currency')?.value ?? ''\n  const cents = (n: number) => `${(n * 100).toFixed(1)}¢`\n  const outcome = outcomes.find((o) => o.id === pick)!\n  const amount = Math.min(balance, Math.max(0, Number(raw.replace(',', '.')) || 0))\n  const shares = outcome.price > 0 ? amount / outcome.price : 0\n  // Buying: each share pays out 1 if it happens. Selling: you get the price back now.\n  const result = side === 'buy' ? shares : amount\n\n  const add = (n: number) => setRaw(String(Math.min(balance, amount + n)))\n  const submit = (e: SubmitEvent<HTMLFormElement>) => {\n    e.preventDefault()\n    if (amount > 0) onTrade({ side, outcome: pick, amount, shares })\n  }\n\n  return (\n    <MotionConfig reducedMotion=\"user\" transition={{ type: 'spring', bounce: 0, duration: 0.35 }}>\n      <form onSubmit={submit} className={cn('w-full max-w-sm overflow-hidden rounded-3xl border bg-card text-card-foreground', className)}>\n        <LayoutGroup id={`${ids}-side`}>\n          <div role=\"group\" aria-label={`${t.buy} / ${t.sell}`} className=\"flex gap-4 border-b px-4\">\n            {(['buy', 'sell'] as const).map((s) => (\n              <button\n                key={s}\n                type=\"button\"\n                aria-pressed={side === s}\n                onClick={() => setSide(s)}\n                className={cn('relative min-h-12 text-lg font-semibold transition-colors duration-150', side === s ? 'text-foreground' : 'text-muted-foreground hover:text-foreground')}\n              >\n                {t[s]}\n                {side === s && <motion.span layoutId=\"underline\" className=\"absolute inset-x-0 -bottom-px h-0.5 rounded-full bg-foreground\" />}\n              </button>\n            ))}\n          </div>\n        </LayoutGroup>\n\n        <div className=\"space-y-3 p-3\">\n          <LayoutGroup id={`${ids}-outcome`}>\n            <div role=\"group\" aria-label=\"Outcome\" className=\"grid grid-cols-2 gap-1 rounded-2xl bg-muted p-1\">\n              {outcomes.map((o, i) => {\n                const on = pick === o.id\n                return (\n                  <button\n                    key={o.id}\n                    type=\"button\"\n                    aria-pressed={on}\n                    onClick={() => setPick(o.id)}\n                    className={cn(\n                      'relative min-h-12 rounded-xl text-sm font-semibold transition-colors duration-150',\n                      on ? (i === 0 ? 'text-success' : 'text-destructive') : 'text-muted-foreground hover:text-foreground',\n                    )}\n                  >\n                    {on && (\n                      <motion.span\n                        layoutId=\"pill\"\n                        style={{ borderRadius: 12 }}\n                        className={cn('absolute inset-0', i === 0 ? 'bg-success/15' : 'bg-destructive/15')}\n                      />\n                    )}\n                    <span className=\"relative\">\n                      {o.label} {cents(o.price)}\n                    </span>\n                  </button>\n                )\n              })}\n            </div>\n          </LayoutGroup>\n\n          <div className=\"rounded-2xl bg-muted/60 px-4 pt-4 pb-3 text-center\">\n            <label htmlFor={`${ids}-amount`} className=\"text-sm font-medium\">\n              {t.amount}\n            </label>\n            <div className=\"mt-1 flex items-baseline justify-center font-mono text-5xl font-semibold tabular-nums\">\n              <span className=\"text-muted-foreground\">{symbol}</span>\n              <input\n                id={`${ids}-amount`}\n                inputMode=\"decimal\"\n                autoComplete=\"off\"\n                placeholder=\"0\"\n                value={raw}\n                onChange={(e) => setRaw(e.target.value.replace(/[^\\d.,]/g, ''))}\n                style={{ width: `${Math.max(1, raw.length || 1)}ch` }}\n                className=\"min-w-[1ch] bg-transparent text-center outline-none placeholder:text-muted-foreground/50\"\n              />\n            </div>\n            <div className=\"mt-4 flex flex-wrap justify-center gap-1.5\">\n              {CHIPS.map((n) => (\n                <button key={n} type=\"button\" onClick={() => add(n)} className=\"min-h-10 rounded-xl bg-background px-3 text-sm font-semibold transition-[background-color,transform] duration-150 hover:bg-accent active:scale-[0.96]\">\n                  +{symbol}\n                  {n}\n                </button>\n              ))}\n              <button type=\"button\" onClick={() => setRaw(String(balance))} className=\"min-h-10 rounded-xl bg-background px-3 text-sm font-semibold transition-[background-color,transform] duration-150 hover:bg-accent active:scale-[0.96]\">\n                {t.max}\n              </button>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"flex items-end justify-between gap-3 border-t px-4 pt-3\">\n          <div>\n            <p className=\"font-semibold\">{side === 'buy' ? t.toWin : t.toGet}</p>\n            <p className=\"text-sm text-muted-foreground\">\n              {t.avg} {cents(outcome.price)}\n            </p>\n          </div>\n          <output className={cn('font-mono text-3xl font-semibold tabular-nums', result > 0 ? 'text-success' : 'text-muted-foreground')}>\n            {money.format(result)}\n          </output>\n        </div>\n        <div className=\"p-4 pt-3\">\n          <button\n            type=\"submit\"\n            disabled={amount <= 0}\n            className=\"min-h-12 w-full rounded-2xl bg-foreground font-semibold text-background transition-[opacity,transform] duration-150 active:scale-[0.98] disabled:opacity-40\"\n          >\n            {t.trade}\n          </button>\n        </div>\n      </form>\n    </MotionConfig>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "widgets"
  ],
  "type": "registry:component"
}