{"$schema":"https://ui.shadcn.com/schema/registry.json","author":"Kaiyu Hsu <uicapsule@kyh.io>","categories":["controls","skeuomorphism"],"dependencies":["motion"],"description":"The iOS time-picker wheel on the web — 3D cylinder, momentum flicks, detent snapping, edge fog.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { useState } from \"react\";\nimport type { FC } from \"react\";\nimport { motion, useMotionValue, useMotionValueEvent, useTransform } from \"motion/react\";\nimport type { MotionValue } from \"motion/react\";\n\nconst ITEM_HEIGHT = 38;\nconst VISIBLE_ITEMS = 7;\nconst WHEEL_HEIGHT = ITEM_HEIGHT * VISIBLE_ITEMS;\nconst CENTER_OFFSET = (WHEEL_HEIGHT - ITEM_HEIGHT) / 2;\n\nconst HOURS = Array.from({ length: 12 }, (_, index) => String(index + 1));\nconst MINUTES = Array.from({ length: 60 }, (_, index) => String(index).padStart(2, \"0\"));\nconst PERIODS = [\"AM\", \"PM\"];\n\ntype Align = \"left\" | \"center\" | \"right\";\n\nconst ALIGN_CLASS = {\n  center: \"justify-center\",\n  left: \"justify-start pl-2\",\n  right: \"justify-end pr-2\",\n} satisfies Record<Align, string>;\n\ninterface WheelItemProps {\n  y: MotionValue<number>;\n  index: number;\n  label: string;\n  align: Align;\n}\n\nconst WheelItem: FC<WheelItemProps> = ({ y, index, label, align }) => {\n  // Distance of this item from the selection line, in items (-3..3 visible).\n  const distance = useTransform(y, (value) => (value + index * ITEM_HEIGHT) / ITEM_HEIGHT);\n  const rotateX = useTransform(distance, [-3.5, 0, 3.5], [-62, 0, 62]);\n  const opacity = useTransform(distance, [-3.5, -2, 0, 2, 3.5], [0, 0.35, 1, 0.35, 0]);\n  const scale = useTransform(distance, [-3.5, 0, 3.5], [0.82, 1, 0.82]);\n\n  return (\n    <motion.div\n      style={{\n        height: ITEM_HEIGHT,\n        opacity,\n        rotateX,\n        scale,\n        transformPerspective: 780,\n      }}\n      className={`flex items-center text-[21px] text-white tabular-nums ${ALIGN_CLASS[align]}`}\n    >\n      {label}\n    </motion.div>\n  );\n};\n\ninterface WheelProps {\n  items: string[];\n  initialIndex: number;\n  onChange: (index: number) => void;\n  width: string;\n  align: Align;\n}\n\nconst Wheel: FC<WheelProps> = ({ items, initialIndex, onChange, width, align }) => {\n  const y = useMotionValue(-initialIndex * ITEM_HEIGHT);\n\n  useMotionValueEvent(y, \"change\", (value) => {\n    const index = Math.round(-value / ITEM_HEIGHT);\n    onChange(Math.min(items.length - 1, Math.max(0, index)));\n  });\n\n  return (\n    <div className={`relative ${width}`} style={{ height: WHEEL_HEIGHT }}>\n      <div className=\"absolute inset-0 overflow-hidden\" style={{ perspective: 780 }}>\n        <motion.div\n          drag=\"y\"\n          style={{ paddingTop: CENTER_OFFSET, y }}\n          dragConstraints={{\n            bottom: 0,\n            top: -(items.length - 1) * ITEM_HEIGHT,\n          }}\n          dragElastic={0.12}\n          dragTransition={{\n            bounceDamping: 28,\n            bounceStiffness: 320,\n            modifyTarget: (target) => Math.round(target / ITEM_HEIGHT) * ITEM_HEIGHT,\n            power: 0.55,\n            timeConstant: 220,\n          }}\n          className=\"cursor-grab active:cursor-grabbing\"\n        >\n          {items.map((label, index) => (\n            <WheelItem key={label} y={y} index={index} label={label} align={align} />\n          ))}\n        </motion.div>\n      </div>\n    </div>\n  );\n};\n\nexport const DrumPicker = () => {\n  const [hourIndex, setHourIndex] = useState(8);\n  const [minuteIndex, setMinuteIndex] = useState(41);\n  const [periodIndex, setPeriodIndex] = useState(0);\n\n  const hour = HOURS[hourIndex] ?? \"9\";\n  const minute = MINUTES[minuteIndex] ?? \"41\";\n  const period = PERIODS[periodIndex] ?? \"AM\";\n\n  return (\n    <div className=\"w-[340px] rounded-[32px] bg-[#1d0b10] p-6 shadow-2xl shadow-black/60 ring-1 ring-white/10 select-none\">\n      <div className=\"flex items-baseline justify-between\">\n        <p className=\"text-[17px] font-semibold text-white\">Alarm</p>\n        <p className=\"text-[13px] text-white/40 tabular-nums\">\n          {hour}:{minute} {period}\n        </p>\n      </div>\n\n      <div className=\"relative mt-4\">\n        <div\n          aria-hidden\n          className=\"pointer-events-none absolute inset-x-0 top-1/2 -translate-y-1/2 rounded-xl bg-rose-400/[0.1]\"\n          style={{ height: ITEM_HEIGHT }}\n        />\n        <div\n          aria-hidden\n          className=\"pointer-events-none absolute inset-x-0 top-0 z-10 h-16 bg-gradient-to-b from-[#1d0b10] to-transparent\"\n        />\n        <div\n          aria-hidden\n          className=\"pointer-events-none absolute inset-x-0 bottom-0 z-10 h-16 bg-gradient-to-t from-[#1d0b10] to-transparent\"\n        />\n\n        <div className=\"flex items-stretch justify-center gap-1 px-6\">\n          <Wheel\n            items={HOURS}\n            initialIndex={8}\n            onChange={setHourIndex}\n            width=\"w-16\"\n            align=\"right\"\n          />\n          <Wheel\n            items={MINUTES}\n            initialIndex={41}\n            onChange={setMinuteIndex}\n            width=\"w-16\"\n            align=\"center\"\n          />\n          <Wheel\n            items={PERIODS}\n            initialIndex={0}\n            onChange={setPeriodIndex}\n            width=\"w-14\"\n            align=\"left\"\n          />\n        </div>\n      </div>\n\n      <p className=\"mt-4 text-center text-[11px] text-white/30\">Flick a wheel — it snaps to rest</p>\n    </div>\n  );\n};\n","path":"/drum-picker.tsx","target":"uicapsule/drum-picker/drum-picker.tsx","type":"registry:file"},{"content":"\"use client\";\n\nimport { DrumPicker } from \"./drum-picker\";\n\nconst Preview = () => (\n  <main className=\"flex h-dvh items-center justify-center overflow-hidden bg-[radial-gradient(ellipse_60%_45%_at_50%_100%,rgb(239_68_68/0.16)_0%,transparent_70%),radial-gradient(circle_at_50%_35%,#3a0b14_0%,#22060c_50%,#0f0306_100%)] p-5\">\n    <DrumPicker />\n  </main>\n);\n\nexport default Preview;\n","path":"/preview.tsx","target":"uicapsule/drum-picker/preview.tsx","type":"registry:file"}],"homepage":"https://www.uicapsule.com/ui/drum-picker","name":"drum-picker","registryDependencies":[],"title":"Drum Picker","type":"registry:block"}